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:
parent
684b04e02a
commit
fc4e01a3c9
10 changed files with 194 additions and 32 deletions
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 };
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue