diff --git a/Applications/FileManager/main.cpp b/Applications/FileManager/main.cpp index d204017e6f..869556476d 100644 --- a/Applications/FileManager/main.cpp +++ b/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&) { - directory_view->set_view_mode(DirectoryView::ViewMode::List); - }); + RetainPtr view_as_table_action; + RetainPtr view_as_icons_action; - 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&) { - directory_view->set_view_mode(DirectoryView::ViewMode::Icon); + 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); + + 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("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("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())); diff --git a/LibGUI/GAction.cpp b/LibGUI/GAction.cpp index a402215a29..90a63260f6 100644 --- a/LibGUI/GAction.cpp +++ b/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); + }); +} diff --git a/LibGUI/GAction.h b/LibGUI/GAction.h index 1d8d65adce..7dafc464ec 100644 --- a/LibGUI/GAction.h +++ b/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&); void unregister_button(Badge, GButton&); void register_menu_item(Badge, GMenuItem&); @@ -79,6 +85,8 @@ private: RetainPtr m_icon; GShortcut m_shortcut; bool m_enabled { true }; + bool m_checkable { false }; + bool m_checked { false }; ShortcutScope m_scope { ShortcutScope::None }; HashTable m_buttons; diff --git a/LibGUI/GButton.cpp b/LibGUI/GButton.cpp index 534327df78..7ca61b85ea 100644 --- a/LibGUI/GButton.cpp +++ b/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&& icon) diff --git a/LibGUI/GMenu.cpp b/LibGUI/GMenu.cpp index 1bc0fc4f9a..2a1864802d 100644 --- a/LibGUI/GMenu.cpp +++ b/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(); diff --git a/LibGUI/GMenuItem.cpp b/LibGUI/GMenuItem.cpp index 8cbba6d943..671df2d631 100644 --- a/LibGUI/GMenuItem.cpp +++ b/LibGUI/GMenuItem.cpp @@ -16,6 +16,9 @@ GMenuItem::GMenuItem(unsigned menu_id, Retained&& 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(); diff --git a/LibGUI/GMenuItem.h b/LibGUI/GMenuItem.h index 0cda499e2d..e470424eef 100644 --- a/LibGUI/GMenuItem.h +++ b/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 m_action; }; diff --git a/Servers/WindowServer/WSAPITypes.h b/Servers/WindowServer/WSAPITypes.h index 5c1cd50a2f..a22ed4bc91 100644 --- a/Servers/WindowServer/WSAPITypes.h +++ b/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; diff --git a/Servers/WindowServer/WSClientConnection.cpp b/Servers/WindowServer/WSClientConnection.cpp index b52bcf00e4..78826f0fa6 100644 --- a/Servers/WindowServer/WSClientConnection.cpp +++ b/Servers/WindowServer/WSClientConnection.cpp @@ -241,7 +241,7 @@ void WSClientConnection::handle_request(const WSAPIAddMenuItemRequest& request) return; } auto& menu = *(*it).value; - menu.add_item(make(menu, identifier, request.text(), request.shortcut_text(), request.is_enabled())); + menu.add_item(make(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; diff --git a/Servers/WindowServer/WSEvent.h b/Servers/WindowServer/WSEvent.h index e1a9d80fb0..d8fe0cd9ca 100644 --- a/Servers/WindowServer/WSEvent.h +++ b/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 { diff --git a/Servers/WindowServer/WSEventLoop.cpp b/Servers/WindowServer/WSEventLoop.cpp index 9c3d19654d..8a2e0a28aa 100644 --- a/Servers/WindowServer/WSEventLoop.cpp +++ b/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(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(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(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(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(client_id, message.menu.menu_id)); diff --git a/Servers/WindowServer/WSMenu.cpp b/Servers/WindowServer/WSMenu.cpp index 877f074c48..983c1527d9 100644 --- a/Servers/WindowServer/WSMenu.cpp +++ b/Servers/WindowServer/WSMenu.cpp @@ -6,6 +6,7 @@ #include "WSWindowManager.h" #include #include +#include #include #include #include @@ -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); } diff --git a/Servers/WindowServer/WSMenuItem.cpp b/Servers/WindowServer/WSMenuItem.cpp index 95823c86ac..94db473dec 100644 --- a/Servers/WindowServer/WSMenuItem.cpp +++ b/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(); +} diff --git a/Servers/WindowServer/WSMenuItem.h b/Servers/WindowServer/WSMenuItem.h index 263114648e..c725a43c14 100644 --- a/Servers/WindowServer/WSMenuItem.h +++ b/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; diff --git a/SharedGraphics/StylePainter.cpp b/SharedGraphics/StylePainter.cpp index 522a526868..338b9bf226 100644 --- a/SharedGraphics/StylePainter.cpp +++ b/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);