From fb7a885cae2177447fd2d94d8905ba522af43ef2 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sun, 19 Jan 2020 21:28:38 +0100 Subject: [PATCH] WindowServer: Allow scrolling of menus that don't fit on screen Menus now have a scroll offset (index based, not pixel based) which is controlled either with the mouse wheel or with the up/down arrow keys. This finally allows us to browse all of the fonts that @xTibor has made avilable through his serenity-fontdev project: https://github.com/xTibor/serenity-fontdev I'm not completely sure about the up/down arrows. They feel like maybe they occupy a bit too much vertical space. Also FIXME: this mechanism probably won't look completely right for menus that have separators in them. Fixes #1043. --- Servers/WindowServer/WSMenu.cpp | 147 +++++++++++++++++++--------- Servers/WindowServer/WSMenu.h | 13 ++- Servers/WindowServer/WSMenuItem.cpp | 7 ++ Servers/WindowServer/WSMenuItem.h | 2 +- 4 files changed, 119 insertions(+), 50 deletions(-) diff --git a/Servers/WindowServer/WSMenu.cpp b/Servers/WindowServer/WSMenu.cpp index cfcc88a42d..ac9b9ec774 100644 --- a/Servers/WindowServer/WSMenu.cpp +++ b/Servers/WindowServer/WSMenu.cpp @@ -92,7 +92,7 @@ static const int s_item_icon_width = 16; static const int s_checkbox_or_icon_padding = 6; static const int s_stripe_width = 23; -int WSMenu::width() const +int WSMenu::content_width() const { int widest_text = 0; int widest_shortcut = 0; @@ -114,13 +114,6 @@ int WSMenu::width() const return max(widest_item, rect_in_menubar().width()) + horizontal_padding() + frame_thickness() * 2; } -int WSMenu::height() const -{ - if (m_items.is_empty()) - return 0; - return (m_items.last().rect().bottom() + 1) + frame_thickness(); -} - void WSMenu::redraw() { if (!menu_window()) @@ -131,7 +124,7 @@ void WSMenu::redraw() WSWindow& WSMenu::ensure_menu_window() { - int width = this->width(); + int width = this->content_width(); if (!m_menu_window) { Point next_item_location(frame_thickness(), frame_thickness()); for (auto& item : m_items) { @@ -144,14 +137,32 @@ WSWindow& WSMenu::ensure_menu_window() next_item_location.move_by(0, height); } + int window_height_available = WSScreen::the().height() - WSMenuManager::the().menubar_rect().height() - frame_thickness() * 2; + int max_window_height = (window_height_available / item_height()) * item_height() + frame_thickness() * 2; + int content_height = m_items.is_empty() ? 0 : (m_items.last().rect().bottom() + 1) + frame_thickness(); + int window_height = min(max_window_height, content_height); + if (window_height < content_height) { + m_scrollable = true; + m_max_scroll_offset = item_count() - window_height / item_height() + 2; + } + auto window = WSWindow::construct(*this, WSWindowType::Menu); - window->set_rect(0, 0, width, height()); + window->set_rect(0, 0, width, window_height); m_menu_window = move(window); draw(); } return *m_menu_window; } +int WSMenu::visible_item_count() const +{ + if (!is_scrollable()) + return m_items.size(); + ASSERT(m_menu_window); + // Make space for up/down arrow indicators + return m_menu_window->height() / item_height() - 2; +} + void WSMenu::draw() { auto palette = WSWindowManager::the().palette(); @@ -164,7 +175,7 @@ void WSMenu::draw() Rect rect { {}, menu_window()->size() }; painter.fill_rect(rect.shrunken(6, 6), palette.menu_base()); StylePainter::paint_window_frame(painter, rect, palette); - int width = this->width(); + int width = this->content_width(); if (!s_checked_bitmap) s_checked_bitmap = &CharacterBitmap::create_from_ascii(s_checked_bitmap_data, s_checked_bitmap_width, s_checked_bitmap_height).leak_ref(); @@ -176,11 +187,23 @@ void WSMenu::draw() has_items_with_icon = has_items_with_icon | !!item.icon(); } - Rect stripe_rect { frame_thickness(), frame_thickness(), s_stripe_width, height() - frame_thickness() * 2 }; + Rect stripe_rect { frame_thickness(), frame_thickness(), s_stripe_width, menu_window()->height() - frame_thickness() * 2 }; painter.fill_rect(stripe_rect, palette.menu_stripe()); painter.draw_line(stripe_rect.top_right(), stripe_rect.bottom_right(), palette.menu_stripe().darkened()); - for (auto& item : m_items) { + int visible_item_count = this->visible_item_count(); + + if (is_scrollable()) { + bool can_go_up = m_scroll_offset > 0; + bool can_go_down = m_scroll_offset < m_max_scroll_offset; + Rect up_indicator_rect { frame_thickness(), frame_thickness(), content_width(), item_height() }; + painter.draw_text(up_indicator_rect, "\xc3\xb6", TextAlignment::Center, can_go_up ? palette.menu_base_text() : palette.color(ColorRole::DisabledText)); + Rect down_indicator_rect { frame_thickness(), menu_window()->height() - item_height() - frame_thickness(), content_width(), item_height() }; + painter.draw_text(down_indicator_rect, "\xc3\xb7", TextAlignment::Center, can_go_down ? palette.menu_base_text() : palette.color(ColorRole::DisabledText)); + } + + for (int i = 0; i < visible_item_count; ++i) { + auto& item = m_items.at(m_scroll_offset + i); if (item.type() == WSMenuItem::Text) { Color text_color = palette.menu_base_text(); if (&item == hovered_item() && item.is_enabled()) { @@ -277,34 +300,40 @@ void WSMenu::decend_into_submenu_at_hovered_item() m_in_submenu = true; } +void WSMenu::handle_hover_event(const WSMouseEvent& event) +{ + ASSERT(menu_window()); + auto mouse_event = static_cast(event); + + if (hovered_item() && hovered_item()->is_submenu()) { + + auto item = *hovered_item(); + auto submenu_top_left = item.rect().location() + Point { item.rect().width(), 0 }; + auto submenu_bottom_left = submenu_top_left + Point { 0, item.submenu()->menu_window()->height() }; + + auto safe_hover_triangle = Triangle { m_last_position_in_hover, submenu_top_left, submenu_bottom_left }; + m_last_position_in_hover = mouse_event.position(); + + // Don't update the hovered item if mouse is moving towards a submenu + if (safe_hover_triangle.contains(mouse_event.position())) + return; + } + + int index = item_index_at(mouse_event.position()); + if (m_hovered_item_index == index) + return; + m_hovered_item_index = index; + + // FIXME: Tell parent menu (if it exists) that it is currently in a submenu + m_in_submenu = false; + update_for_new_hovered_item(); + return; +} + void WSMenu::event(CEvent& event) { if (event.type() == WSEvent::MouseMove) { - ASSERT(menu_window()); - auto mouse_event = static_cast(event); - - if (hovered_item() && hovered_item()->is_submenu()) { - - auto item = *hovered_item(); - auto submenu_top_left = item.rect().location() + Point { item.rect().width(), 0 }; - auto submenu_bottom_left = submenu_top_left + Point { 0, item.submenu()->height() }; - - auto safe_hover_triangle = Triangle { m_last_position_in_hover, submenu_top_left, submenu_bottom_left }; - m_last_position_in_hover = mouse_event.position(); - - // Don't update the hovered item if mouse is moving towards a submenu - if (safe_hover_triangle.contains(mouse_event.position())) - return; - } - - int index = item_index_at(mouse_event.position()); - if (m_hovered_item_index == index) - return; - m_hovered_item_index = index; - - // FIXME: Tell parent menu (if it exists) that it is currently in a submenu - m_in_submenu = false; - update_for_new_hovered_item(); + handle_hover_event(static_cast(event)); return; } @@ -313,6 +342,18 @@ void WSMenu::event(CEvent& event) return; } + if (event.type() == WSEvent::MouseWheel && is_scrollable()) { + auto& mouse_event = static_cast(event); + m_scroll_offset += mouse_event.wheel_delta(); + if (m_scroll_offset < 0) + m_scroll_offset = 0; + if (m_scroll_offset >= m_max_scroll_offset) + m_scroll_offset = m_max_scroll_offset; + handle_hover_event(mouse_event); + redraw(); + return; + } + if (event.type() == WSEvent::KeyDown) { auto key = static_cast(event).key(); @@ -347,12 +388,18 @@ void WSMenu::event(CEvent& event) if (key == Key_Up) { ASSERT(m_items.at(0).type() != WSMenuItem::Separator); + if (is_scrollable() && m_hovered_item_index == 0) + return; + do { m_hovered_item_index--; if (m_hovered_item_index < 0) m_hovered_item_index = m_items.size() - 1; } while (hovered_item()->type() == WSMenuItem::Separator); + if (is_scrollable() && m_hovered_item_index < m_scroll_offset) + --m_scroll_offset; + update_for_new_hovered_item(); return; } @@ -360,12 +407,18 @@ void WSMenu::event(CEvent& event) if (key == Key_Down) { ASSERT(m_items.at(0).type() != WSMenuItem::Separator); + if (is_scrollable() && m_hovered_item_index == m_items.size() - 1) + return; + do { m_hovered_item_index++; if (m_hovered_item_index >= m_items.size()) m_hovered_item_index = 0; } while (hovered_item()->type() == WSMenuItem::Separator); + if (is_scrollable() && m_hovered_item_index >= (m_scroll_offset + visible_item_count())) + ++m_scroll_offset; + update_for_new_hovered_item(); return; } @@ -452,16 +505,16 @@ void WSMenu::popup(const Point& position, bool is_submenu) const int margin = 30; Point adjusted_pos = position; - if (window.height() >= WSScreen::the().height()) { - adjusted_pos.set_y(0); - } else { - if (adjusted_pos.x() + window.width() >= WSScreen::the().width() - margin) { - adjusted_pos = adjusted_pos.translated(-window.width(), 0); - } - if (adjusted_pos.y() + window.height() >= WSScreen::the().height() - margin) { - adjusted_pos = adjusted_pos.translated(0, -window.height()); - } + + if (adjusted_pos.x() + window.width() >= WSScreen::the().width() - margin) { + adjusted_pos = adjusted_pos.translated(-window.width(), 0); } + if (adjusted_pos.y() + window.height() >= WSScreen::the().height() - margin) { + adjusted_pos = adjusted_pos.translated(0, -window.height()); + } + + if (adjusted_pos.y() < WSMenuManager::the().menubar_rect().height()) + adjusted_pos.set_y(WSMenuManager::the().menubar_rect().height()); window.move_to(adjusted_pos); window.set_visible(true); diff --git a/Servers/WindowServer/WSMenu.h b/Servers/WindowServer/WSMenu.h index 8aea486501..78dab04495 100644 --- a/Servers/WindowServer/WSMenu.h +++ b/Servers/WindowServer/WSMenu.h @@ -84,8 +84,7 @@ 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; } - int width() const; - int height() const; + int content_width() const; int item_height() const { return 20; } int frame_thickness() const { return 3; } @@ -112,9 +111,15 @@ public: void redraw_if_theme_changed(); + bool is_scrollable() const { return m_scrollable; } + int scroll_offset() const { return m_scroll_offset; } + private: virtual void event(CEvent&) override; + void handle_hover_event(const WSMouseEvent&); + int visible_item_count() const; + int item_index_at(const Point&); int padding_between_text_and_shortcut() const { return 50; } void did_activate(WSMenuItem&); @@ -137,4 +142,8 @@ private: int m_theme_index_at_last_paint { -1 }; int m_hovered_item_index { -1 }; bool m_in_submenu { false }; + + bool m_scrollable { false }; + int m_scroll_offset { 0 }; + int m_max_scroll_offset { 0 }; }; diff --git a/Servers/WindowServer/WSMenuItem.cpp b/Servers/WindowServer/WSMenuItem.cpp index 6cfb302913..14b44df33c 100644 --- a/Servers/WindowServer/WSMenuItem.cpp +++ b/Servers/WindowServer/WSMenuItem.cpp @@ -76,3 +76,10 @@ WSMenu* WSMenuItem::submenu() return m_menu.client()->find_menu_by_id(m_submenu_id); return WSMenuManager::the().find_internal_menu_by_id(m_submenu_id); } + +Rect WSMenuItem::rect() const +{ + if (!m_menu.is_scrollable()) + return m_rect; + return m_rect.translated(0, m_menu.item_height() - (m_menu.scroll_offset() * m_menu.item_height())); +} diff --git a/Servers/WindowServer/WSMenuItem.h b/Servers/WindowServer/WSMenuItem.h index a5190f2c6a..feabb20535 100644 --- a/Servers/WindowServer/WSMenuItem.h +++ b/Servers/WindowServer/WSMenuItem.h @@ -63,7 +63,7 @@ public: void set_shortcut_text(const String& text) { m_shortcut_text = text; } void set_rect(const Rect& rect) { m_rect = rect; } - Rect rect() const { return m_rect; } + Rect rect() const; unsigned identifier() const { return m_identifier; }