1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 20:27:45 +00:00

WindowServer: Add support for default MenuItem

This allows marking a MenuItem as a default action, e.g. in a
context menu for an action that reflects what e.g. a double click
would perform.

Also enhance the window menu to mark the close action as the
default, and when double clicked perform that action.

Fixes #1289
This commit is contained in:
Tom 2020-07-07 12:09:18 -06:00 committed by Andreas Kling
parent 684b04e02a
commit fc4e01a3c9
10 changed files with 194 additions and 32 deletions

View file

@ -648,7 +648,7 @@ void ClientConnection::handle(const Messages::WindowServer::WM_PopupWindowMenu&
return;
}
auto& window = *(*it).value;
window.popup_window_menu(message.screen_position());
window.popup_window_menu(message.screen_position(), WindowMenuDefaultAction::BasedOnWindowState);
}
void ClientConnection::handle(const Messages::WindowServer::WM_StartWindowResize& request)

View file

@ -110,9 +110,10 @@ int Menu::content_width() const
for (auto& item : m_items) {
if (item.type() != MenuItem::Text)
continue;
int text_width = font().width(item.text());
auto& use_font = item.is_default() ? Gfx::Font::default_bold_font() : font();
int text_width = use_font.width(item.text());
if (!item.shortcut_text().is_empty()) {
int shortcut_width = font().width(item.shortcut_text());
int shortcut_width = use_font.width(item.shortcut_text());
widest_shortcut = max(shortcut_width, widest_shortcut);
}
widest_text = max(widest_text, text_width);
@ -248,10 +249,14 @@ void Menu::draw()
icon_rect.center_vertically_within(text_rect);
painter.blit(icon_rect.location(), *item.icon(), item.icon()->rect());
}
auto& previous_font = painter.font();
if (item.is_default())
painter.set_font(Gfx::Font::default_bold_font());
painter.draw_text(text_rect, item.text(), Gfx::TextAlignment::CenterLeft, text_color);
if (!item.shortcut_text().is_empty()) {
painter.draw_text(item.rect().translated(-right_padding(), 0), item.shortcut_text(), Gfx::TextAlignment::CenterRight, text_color);
}
painter.set_font(previous_font);
if (item.is_submenu()) {
static auto& submenu_arrow_bitmap = Gfx::CharacterBitmap::create_from_ascii(s_submenu_arrow_bitmap_data, s_submenu_arrow_bitmap_width, s_submenu_arrow_bitmap_height).leak_ref();
Gfx::IntRect submenu_arrow_rect {
@ -458,6 +463,19 @@ void Menu::did_activate(MenuItem& item)
m_client->post_message(Messages::WindowClient::MenuItemActivated(m_menu_id, item.identifier()));
}
bool Menu::activate_default()
{
for (auto& item : m_items) {
if (item.type() == MenuItem::Type::Separator)
continue;
if (item.is_enabled() && item.is_default()) {
did_activate(item);
return true;
}
}
return false;
}
MenuItem* Menu::item_with_identifier(unsigned identifer)
{
for (auto& item : m_items) {

View file

@ -87,6 +87,8 @@ public:
bool is_window_menu_open() { return m_is_window_menu_open; }
void set_window_menu_open(bool is_open) { m_is_window_menu_open = is_open; }
bool activate_default();
int content_width() const;
int item_height() const { return 20; }

View file

@ -71,6 +71,14 @@ void MenuItem::set_checked(bool checked)
m_menu.redraw();
}
void MenuItem::set_default(bool is_default)
{
if (m_default == is_default)
return;
m_default = is_default;
m_menu.redraw();
}
Menu* MenuItem::submenu()
{
ASSERT(is_submenu());

View file

@ -58,6 +58,9 @@ public:
bool is_checked() const { return m_checked; }
void set_checked(bool);
bool is_default() const { return m_default; }
void set_default(bool);
String text() const { return m_text; }
void set_text(const String& text) { m_text = text; }
@ -88,6 +91,7 @@ private:
bool m_enabled { true };
bool m_checkable { false };
bool m_checked { false };
bool m_default { false };
unsigned m_identifier { 0 };
String m_text;
String m_shortcut_text;

View file

@ -405,7 +405,7 @@ void Window::request_update(const Gfx::IntRect& rect, bool ignore_occlusion)
m_pending_paint_rects.add(rect);
}
void Window::popup_window_menu(const Gfx::IntPoint& position)
void Window::ensure_window_menu()
{
if (!m_window_menu) {
m_window_menu = Menu::construct(nullptr, -1, "(Window Menu)");
@ -422,7 +422,9 @@ void Window::popup_window_menu(const Gfx::IntPoint& position)
m_window_menu->add_item(make<MenuItem>(*m_window_menu, MenuItem::Type::Separator));
auto close_item = make<MenuItem>(*m_window_menu, 3, "Close");
close_item->set_icon(&close_icon());
m_window_menu_close_item = close_item.ptr();
m_window_menu_close_item->set_icon(&close_icon());
m_window_menu_close_item->set_default(true);
m_window_menu->add_item(move(close_item));
m_window_menu->item((int)PopupMenuItem::Minimize).set_enabled(m_minimizable);
@ -447,12 +449,35 @@ void Window::popup_window_menu(const Gfx::IntPoint& position)
}
};
}
}
void Window::popup_window_menu(const Gfx::IntPoint& position, WindowMenuDefaultAction default_action)
{
ensure_window_menu();
if (default_action == WindowMenuDefaultAction::BasedOnWindowState) {
// When clicked on the task bar, determine the default action
if (!is_active() && !is_minimized())
default_action = WindowMenuDefaultAction::None;
else if (is_minimized())
default_action = WindowMenuDefaultAction::Unminimize;
else
default_action = WindowMenuDefaultAction::Minimize;
}
m_window_menu_minimize_item->set_default(default_action == WindowMenuDefaultAction::Minimize || default_action == WindowMenuDefaultAction::Unminimize);
m_window_menu_minimize_item->set_icon(m_minimized ? nullptr : &minimize_icon());
m_window_menu_maximize_item->set_default(default_action == WindowMenuDefaultAction::Maximize || default_action == WindowMenuDefaultAction::Restore);
m_window_menu_maximize_item->set_icon(m_maximized ? &restore_icon() : &maximize_icon());
m_window_menu_close_item->set_default(default_action == WindowMenuDefaultAction::Close);
m_window_menu->popup(position);
}
void Window::window_menu_activate_default()
{
ensure_window_menu();
m_window_menu->activate_default();
}
void Window::request_close()
{
Event close_request(Event::WindowCloseRequest);

View file

@ -62,6 +62,16 @@ enum class PopupMenuItem {
Maximize,
};
enum class WindowMenuDefaultAction {
None = 0,
BasedOnWindowState,
Close,
Minimize,
Unminimize,
Maximize,
Restore
};
class Window final : public Core::Object
, public InlineLinkedListNode<Window> {
C_OBJECT(Window)
@ -70,7 +80,8 @@ public:
Window(Core::Object&, WindowType);
virtual ~Window() override;
void popup_window_menu(const Gfx::IntPoint&);
void popup_window_menu(const Gfx::IntPoint&, WindowMenuDefaultAction);
void window_menu_activate_default();
void request_close();
unsigned wm_event_mask() const { return m_wm_event_mask; }
@ -240,6 +251,7 @@ private:
void update_menu_item_text(PopupMenuItem item);
void update_menu_item_enabled(PopupMenuItem item);
void add_child_window(Window&);
void ensure_window_menu();
ClientConnection* m_client { nullptr };
@ -283,6 +295,7 @@ private:
RefPtr<Menu> m_window_menu;
MenuItem* m_window_menu_minimize_item { nullptr };
MenuItem* m_window_menu_maximize_item { nullptr };
MenuItem* m_window_menu_close_item { nullptr };
int m_minimize_animation_step { -1 };
int m_progress { -1 };
};

View file

@ -370,10 +370,35 @@ void WindowFrame::on_mouse_event(const MouseEvent& event)
if (m_window.type() != WindowType::Normal && m_window.type() != WindowType::Notification)
return;
if (m_window.type() == WindowType::Normal && event.type() == Event::MouseDown && (event.button() == MouseButton::Left || event.button() == MouseButton::Right) && title_bar_icon_rect().contains(event.position())) {
if (m_window.type() == WindowType::Normal && title_bar_icon_rect().contains(event.position())) {
wm.move_to_front_and_make_active(m_window);
m_window.popup_window_menu(title_bar_rect().bottom_left().translated(rect().location()));
return;
if (event.type() == Event::MouseDown && (event.button() == MouseButton::Left || event.button() == MouseButton::Right)) {
// Manually start a potential double click. Since we're opening
// a menu, we will only receive the MouseDown event, so we
// need to record that fact. If the user subsequently clicks
// on the same area, the menu will get closed, and we will
// receive a MouseUp event, but because windows have changed
// we don't get a MouseDoubleClick event. We can however record
// this click, and when we receive the MouseUp event check if
// it would have been considered a double click, if it weren't
// for the fact that we opened and closed a window in the meanwhile
auto& wm = WindowManager::the();
wm.start_menu_doubleclick(m_window, event);
m_window.popup_window_menu(title_bar_rect().bottom_left().translated(rect().location()), WindowMenuDefaultAction::Close);
return;
} else if (event.type() == Event::MouseUp && event.button() == MouseButton::Left) {
// Since the MouseDown event opened a menu, another MouseUp
// from the second click outside the menu wouldn't be considered
// a double click, so let's manually check if it would otherwise
// have been be considered to be one
auto& wm = WindowManager::the();
if (wm.is_menu_doubleclick(m_window, event)) {
// It is a double click, so perform activate the default item
m_window.window_menu_activate_default();
}
return;
}
}
// This is slightly hackish, but expand the title bar rect by two pixels downwards,
@ -394,7 +419,8 @@ void WindowFrame::on_mouse_event(const MouseEvent& event)
}
if (event.type() == Event::MouseDown) {
if (m_window.type() == WindowType::Normal && event.button() == MouseButton::Right) {
m_window.popup_window_menu(event.position().translated(rect().location()));
auto default_action = m_window.is_maximized() ? WindowMenuDefaultAction::Restore : WindowMenuDefaultAction::Maximize;
m_window.popup_window_menu(event.position().translated(rect().location()), default_action);
return;
}
if (m_window.is_movable() && event.button() == MouseButton::Left)

View file

@ -670,6 +670,24 @@ void WindowManager::set_cursor_tracking_button(Button* button)
m_cursor_tracking_button = button ? button->make_weak_ptr() : nullptr;
}
auto WindowManager::DoubleClickInfo::metadata_for_button(MouseButton button) const -> const ClickMetadata&
{
switch (button) {
case MouseButton::Left:
return m_left;
case MouseButton::Right:
return m_right;
case MouseButton::Middle:
return m_middle;
case MouseButton::Back:
return m_back;
case MouseButton::Forward:
return m_forward;
default:
ASSERT_NOT_REACHED();
}
}
auto WindowManager::DoubleClickInfo::metadata_for_button(MouseButton button) -> ClickMetadata&
{
switch (button) {
@ -690,6 +708,59 @@ auto WindowManager::DoubleClickInfo::metadata_for_button(MouseButton button) ->
// #define DOUBLECLICK_DEBUG
bool WindowManager::is_considered_doubleclick(const MouseEvent& event, const DoubleClickInfo::ClickMetadata& metadata) const
{
int elapsed_since_last_click = metadata.clock.elapsed();
if (elapsed_since_last_click < m_double_click_speed) {
auto diff = event.position() - metadata.last_position;
auto distance_travelled_squared = diff.x() * diff.x() + diff.y() * diff.y();
if (distance_travelled_squared <= (m_max_distance_for_double_click * m_max_distance_for_double_click))
return true;
}
return false;
}
void WindowManager::start_menu_doubleclick(Window& window, const MouseEvent& event)
{
// This is a special case. Basically, we're trying to determine whether
// double clicking on the window menu icon happened. In this case, the
// WindowFrame only receives a MouseDown event, and since the window
// menu popus up, it does not see the MouseUp event. But, if they subsequently
// click there again, the menu is closed and we receive a MouseUp event.
// So, in order to be able to detect a double click when a menu is being
// opened by the MouseDown event, we need to consider the MouseDown event
// as a potential double-click trigger
ASSERT(event.type() == Event::MouseDown);
auto& metadata = m_double_click_info.metadata_for_button(event.button());
if (&window != m_double_click_info.m_clicked_window) {
// we either haven't clicked anywhere, or we haven't clicked on this
// window. set the current click window, and reset the timers.
#if defined(DOUBLECLICK_DEBUG)
dbg() << "Initial mousedown on window " << &window << " for menu (previous was " << m_double_click_info.m_clicked_window << ')';
#endif
m_double_click_info.m_clicked_window = window.make_weak_ptr();
m_double_click_info.reset();
}
metadata.last_position = event.position();
metadata.clock.start();
}
bool WindowManager::is_menu_doubleclick(Window& window, const MouseEvent& event) const
{
ASSERT(event.type() == Event::MouseUp);
if (&window != m_double_click_info.m_clicked_window)
return false;
auto& metadata = m_double_click_info.metadata_for_button(event.button());
if (!metadata.clock.is_valid())
return false;
return is_considered_doubleclick(event, metadata);
}
void WindowManager::process_event_for_doubleclick(Window& window, MouseEvent& event)
{
// We only care about button presses (because otherwise it's not a doubleclick, duh!)
@ -707,32 +778,20 @@ void WindowManager::process_event_for_doubleclick(Window& window, MouseEvent& ev
auto& metadata = m_double_click_info.metadata_for_button(event.button());
// if the clock is invalid, we haven't clicked with this button on this
// window yet, so there's nothing to do.
if (!metadata.clock.is_valid()) {
if (!metadata.clock.is_valid() || !is_considered_doubleclick(event, metadata)) {
// either the clock is invalid because we haven't clicked on this
// button on this window yet, so there's nothing to do, or this
// isn't considered to be a double click. either way, restart the
// clock
metadata.clock.start();
} else {
int elapsed_since_last_click = metadata.clock.elapsed();
metadata.clock.start();
if (elapsed_since_last_click < m_double_click_speed) {
auto diff = event.position() - metadata.last_position;
auto distance_travelled_squared = diff.x() * diff.x() + diff.y() * diff.y();
if (distance_travelled_squared > (m_max_distance_for_double_click * m_max_distance_for_double_click)) {
// too far; try again
metadata.clock.start();
} else {
#if defined(DOUBLECLICK_DEBUG)
dbg() << "Transforming MouseUp to MouseDoubleClick (" << elapsed_since_last_click << " < " << m_double_click_speed << ")!";
dbg() << "Transforming MouseUp to MouseDoubleClick (" << elapsed_since_last_click << " < " << m_double_click_speed << ")!";
#endif
event = MouseEvent(Event::MouseDoubleClick, event.position(), event.buttons(), event.button(), event.modifiers(), event.wheel_delta());
// invalidate this now we've delivered a doubleclick, otherwise
// tripleclick will deliver two doubleclick events (incorrectly).
metadata.clock = {};
}
} else {
// too slow; try again
metadata.clock.start();
}
event = MouseEvent(Event::MouseDoubleClick, event.position(), event.buttons(), event.button(), event.modifiers(), event.wheel_delta());
// invalidate this now we've delivered a doubleclick, otherwise
// tripleclick will deliver two doubleclick events (incorrectly).
metadata.clock = {};
}
metadata.last_position = event.position();

View file

@ -172,6 +172,9 @@ public:
void did_popup_a_menu(Badge<Menu>);
void start_menu_doubleclick(Window& window, const MouseEvent& event);
bool is_menu_doubleclick(Window& window, const MouseEvent& event) const;
private:
NonnullRefPtr<Cursor> get_cursor(const String& name);
NonnullRefPtr<Cursor> get_cursor(const String& name, const Gfx::IntPoint& hotspot);
@ -227,6 +230,7 @@ private:
Gfx::IntPoint last_position;
};
const ClickMetadata& metadata_for_button(MouseButton) const;
ClickMetadata& metadata_for_button(MouseButton);
void reset()
@ -247,6 +251,9 @@ private:
ClickMetadata m_back;
ClickMetadata m_forward;
};
bool is_considered_doubleclick(const MouseEvent& event, const DoubleClickInfo::ClickMetadata& metadata) const;
DoubleClickInfo m_double_click_info;
int m_double_click_speed { 0 };
int m_max_distance_for_double_click { 4 };