From 3e7e52c5f04a05dcabf53657ef5c36804fda60e1 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Thu, 27 Jan 2022 19:49:51 +0100 Subject: [PATCH] LibGUI: Add a universally available "command palette" to GUI programs You can now press Ctrl+Shift+A in any LibGUI application and we'll give you a command palette dialog where you can search for context-relevant actions by name (via the keyboard.) The set of actions is currently the same one you'd access via keyboard shortcuts. In the future, we'll probably want to add APIs to allow richer integrations with this feature. --- Userland/Libraries/LibGUI/Application.h | 2 + Userland/Libraries/LibGUI/CMakeLists.txt | 1 + Userland/Libraries/LibGUI/CommandPalette.cpp | 181 ++++++++++++++++++ Userland/Libraries/LibGUI/CommandPalette.h | 37 ++++ Userland/Libraries/LibGUI/Forward.h | 1 + .../LibGUI/WindowServerConnection.cpp | 14 +- 6 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 Userland/Libraries/LibGUI/CommandPalette.cpp create mode 100644 Userland/Libraries/LibGUI/CommandPalette.h diff --git a/Userland/Libraries/LibGUI/Application.h b/Userland/Libraries/LibGUI/Application.h index 09b1f50a22..04dcbe3b75 100644 --- a/Userland/Libraries/LibGUI/Application.h +++ b/Userland/Libraries/LibGUI/Application.h @@ -84,6 +84,8 @@ public: Function on_action_enter; Function on_action_leave; + auto const& global_shortcut_actions(Badge) const { return m_global_shortcut_actions; } + private: Application(int argc, char** argv, Core::EventLoop::MakeInspectable = Core::EventLoop::MakeInspectable::No); Application(Main::Arguments const& arguments, Core::EventLoop::MakeInspectable inspectable = Core::EventLoop::MakeInspectable::No) diff --git a/Userland/Libraries/LibGUI/CMakeLists.txt b/Userland/Libraries/LibGUI/CMakeLists.txt index 44c1d6c498..47b911c8d0 100644 --- a/Userland/Libraries/LibGUI/CMakeLists.txt +++ b/Userland/Libraries/LibGUI/CMakeLists.txt @@ -27,6 +27,7 @@ set(SOURCES CommonLocationsProvider.cpp ComboBox.cpp Command.cpp + CommandPalette.cpp Desktop.cpp Dialog.cpp DisplayLink.cpp diff --git a/Userland/Libraries/LibGUI/CommandPalette.cpp b/Userland/Libraries/LibGUI/CommandPalette.cpp new file mode 100644 index 0000000000..dcddb2826f --- /dev/null +++ b/Userland/Libraries/LibGUI/CommandPalette.cpp @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2022, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace GUI { + +class ActionModel final : public GUI::Model { +public: + enum Column { + Text, + Shortcut, + __Count, + }; + + ActionModel(NonnullRefPtrVector& actions) + : m_actions(actions) + { + } + + virtual ~ActionModel() override { } + + virtual int row_count(ModelIndex const& parent_index) const override + { + if (!parent_index.is_valid()) + return m_actions.size(); + return 0; + } + + virtual int column_count(ModelIndex const& = ModelIndex()) const override + { + return Column::__Count; + } + + virtual String column_name(int) const override { return {}; } + + virtual ModelIndex index(int row, int column = 0, ModelIndex const& = ModelIndex()) const override + { + return create_index(row, column, m_actions.ptr_at(row).ptr()); + } + + virtual Variant data(ModelIndex const& index, ModelRole role = ModelRole::Display) const override + { + if (role != ModelRole::Display) + return {}; + + auto& action = *static_cast(index.internal_data()); + + switch (index.column()) { + case Column::Text: + return Gfx::parse_ampersand_string(action.text()); + case Column::Shortcut: + if (!action.shortcut().is_valid()) + return ""; + return action.shortcut().to_string(); + } + + VERIFY_NOT_REACHED(); + } + + virtual TriState data_matches(GUI::ModelIndex const& index, GUI::Variant const& term) const override + { + auto& action = *static_cast(index.internal_data()); + auto text = Gfx::parse_ampersand_string(action.text()); + if (text.contains(term.as_string(), CaseSensitivity::CaseInsensitive)) + return TriState::True; + return TriState::False; + } + +private: + NonnullRefPtrVector const& m_actions; +}; + +CommandPalette::CommandPalette(GUI::Window& parent_window, ScreenPosition screen_position) + : GUI::Dialog(&parent_window, screen_position) +{ + set_frameless(true); + resize(400, 300); + + collect_actions(parent_window); + + auto& main_widget = set_main_widget(); + main_widget.set_frame_shadow(Gfx::FrameShadow::Raised); + main_widget.set_fill_with_background_color(true); + + auto& layout = main_widget.set_layout(); + layout.set_margins(4); + + m_text_box = main_widget.add(); + m_table_view = main_widget.add(); + m_model = adopt_ref(*new ActionModel(m_actions)); + m_table_view->set_column_headers_visible(false); + + m_filter_model = MUST(GUI::FilteringProxyModel::create(*m_model)); + m_filter_model->set_filter_term(""); + + m_table_view->set_model(*m_filter_model); + + m_text_box->on_change = [this] { + m_filter_model->set_filter_term(m_text_box->text()); + if (m_filter_model->row_count() != 0) + m_table_view->set_cursor(m_filter_model->index(0, 0), GUI::AbstractView::SelectionUpdate::Set); + }; + + m_text_box->on_down_pressed = [this] { + m_table_view->move_cursor(GUI::AbstractView::CursorMovement::Down, GUI::AbstractView::SelectionUpdate::Set); + }; + + m_text_box->on_up_pressed = [this] { + m_table_view->move_cursor(GUI::AbstractView::CursorMovement::Up, GUI::AbstractView::SelectionUpdate::Set); + }; + + m_text_box->on_return_pressed = [this] { + if (!m_table_view->selection().is_empty()) + finish_with_index(m_table_view->selection().first()); + }; + + m_table_view->on_activation = [this](GUI::ModelIndex const& filter_index) { + finish_with_index(filter_index); + }; + + m_text_box->set_focus(true); +} + +CommandPalette::~CommandPalette() +{ +} + +void CommandPalette::collect_actions(GUI::Window& parent_window) +{ + OrderedHashTable> actions; + + auto collect_action_children = [&](Core::Object& action_parent) { + action_parent.for_each_child_of_type([&](GUI::Action& action) { + if (action.is_enabled()) + actions.set(action); + return IterationDecision::Continue; + }); + }; + + for (auto* widget = parent_window.focused_widget(); widget; widget = widget->parent_widget()) + collect_action_children(*widget); + + collect_action_children(parent_window); + + if (!parent_window.is_modal()) { + for (auto const& it : GUI::Application::the()->global_shortcut_actions({})) { + if (it.value->is_enabled()) + actions.set(*it.value); + } + } + + m_actions.clear(); + for (auto& action : actions) + m_actions.append(action); +} + +void CommandPalette::finish_with_index(GUI::ModelIndex const& filter_index) +{ + if (!filter_index.is_valid()) + return; + auto action_index = m_filter_model->map(filter_index); + auto* action = static_cast(action_index.internal_data()); + VERIFY(action); + m_selected_action = action; + done(ExecOK); +} + +} diff --git a/Userland/Libraries/LibGUI/CommandPalette.h b/Userland/Libraries/LibGUI/CommandPalette.h new file mode 100644 index 0000000000..202dea1973 --- /dev/null +++ b/Userland/Libraries/LibGUI/CommandPalette.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace GUI { + +class CommandPalette final : public GUI::Dialog { + C_OBJECT(CommandPalette); + +public: + GUI::Action* selected_action() { return m_selected_action; } + GUI::Action const* selected_action() const { return m_selected_action; } + +private: + explicit CommandPalette(GUI::Window& parent_window, ScreenPosition screen_position = CenterWithinParent); + virtual ~CommandPalette() override; + + void collect_actions(GUI::Window& parent_window); + void finish_with_index(GUI::ModelIndex const&); + + RefPtr m_selected_action; + NonnullRefPtrVector m_actions; + + RefPtr m_text_box; + RefPtr m_table_view; + RefPtr m_model; + RefPtr m_filter_model; +}; + +} diff --git a/Userland/Libraries/LibGUI/Forward.h b/Userland/Libraries/LibGUI/Forward.h index 34a050eab6..1bec505b86 100644 --- a/Userland/Libraries/LibGUI/Forward.h +++ b/Userland/Libraries/LibGUI/Forward.h @@ -22,6 +22,7 @@ class Button; class CheckBox; class ComboBox; class Command; +class CommandPalette; class DragEvent; class DropEvent; class EditingEngine; diff --git a/Userland/Libraries/LibGUI/WindowServerConnection.cpp b/Userland/Libraries/LibGUI/WindowServerConnection.cpp index 75812ed9e5..dcd7a015f7 100644 --- a/Userland/Libraries/LibGUI/WindowServerConnection.cpp +++ b/Userland/Libraries/LibGUI/WindowServerConnection.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021, Andreas Kling + * Copyright (c) 2018-2022, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -194,6 +195,17 @@ void WindowServerConnection::key_down(i32 window_id, u32 code_point, u32 key, u3 key_event->m_code_point = emoji_code_point; } + // FIXME: This shortcut should be configurable. + if (modifiers == (Mod_Ctrl | Mod_Shift) && key == Key_A) { + auto command_palette = CommandPalette::construct(*window); + if (command_palette->exec() != GUI::Dialog::ExecOK) + return; + auto* action = command_palette->selected_action(); + VERIFY(action); + action->activate(); + return; + } + Core::EventLoop::current().post_event(*window, move(key_event)); }