ソースを参照

LibGUI+WindowServer: Make it possible to have checkable GActions.

They show up as checkable GButtons in GToolBar, and with (or without) check
marks in menus.

There are a bunch of places to make use of this. This patch only takes
advantage of it in the FileManager for the view type actions.
Andreas Kling 6 年 前
コミット
8f81a3f9dd

+ 17 - 6
Applications/FileManager/main.cpp

@@ -98,13 +98,24 @@ int main(int argc, char** argv)
         }
     });
 
-    auto view_as_table_action = GAction::create("Table view", { Mod_Ctrl, KeyCode::Key_L }, GraphicsBitmap::load_from_file("/res/icons/16x16/table-view.png"), [&] (const GAction&) {
+    RetainPtr<GAction> view_as_table_action;
+    RetainPtr<GAction> view_as_icons_action;
+
+    view_as_table_action = GAction::create("Table view", { Mod_Ctrl, KeyCode::Key_L }, GraphicsBitmap::load_from_file("/res/icons/16x16/table-view.png"), [&] (const GAction&) {
         directory_view->set_view_mode(DirectoryView::ViewMode::List);
+        view_as_icons_action->set_checked(false);
+        view_as_table_action->set_checked(true);
     });
+    view_as_table_action->set_checkable(true);
+    view_as_table_action->set_checked(false);
 
-    auto view_as_icons_action = GAction::create("Icon view", { Mod_Ctrl, KeyCode::Key_I }, GraphicsBitmap::load_from_file("/res/icons/16x16/icon-view.png"), [&] (const GAction&) {
+    view_as_icons_action = GAction::create("Icon view", { Mod_Ctrl, KeyCode::Key_I }, GraphicsBitmap::load_from_file("/res/icons/16x16/icon-view.png"), [&] (const GAction&) {
         directory_view->set_view_mode(DirectoryView::ViewMode::Icon);
+        view_as_table_action->set_checked(false);
+        view_as_icons_action->set_checked(true);
     });
+    view_as_icons_action->set_checkable(true);
+    view_as_icons_action->set_checked(true);
 
     auto copy_action = GAction::create("Copy", GraphicsBitmap::load_from_file("/res/icons/16x16/edit-copy.png"), [] (const GAction&) {
         dbgprintf("'Copy' action activated!\n");
@@ -138,8 +149,8 @@ int main(int argc, char** argv)
     menubar->add_menu(move(file_menu));
 
     auto view_menu = make<GMenu>("View");
-    view_menu->add_action(view_as_table_action.copy_ref());
-    view_menu->add_action(view_as_icons_action.copy_ref());
+    view_menu->add_action(*view_as_icons_action);
+    view_menu->add_action(*view_as_table_action);
     menubar->add_menu(move(view_menu));
 
     auto go_menu = make<GMenu>("Go");
@@ -165,8 +176,8 @@ int main(int argc, char** argv)
     main_toolbar->add_action(delete_action.copy_ref());
 
     main_toolbar->add_separator();
-    main_toolbar->add_action(view_as_icons_action.copy_ref());
-    main_toolbar->add_action(view_as_table_action.copy_ref());
+    main_toolbar->add_action(*view_as_icons_action);
+    main_toolbar->add_action(*view_as_table_action);
 
     directory_view->on_path_change = [window, location_textbox, &file_system_model, tree_view] (const String& new_path) {
         window->set_title(String::format("FileManager: %s", new_path.characters()));

+ 13 - 0
LibGUI/GAction.cpp

@@ -106,3 +106,16 @@ void GAction::set_enabled(bool enabled)
         item.set_enabled(enabled);
     });
 }
+
+void GAction::set_checked(bool checked)
+{
+    if (m_checked == checked)
+        return;
+    m_checked = checked;
+    for_each_toolbar_button([checked] (GButton& button) {
+        button.set_checked(checked);
+    });
+    for_each_menu_item([checked] (GMenuItem& item) {
+        item.set_checked(checked);
+    });
+}

+ 8 - 0
LibGUI/GAction.h

@@ -59,6 +59,12 @@ public:
     bool is_enabled() const { return m_enabled; }
     void set_enabled(bool);
 
+    bool is_checkable() const { return m_checkable; }
+    void set_checkable(bool checkable) { m_checkable = checkable; }
+
+    bool is_checked() const { ASSERT(is_checkable()); return m_checked; }
+    void set_checked(bool);
+
     void register_button(Badge<GButton>, GButton&);
     void unregister_button(Badge<GButton>, GButton&);
     void register_menu_item(Badge<GMenuItem>, GMenuItem&);
@@ -79,6 +85,8 @@ private:
     RetainPtr<GraphicsBitmap> m_icon;
     GShortcut m_shortcut;
     bool m_enabled { true };
+    bool m_checkable { false };
+    bool m_checked { false };
     ShortcutScope m_scope { ShortcutScope::None };
 
     HashTable<GButton*> m_buttons;

+ 3 - 0
LibGUI/GButton.cpp

@@ -133,6 +133,9 @@ void GButton::set_action(GAction& action)
     m_action = action.make_weak_ptr();
     action.register_button({ }, *this);
     set_enabled(action.is_enabled());
+    set_checkable(action.is_checkable());
+    if (action.is_checkable())
+        set_checked(action.is_checked());
 }
 
 void GButton::set_icon(RetainPtr<GraphicsBitmap>&& icon)

+ 3 - 0
LibGUI/GMenu.cpp

@@ -90,6 +90,9 @@ int GMenu::realize_menu()
             request.menu.menu_id = m_menu_id;
             request.menu.identifier = i;
             request.menu.enabled = action.is_enabled();
+            request.menu.checkable = action.is_checkable();
+            if (action.is_checkable())
+                request.menu.checked = action.is_checked();
             ASSERT(action.text().length() < (ssize_t)sizeof(request.text));
             strcpy(request.text, action.text().characters());
             request.text_length = action.text().length();

+ 15 - 0
LibGUI/GMenuItem.cpp

@@ -16,6 +16,9 @@ GMenuItem::GMenuItem(unsigned menu_id, Retained<GAction>&& action)
 {
     m_action->register_menu_item({ }, *this);
     m_enabled = m_action->is_enabled();
+    m_checkable = m_action->is_checkable();
+    if (m_checkable)
+        m_checked = m_action->is_checked();
 }
 
 GMenuItem::~GMenuItem()
@@ -32,6 +35,15 @@ void GMenuItem::set_enabled(bool enabled)
     update_window_server();
 }
 
+void GMenuItem::set_checked(bool checked)
+{
+    ASSERT(is_checkable());
+    if (m_checked == checked)
+        return;
+    m_checked = checked;
+    update_window_server();
+}
+
 void GMenuItem::update_window_server()
 {
     auto& action = *m_action;
@@ -40,6 +52,9 @@ void GMenuItem::update_window_server()
     request.menu.menu_id = m_menu_id;
     request.menu.identifier = m_identifier;
     request.menu.enabled = action.is_enabled();
+    request.menu.checkable = action.is_checkable();
+    if (action.is_checkable())
+        request.menu.checked = action.is_checked();
     ASSERT(action.text().length() < (ssize_t)sizeof(request.text));
     strcpy(request.text, action.text().characters());
     request.text_length = action.text().length();

+ 8 - 0
LibGUI/GMenuItem.h

@@ -20,6 +20,12 @@ public:
     GAction* action() { return m_action.ptr(); }
     unsigned identifier() const { return m_identifier; }
 
+    bool is_checkable() const { return m_checkable; }
+    void set_checkable(bool checkable) { m_checkable = checkable; }
+
+    bool is_checked() const { return m_checked; }
+    void set_checked(bool);
+
     bool is_enabled() const { return m_enabled; }
     void set_enabled(bool);
 
@@ -33,6 +39,8 @@ private:
     unsigned m_menu_id { 0 };
     unsigned m_identifier { 0 };
     bool m_enabled { true };
+    bool m_checkable { false };
+    bool m_checked { false };
     RetainPtr<GAction> m_action;
 };
 

+ 2 - 0
Servers/WindowServer/WSAPITypes.h

@@ -246,6 +246,8 @@ struct WSAPI_ClientMessage {
             char shortcut_text[32];
             int shortcut_text_length;
             bool enabled;
+            bool checkable;
+            bool checked;
             WSAPI_Point position;
             bool top_anchored;
         } menu;

+ 4 - 1
Servers/WindowServer/WSClientConnection.cpp

@@ -241,7 +241,7 @@ void WSClientConnection::handle_request(const WSAPIAddMenuItemRequest& request)
         return;
     }
     auto& menu = *(*it).value;
-    menu.add_item(make<WSMenuItem>(menu, identifier, request.text(), request.shortcut_text(), request.is_enabled()));
+    menu.add_item(make<WSMenuItem>(menu, identifier, request.text(), request.shortcut_text(), request.is_enabled(), request.is_checkable(), request.is_checked()));
     WSAPI_ServerMessage response;
     response.type = WSAPI_ServerMessage::Type::DidAddMenuItem;
     response.menu.menu_id = menu_id;
@@ -292,6 +292,9 @@ void WSClientConnection::handle_request(const WSAPIUpdateMenuItemRequest& reques
     menu_item->set_text(request.text());
     menu_item->set_shortcut_text(request.shortcut_text());
     menu_item->set_enabled(request.is_enabled());
+    menu_item->set_checkable(request.is_checkable());
+    if (request.is_checkable())
+        menu_item->set_checked(request.is_checked());
     WSAPI_ServerMessage response;
     response.type = WSAPI_ServerMessage::Type::DidUpdateMenuItem;
     response.menu.menu_id = menu_id;

+ 15 - 3
Servers/WindowServer/WSEvent.h

@@ -276,13 +276,15 @@ private:
 
 class WSAPIAddMenuItemRequest : public WSAPIClientRequest {
 public:
-    WSAPIAddMenuItemRequest(int client_id, int menu_id, unsigned identifier, const String& text, const String& shortcut_text, bool enabled)
+    WSAPIAddMenuItemRequest(int client_id, int menu_id, unsigned identifier, const String& text, const String& shortcut_text, bool enabled, bool checkable, bool checked)
         : WSAPIClientRequest(WSEvent::APIAddMenuItemRequest, client_id)
         , m_menu_id(menu_id)
         , m_identifier(identifier)
         , m_text(text)
         , m_shortcut_text(shortcut_text)
         , m_enabled(enabled)
+        , m_checkable(checkable)
+        , m_checked(checked)
     {
     }
 
@@ -291,24 +293,30 @@ public:
     String text() const { return m_text; }
     String shortcut_text() const { return m_shortcut_text; }
     bool is_enabled() const { return m_enabled; }
+    bool is_checkable() const { return m_checkable; }
+    bool is_checked() const { return m_checked; }
 
 private:
     int m_menu_id { 0 };
     unsigned m_identifier { 0 };
     String m_text;
     String m_shortcut_text;
-    bool m_enabled { true };
+    bool m_enabled;
+    bool m_checkable;
+    bool m_checked;
 };
 
 class WSAPIUpdateMenuItemRequest : public WSAPIClientRequest {
 public:
-    WSAPIUpdateMenuItemRequest(int client_id, int menu_id, unsigned identifier, const String& text, const String& shortcut_text, bool enabled)
+    WSAPIUpdateMenuItemRequest(int client_id, int menu_id, unsigned identifier, const String& text, const String& shortcut_text, bool enabled, bool checkable, bool checked)
         : WSAPIClientRequest(WSEvent::APIUpdateMenuItemRequest, client_id)
         , m_menu_id(menu_id)
         , m_identifier(identifier)
         , m_text(text)
         , m_shortcut_text(shortcut_text)
         , m_enabled(enabled)
+        , m_checkable(checkable)
+        , m_checked(checked)
     {
     }
 
@@ -317,6 +325,8 @@ public:
     String text() const { return m_text; }
     String shortcut_text() const { return m_shortcut_text; }
     bool is_enabled() const { return m_enabled; }
+    bool is_checkable() const { return m_checkable; }
+    bool is_checked() const { return m_checked; }
 
 private:
     int m_menu_id { 0 };
@@ -324,6 +334,8 @@ private:
     String m_text;
     String m_shortcut_text;
     bool m_enabled { true };
+    bool m_checkable;
+    bool m_checked;
 };
 
 class WSAPIAddMenuSeparatorRequest : public WSAPIClientRequest {

+ 2 - 2
Servers/WindowServer/WSEventLoop.cpp

@@ -170,12 +170,12 @@ bool WSEventLoop::on_receive_from_client(int client_id, const WSAPI_ClientMessag
     case WSAPI_ClientMessage::Type::AddMenuItem:
         ASSERT(message.text_length < (ssize_t)sizeof(message.text));
         ASSERT(message.menu.shortcut_text_length < (ssize_t)sizeof(message.menu.shortcut_text));
-        post_event(client, make<WSAPIAddMenuItemRequest>(client_id, message.menu.menu_id, message.menu.identifier, String(message.text, message.text_length), String(message.menu.shortcut_text, message.menu.shortcut_text_length), message.menu.enabled));
+        post_event(client, make<WSAPIAddMenuItemRequest>(client_id, message.menu.menu_id, message.menu.identifier, String(message.text, message.text_length), String(message.menu.shortcut_text, message.menu.shortcut_text_length), message.menu.enabled, message.menu.checkable, message.menu.checked));
         break;
     case WSAPI_ClientMessage::Type::UpdateMenuItem:
         ASSERT(message.text_length < (ssize_t)sizeof(message.text));
         ASSERT(message.menu.shortcut_text_length < (ssize_t)sizeof(message.menu.shortcut_text));
-        post_event(client, make<WSAPIUpdateMenuItemRequest>(client_id, message.menu.menu_id, message.menu.identifier, String(message.text, message.text_length), String(message.menu.shortcut_text, message.menu.shortcut_text_length), message.menu.enabled));
+        post_event(client, make<WSAPIUpdateMenuItemRequest>(client_id, message.menu.menu_id, message.menu.identifier, String(message.text, message.text_length), String(message.menu.shortcut_text, message.menu.shortcut_text_length), message.menu.enabled, message.menu.checkable, message.menu.checked));
         break;
     case WSAPI_ClientMessage::Type::AddMenuSeparator:
         post_event(client, make<WSAPIAddMenuSeparatorRequest>(client_id, message.menu.menu_id));

+ 33 - 1
Servers/WindowServer/WSMenu.cpp

@@ -6,6 +6,7 @@
 #include "WSWindowManager.h"
 #include <WindowServer/WSAPITypes.h>
 #include <WindowServer/WSClientConnection.h>
+#include <SharedGraphics/CharacterBitmap.h>
 #include <SharedGraphics/Painter.h>
 #include <SharedGraphics/StylePainter.h>
 #include <SharedGraphics/Font.h>
@@ -26,6 +27,23 @@ const Font& WSMenu::font() const
     return Font::default_font();
 }
 
+static const char* s_checked_bitmap_data = {
+    "         "
+    "      ## "
+    "     ##  "
+    "     ##  "
+    "    ##   "
+    " ## ##   "
+    "  ####   "
+    "   ##    "
+    "         "
+};
+
+static CharacterBitmap* s_checked_bitmap;
+static const int s_checked_bitmap_width = 9;
+static const int s_checked_bitmap_height = 9;
+static const int s_checked_bitmap_padding = 6;
+
 int WSMenu::width() const
 {
     int longest = 0;
@@ -34,6 +52,8 @@ int WSMenu::width() const
             int item_width = font().width(item->text());
             if (!item->shortcut_text().is_empty())
                 item_width += padding_between_text_and_shortcut() + font().width(item->shortcut_text());
+            if (item->is_checkable())
+                item_width += s_checked_bitmap_width + s_checked_bitmap_padding;
 
             longest = max(longest, item_width);
         }
@@ -92,6 +112,9 @@ void WSMenu::draw()
     StylePainter::paint_menu_frame(painter, rect);
     int width = this->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();
+
     for (auto& item : m_items) {
         if (item->type() == WSMenuItem::Text) {
             Color text_color = Color::Black;
@@ -101,7 +124,16 @@ void WSMenu::draw()
             }
             if (!item->is_enabled())
                 text_color = Color::MidGray;
-            painter.draw_text(item->rect().translated(left_padding(), 0), item->text(), TextAlignment::CenterLeft, text_color);
+            Rect text_rect = item->rect().translated(left_padding(), 0);
+            if (item->is_checkable()) {
+                if (item->is_checked()) {
+                    Rect checkmark_rect { text_rect.location().x(), 0, s_checked_bitmap_width, s_checked_bitmap_height };
+                    checkmark_rect.center_vertically_within(text_rect);
+                    painter.draw_bitmap(checkmark_rect.location(), *s_checked_bitmap, Color::Black);
+                }
+                text_rect.move_by(s_checked_bitmap_width + s_checked_bitmap_padding, 0);
+            }
+            painter.draw_text(text_rect, item->text(), TextAlignment::CenterLeft, text_color);
             if (!item->shortcut_text().is_empty()) {
                 painter.draw_text(item->rect().translated(-right_padding(), 0), item->shortcut_text(), TextAlignment::CenterRight, text_color);
             }

+ 11 - 1
Servers/WindowServer/WSMenuItem.cpp

@@ -1,10 +1,12 @@
 #include "WSMenuItem.h"
 #include "WSMenu.h"
 
-WSMenuItem::WSMenuItem(WSMenu& menu, unsigned identifier, const String& text, const String& shortcut_text, bool enabled)
+WSMenuItem::WSMenuItem(WSMenu& menu, unsigned identifier, const String& text, const String& shortcut_text, bool enabled, bool checkable, bool checked)
     : m_menu(menu)
     , m_type(Text)
     , m_enabled(enabled)
+    , m_checkable(checkable)
+    , m_checked(checked)
     , m_identifier(identifier)
     , m_text(text)
     , m_shortcut_text(shortcut_text)
@@ -28,3 +30,11 @@ void WSMenuItem::set_enabled(bool enabled)
     m_enabled = enabled;
     m_menu.redraw();
 }
+
+void WSMenuItem::set_checked(bool checked)
+{
+    if (m_checked == checked)
+        return;
+    m_checked = checked;
+    m_menu.redraw();
+}

+ 9 - 1
Servers/WindowServer/WSMenuItem.h

@@ -14,7 +14,7 @@ public:
         Separator,
     };
 
-    WSMenuItem(WSMenu&, unsigned identifier, const String& text, const String& shortcut_text = { }, bool enabled = true);
+    WSMenuItem(WSMenu&, unsigned identifier, const String& text, const String& shortcut_text = { }, bool enabled = true, bool checkable = false, bool checked = false);
     WSMenuItem(WSMenu&, Type);
     ~WSMenuItem();
 
@@ -23,6 +23,12 @@ public:
     bool is_enabled() const { return m_enabled; }
     void set_enabled(bool);
 
+    bool is_checkable() const { return m_checkable; }
+    void set_checkable(bool checkable) { m_checkable = checkable; }
+
+    bool is_checked() const { return m_checked; }
+    void set_checked(bool);
+
     String text() const { return m_text; }
     void set_text(const String& text) { m_text = text; }
 
@@ -38,6 +44,8 @@ private:
     WSMenu& m_menu;
     Type m_type { None };
     bool m_enabled { true };
+    bool m_checkable { false };
+    bool m_checked { false };
     unsigned m_identifier { 0 };
     String m_text;
     String m_shortcut_text;

+ 2 - 2
SharedGraphics/StylePainter.cpp

@@ -58,7 +58,7 @@ void StylePainter::paint_button(Painter& painter, const Rect& rect, ButtonStyle
     if (button_style == ButtonStyle::Normal)
         return paint_button_new(painter, rect, pressed, checked, hovered);
 
-    Color button_color = Color::LightGray;
+    Color button_color = checked ? Color::from_rgb(0xd6d2ce) : Color::LightGray;
     Color highlight_color = Color::White;
     Color shadow_color = Color(96, 96, 96);
 
@@ -71,7 +71,7 @@ void StylePainter::paint_button(Painter& painter, const Rect& rect, ButtonStyle
     PainterStateSaver saver(painter);
     painter.translate(rect.location());
 
-    if (pressed) {
+    if (pressed || checked) {
         // Base
         painter.fill_rect({ 1, 1, rect.width() - 2, rect.height() - 2 }, button_color);