Serenity Operating System
1/*
2 * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2022, the SerenityOS developers.
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include <AK/DeprecatedString.h>
9#include <AK/StringBuilder.h>
10#include <LibCore/EventLoop.h>
11#include <LibGUI/Action.h>
12#include <LibGUI/ActionGroup.h>
13#include <LibGUI/Application.h>
14#include <LibGUI/BoxLayout.h>
15#include <LibGUI/Button.h>
16#include <LibGUI/Painter.h>
17#include <LibGUI/SeparatorWidget.h>
18#include <LibGUI/Toolbar.h>
19#include <LibGfx/Palette.h>
20
21REGISTER_WIDGET(GUI, Toolbar)
22
23namespace GUI {
24
25Toolbar::Toolbar(Orientation orientation, int button_size)
26 : m_orientation(orientation)
27 , m_button_size(button_size)
28{
29 REGISTER_BOOL_PROPERTY("collapsible", is_collapsible, set_collapsible);
30 REGISTER_BOOL_PROPERTY("grouped", is_grouped, set_grouped);
31
32 if (m_orientation == Orientation::Horizontal)
33 set_fixed_height(button_size);
34 else
35 set_fixed_width(button_size);
36
37 set_layout<BoxLayout>(orientation, GUI::Margins { 2, 2, 2, 2 }, 0);
38}
39
40class ToolbarButton final : public Button {
41 C_OBJECT(ToolbarButton);
42
43public:
44 virtual ~ToolbarButton() override = default;
45
46private:
47 explicit ToolbarButton(Action& action)
48 {
49 if (action.group() && action.group()->is_exclusive())
50 set_exclusive(true);
51 set_action(action);
52 set_tooltip(tooltip(action));
53 set_focus_policy(FocusPolicy::NoFocus);
54 if (action.icon())
55 set_icon(action.icon());
56 else
57 set_text(String::from_deprecated_string(action.text()).release_value_but_fixme_should_propagate_errors());
58 set_button_style(Gfx::ButtonStyle::Coolbar);
59 }
60
61 virtual void set_text(String text) override
62 {
63 auto const* action = this->action();
64 VERIFY(action);
65
66 set_tooltip(tooltip(*action));
67 if (!action->icon())
68 Button::set_text(move(text));
69 }
70
71 DeprecatedString tooltip(Action const& action) const
72 {
73 StringBuilder builder;
74 builder.append(action.tooltip());
75 if (action.shortcut().is_valid()) {
76 builder.append(" ("sv);
77 builder.append(action.shortcut().to_deprecated_string());
78 builder.append(')');
79 }
80 return builder.to_deprecated_string();
81 }
82
83 virtual void enter_event(Core::Event& event) override
84 {
85 auto* app = Application::the();
86 if (app && action())
87 Core::EventLoop::current().post_event(*app, make<ActionEvent>(ActionEvent::Type::ActionEnter, *action()));
88 return Button::enter_event(event);
89 }
90
91 virtual void leave_event(Core::Event& event) override
92 {
93 auto* app = Application::the();
94 if (app && action())
95 Core::EventLoop::current().post_event(*app, make<ActionEvent>(ActionEvent::Type::ActionLeave, *action()));
96 return Button::leave_event(event);
97 }
98};
99
100ErrorOr<NonnullRefPtr<GUI::Button>> Toolbar::try_add_action(Action& action)
101{
102 auto item = TRY(adopt_nonnull_own_or_enomem(new (nothrow) Item));
103 item->type = Item::Type::Action;
104 item->action = action;
105
106 // NOTE: Grow the m_items capacity before potentially adding a child widget.
107 // This avoids having to untangle the child widget in case of allocation failure.
108 TRY(m_items.try_ensure_capacity(m_items.size() + 1));
109
110 item->widget = TRY(try_add<ToolbarButton>(action));
111 item->widget->set_fixed_size(m_button_size, m_button_size);
112
113 m_items.unchecked_append(move(item));
114 return *static_cast<Button*>(m_items.last()->widget.ptr());
115}
116
117GUI::Button& Toolbar::add_action(Action& action)
118{
119 auto button = MUST(try_add_action(action));
120 return *button;
121}
122
123ErrorOr<void> Toolbar::try_add_separator()
124{
125 // NOTE: Grow the m_items capacity before potentially adding a child widget.
126 TRY(m_items.try_ensure_capacity(m_items.size() + 1));
127
128 auto item = TRY(adopt_nonnull_own_or_enomem(new (nothrow) Item));
129 item->type = Item::Type::Separator;
130 item->widget = TRY(try_add<SeparatorWidget>(m_orientation == Gfx::Orientation::Horizontal ? Gfx::Orientation::Vertical : Gfx::Orientation::Horizontal));
131 m_items.unchecked_append(move(item));
132 return {};
133}
134
135void Toolbar::add_separator()
136{
137 MUST(try_add_separator());
138}
139
140void Toolbar::paint_event(PaintEvent& event)
141{
142 Painter painter(*this);
143 painter.add_clip_rect(event.rect());
144
145 painter.fill_rect(event.rect(), palette().button());
146}
147
148Optional<UISize> Toolbar::calculated_preferred_size() const
149{
150 if (m_orientation == Gfx::Orientation::Horizontal)
151 return { { SpecialDimension::Grow, SpecialDimension::Fit } };
152 else
153 return { { SpecialDimension::Fit, SpecialDimension::Grow } };
154 VERIFY_NOT_REACHED();
155}
156
157Optional<UISize> Toolbar::calculated_min_size() const
158{
159 if (m_collapsible) {
160 if (m_orientation == Gfx::Orientation::Horizontal)
161 return UISize(m_button_size, SpecialDimension::Shrink);
162 else
163 return UISize(SpecialDimension::Shrink, m_button_size);
164 }
165 VERIFY(layout());
166 return { layout()->min_size() };
167}
168
169ErrorOr<void> Toolbar::create_overflow_objects()
170{
171 m_overflow_action = Action::create("Overflow Menu", { Mod_Ctrl | Mod_Shift, Key_O }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/overflow-menu.png"sv)), [&](auto&) {
172 m_overflow_menu->popup(m_overflow_button->screen_relative_rect().bottom_left(), {}, m_overflow_button->rect());
173 });
174 m_overflow_action->set_status_tip("Show hidden toolbar actions");
175 m_overflow_action->set_enabled(false);
176
177 TRY(add_spacer());
178
179 m_overflow_button = TRY(try_add_action(*m_overflow_action));
180 m_overflow_button->set_visible(false);
181
182 return {};
183}
184
185ErrorOr<void> Toolbar::update_overflow_menu()
186{
187 if (!m_collapsible)
188 return {};
189
190 Optional<size_t> marginal_index {};
191 auto position { 0 };
192 auto is_horizontal { m_orientation == Gfx::Orientation::Horizontal };
193 auto margin { is_horizontal ? layout()->margins().horizontal_total() : layout()->margins().vertical_total() };
194 auto spacing { layout()->spacing() };
195 auto toolbar_size { is_horizontal ? width() : height() };
196
197 for (size_t i = 0; i < m_items.size() - 1; ++i) {
198 auto& item = m_items.at(i);
199 auto item_size = is_horizontal ? item->widget->width() : item->widget->height();
200 if (position + item_size + margin > toolbar_size) {
201 marginal_index = i;
202 break;
203 }
204 item->widget->set_visible(true);
205 position += item_size + spacing;
206 }
207
208 if (!marginal_index.has_value()) {
209 if (m_overflow_action) {
210 m_overflow_action->set_enabled(false);
211 m_overflow_button->set_visible(false);
212 }
213 return {};
214 }
215
216 if (marginal_index.value() > 0) {
217 for (size_t i = marginal_index.value() - 1; i > 0; --i) {
218 auto& item = m_items.at(i);
219 auto item_size = is_horizontal ? item->widget->width() : item->widget->height();
220 if (position + m_button_size + spacing + margin <= toolbar_size)
221 break;
222 item->widget->set_visible(false);
223 position -= item_size + spacing;
224 marginal_index = i;
225 }
226 }
227
228 if (m_grouped) {
229 for (size_t i = marginal_index.value(); i > 0; --i) {
230 auto& item = m_items.at(i);
231 if (item->type == Item::Type::Separator)
232 break;
233 item->widget->set_visible(false);
234 marginal_index = i;
235 }
236 }
237
238 if (!m_overflow_action)
239 TRY(create_overflow_objects());
240 m_overflow_action->set_enabled(true);
241 m_overflow_button->set_visible(true);
242
243 m_overflow_menu = TRY(Menu::try_create());
244 m_overflow_button->set_menu(m_overflow_menu);
245
246 for (size_t i = marginal_index.value(); i < m_items.size(); ++i) {
247 auto& item = m_items.at(i);
248 Item* peek_item;
249 if (i > 0) {
250 peek_item = m_items[i - 1];
251 if (peek_item->type == Item::Type::Separator)
252 peek_item->widget->set_visible(false);
253 }
254 if (i < m_items.size() - 1) {
255 item->widget->set_visible(false);
256 peek_item = m_items[i + 1];
257 if (item->action)
258 TRY(m_overflow_menu->try_add_action(*item->action));
259 }
260 if (item->action && peek_item->type == Item::Type::Separator)
261 TRY(m_overflow_menu->try_add_separator());
262 }
263
264 return {};
265}
266
267void Toolbar::resize_event(GUI::ResizeEvent& event)
268{
269 Widget::resize_event(event);
270 if (auto result = update_overflow_menu(); result.is_error())
271 warnln("Failed to update overflow menu");
272}
273
274}