Serenity Operating System
1/*
2 * Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2022, networkException <networkexception@serenityos.org>
4 * Copyright (c) 2022, the SerenityOS developers.
5 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 */
8
9#include <AK/FuzzyMatch.h>
10#include <AK/QuickSort.h>
11#include <LibGUI/Action.h>
12#include <LibGUI/ActionGroup.h>
13#include <LibGUI/Application.h>
14#include <LibGUI/BoxLayout.h>
15#include <LibGUI/CommandPalette.h>
16#include <LibGUI/FilteringProxyModel.h>
17#include <LibGUI/Menu.h>
18#include <LibGUI/MenuItem.h>
19#include <LibGUI/Menubar.h>
20#include <LibGUI/Model.h>
21#include <LibGUI/Painter.h>
22#include <LibGUI/TableView.h>
23#include <LibGUI/TextBox.h>
24#include <LibGUI/Widget.h>
25#include <LibGfx/Painter.h>
26
27namespace GUI {
28
29enum class IconFlags : unsigned {
30 Checkable = 0,
31 Exclusive = 1,
32 Checked = 2,
33};
34
35AK_ENUM_BITWISE_OPERATORS(IconFlags);
36
37class ActionIconDelegate final : public GUI::TableCellPaintingDelegate {
38public:
39 virtual ~ActionIconDelegate() override = default;
40
41 bool should_paint(ModelIndex const& index) override
42 {
43 return index.data().is_u32();
44 }
45
46 virtual void paint(GUI::Painter& painter, Gfx::IntRect const& cell_rect, Gfx::Palette const& palette, ModelIndex const& index) override
47 {
48 auto flags = static_cast<IconFlags>(index.data().as_u32());
49 VERIFY(has_flag(flags, IconFlags::Checkable));
50
51 auto exclusive = has_flag(flags, IconFlags::Exclusive);
52 auto checked = has_flag(flags, IconFlags::Checked);
53
54 if (exclusive) {
55 Gfx::IntRect radio_rect { 0, 0, 12, 12 };
56 radio_rect.center_within(cell_rect);
57 Gfx::StylePainter::paint_radio_button(painter, radio_rect, palette, checked, false);
58 } else {
59 Gfx::IntRect radio_rect { 0, 0, 13, 13 };
60 radio_rect.center_within(cell_rect);
61 Gfx::StylePainter::paint_check_box(painter, radio_rect, palette, true, checked, false);
62 }
63 }
64};
65
66class ActionModel final : public GUI::Model {
67public:
68 enum Column {
69 Icon,
70 Text,
71 Menu,
72 Shortcut,
73 __Count,
74 };
75
76 ActionModel(Vector<NonnullRefPtr<GUI::Action>>& actions)
77 : m_actions(actions)
78 {
79 }
80
81 virtual ~ActionModel() override = default;
82
83 virtual int row_count(ModelIndex const& parent_index) const override
84 {
85 if (!parent_index.is_valid())
86 return m_actions.size();
87 return 0;
88 }
89
90 virtual int column_count(ModelIndex const& = ModelIndex()) const override
91 {
92 return Column::__Count;
93 }
94
95 virtual DeprecatedString column_name(int) const override { return {}; }
96
97 virtual ModelIndex index(int row, int column = 0, ModelIndex const& = ModelIndex()) const override
98 {
99 return create_index(row, column, m_actions.at(row).ptr());
100 }
101
102 virtual Variant data(ModelIndex const& index, ModelRole role = ModelRole::Display) const override
103 {
104 if (role != ModelRole::Display)
105 return {};
106
107 auto& action = *static_cast<GUI::Action*>(index.internal_data());
108
109 switch (index.column()) {
110 case Column::Icon:
111 if (action.icon())
112 return *action.icon();
113 if (action.is_checkable()) {
114 auto flags = IconFlags::Checkable;
115
116 if (action.is_checked())
117 flags |= IconFlags::Checked;
118
119 if (action.group() && action.group()->is_exclusive())
120 flags |= IconFlags::Exclusive;
121
122 return (u32)flags;
123 }
124 return "";
125 case Column::Text:
126 return action_text(index);
127 case Column::Menu:
128 return menu_name(index);
129 case Column::Shortcut:
130 if (!action.shortcut().is_valid())
131 return "";
132 return action.shortcut().to_deprecated_string();
133 }
134
135 VERIFY_NOT_REACHED();
136 }
137
138 virtual TriState data_matches(GUI::ModelIndex const& index, GUI::Variant const& term) const override
139 {
140 auto needle = term.as_string();
141 if (needle.is_empty())
142 return TriState::True;
143
144 auto haystack = DeprecatedString::formatted("{} {}", menu_name(index), action_text(index));
145 if (fuzzy_match(needle, haystack).score > 0)
146 return TriState::True;
147 return TriState::False;
148 }
149
150 static DeprecatedString action_text(ModelIndex const& index)
151 {
152 auto& action = *static_cast<GUI::Action*>(index.internal_data());
153
154 return Gfx::parse_ampersand_string(action.text());
155 }
156
157 static DeprecatedString menu_name(ModelIndex const& index)
158 {
159 auto& action = *static_cast<GUI::Action*>(index.internal_data());
160 if (action.menu_items().is_empty())
161 return {};
162
163 auto* menu_item = *action.menu_items().begin();
164 auto* menu = Menu::from_menu_id(menu_item->menu_id());
165 if (!menu)
166 return {};
167
168 return Gfx::parse_ampersand_string(menu->name());
169 }
170
171private:
172 Vector<NonnullRefPtr<GUI::Action>> const& m_actions;
173};
174
175CommandPalette::CommandPalette(GUI::Window& parent_window, ScreenPosition screen_position)
176 : GUI::Dialog(&parent_window, screen_position)
177{
178 set_window_type(GUI::WindowType::Popup);
179 set_window_mode(GUI::WindowMode::Modeless);
180 set_blocks_emoji_input(true);
181 resize(450, 300);
182
183 collect_actions(parent_window);
184
185 auto main_widget = set_main_widget<GUI::Frame>().release_value_but_fixme_should_propagate_errors();
186 main_widget->set_frame_shape(Gfx::FrameShape::Window);
187 main_widget->set_fill_with_background_color(true);
188
189 main_widget->set_layout<GUI::VerticalBoxLayout>(4);
190
191 m_text_box = main_widget->add<GUI::TextBox>();
192 m_table_view = main_widget->add<GUI::TableView>();
193 m_model = adopt_ref(*new ActionModel(m_actions));
194 m_table_view->set_column_headers_visible(false);
195
196 m_filter_model = MUST(GUI::FilteringProxyModel::create(*m_model));
197 m_filter_model->set_filter_term(""sv);
198
199 m_table_view->set_column_painting_delegate(0, make<ActionIconDelegate>());
200 m_table_view->set_model(*m_filter_model);
201 m_table_view->set_focus_proxy(m_text_box);
202
203 m_text_box->on_change = [this] {
204 m_filter_model->set_filter_term(m_text_box->text());
205 if (m_filter_model->row_count() != 0)
206 m_table_view->set_cursor(m_filter_model->index(0, 0), GUI::AbstractView::SelectionUpdate::Set);
207 };
208
209 m_text_box->on_down_pressed = [this] {
210 m_table_view->move_cursor(GUI::AbstractView::CursorMovement::Down, GUI::AbstractView::SelectionUpdate::Set);
211 };
212
213 m_text_box->on_up_pressed = [this] {
214 m_table_view->move_cursor(GUI::AbstractView::CursorMovement::Up, GUI::AbstractView::SelectionUpdate::Set);
215 };
216
217 m_text_box->on_return_pressed = [this] {
218 if (!m_table_view->selection().is_empty())
219 finish_with_index(m_table_view->selection().first());
220 };
221
222 m_table_view->on_activation = [this](GUI::ModelIndex const& filter_index) {
223 finish_with_index(filter_index);
224 };
225
226 m_text_box->set_focus(true);
227}
228
229void CommandPalette::collect_actions(GUI::Window& parent_window)
230{
231 OrderedHashTable<NonnullRefPtr<GUI::Action>> actions;
232
233 auto collect_action_children = [&](Core::Object& action_parent) {
234 action_parent.for_each_child_of_type<GUI::Action>([&](GUI::Action& action) {
235 if (action.is_enabled() && action.is_visible())
236 actions.set(action);
237 return IterationDecision::Continue;
238 });
239 };
240
241 Function<bool(GUI::Action*)> should_show_action = [&](GUI::Action* action) {
242 return action && action->is_enabled() && action->is_visible() && action->shortcut() != Shortcut(Mod_Ctrl | Mod_Shift, Key_A);
243 };
244
245 Function<void(Menu&)> collect_actions_from_menu = [&](Menu& menu) {
246 for (auto& menu_item : menu.items()) {
247 if (menu_item->submenu())
248 collect_actions_from_menu(*menu_item->submenu());
249
250 auto* action = menu_item->action();
251 if (should_show_action(action))
252 actions.set(*action);
253 }
254 };
255
256 for (auto* widget = parent_window.focused_widget(); widget; widget = widget->parent_widget())
257 collect_action_children(*widget);
258
259 collect_action_children(parent_window);
260
261 parent_window.menubar().for_each_menu([&](Menu& menu) {
262 collect_actions_from_menu(menu);
263
264 return IterationDecision::Continue;
265 });
266
267 if (!parent_window.is_modal()) {
268 for (auto const& it : GUI::Application::the()->global_shortcut_actions({})) {
269 if (should_show_action(it.value))
270 actions.set(*it.value);
271 }
272 }
273
274 m_actions.clear();
275 for (auto& action : actions)
276 m_actions.append(action);
277
278 quick_sort(m_actions, [&](auto& a, auto& b) {
279 // FIXME: This is so awkward. Don't be so awkward.
280 return Gfx::parse_ampersand_string(a->text()) < Gfx::parse_ampersand_string(b->text());
281 });
282}
283
284void CommandPalette::finish_with_index(GUI::ModelIndex const& filter_index)
285{
286 if (!filter_index.is_valid())
287 return;
288 auto action_index = m_filter_model->map(filter_index);
289 auto* action = static_cast<GUI::Action*>(action_index.internal_data());
290 VERIFY(action);
291 m_selected_action = action;
292 done(ExecResult::OK);
293}
294
295}