1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-24 16:47:42 +00:00

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.
This commit is contained in:
Andreas Kling 2022-01-27 19:49:51 +01:00
parent e0b60d56a5
commit 3e7e52c5f0
6 changed files with 235 additions and 1 deletions

View file

@ -84,6 +84,8 @@ public:
Function<void(Action&)> on_action_enter;
Function<void(Action&)> on_action_leave;
auto const& global_shortcut_actions(Badge<GUI::CommandPalette>) 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)

View file

@ -27,6 +27,7 @@ set(SOURCES
CommonLocationsProvider.cpp
ComboBox.cpp
Command.cpp
CommandPalette.cpp
Desktop.cpp
Dialog.cpp
DisplayLink.cpp

View file

@ -0,0 +1,181 @@
/*
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGUI/Action.h>
#include <LibGUI/Application.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/CommandPalette.h>
#include <LibGUI/FilteringProxyModel.h>
#include <LibGUI/Model.h>
#include <LibGUI/TableView.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/Widget.h>
#include <LibGfx/Painter.h>
namespace GUI {
class ActionModel final : public GUI::Model {
public:
enum Column {
Text,
Shortcut,
__Count,
};
ActionModel(NonnullRefPtrVector<GUI::Action>& 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<GUI::Action*>(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<GUI::Action*>(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<GUI::Action> 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<GUI::Frame>();
main_widget.set_frame_shadow(Gfx::FrameShadow::Raised);
main_widget.set_fill_with_background_color(true);
auto& layout = main_widget.set_layout<GUI::VerticalBoxLayout>();
layout.set_margins(4);
m_text_box = main_widget.add<GUI::TextBox>();
m_table_view = main_widget.add<GUI::TableView>();
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<NonnullRefPtr<GUI::Action>> actions;
auto collect_action_children = [&](Core::Object& action_parent) {
action_parent.for_each_child_of_type<GUI::Action>([&](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<GUI::Action*>(action_index.internal_data());
VERIFY(action);
m_selected_action = action;
done(ExecOK);
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGUI/Dialog.h>
#include <LibGUI/FilteringProxyModel.h>
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<GUI::Action> m_selected_action;
NonnullRefPtrVector<GUI::Action> m_actions;
RefPtr<GUI::TextBox> m_text_box;
RefPtr<GUI::TableView> m_table_view;
RefPtr<GUI::Model> m_model;
RefPtr<GUI::FilteringProxyModel> m_filter_model;
};
}

View file

@ -22,6 +22,7 @@ class Button;
class CheckBox;
class ComboBox;
class Command;
class CommandPalette;
class DragEvent;
class DropEvent;
class EditingEngine;

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -10,6 +10,7 @@
#include <LibCore/MimeData.h>
#include <LibGUI/Action.h>
#include <LibGUI/Application.h>
#include <LibGUI/CommandPalette.h>
#include <LibGUI/Desktop.h>
#include <LibGUI/DisplayLink.h>
#include <LibGUI/DragOperation.h>
@ -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));
}