Serenity Operating System
at master 274 lines 8.7 kB view raw
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}