LibGUI: Let Toolbars collapse into an overflow menu

Previously Toolbars were governed by a strict minimum size which
guaranteed all actions remained visible. Now, if set collapsible,
extra actions will fold into an overflow menu on the Toolbar.
This commit is contained in:
thankyouverycool 2022-07-27 08:03:33 -04:00 committed by Andreas Kling
parent 6891bfa965
commit 97b381652a
2 changed files with 114 additions and 4 deletions

View file

@ -26,6 +26,8 @@ Toolbar::Toolbar(Orientation orientation, int button_size)
: m_orientation(orientation) : m_orientation(orientation)
, m_button_size(button_size) , m_button_size(button_size)
{ {
REGISTER_BOOL_PROPERTY("collapsible", is_collapsible, set_collapsible);
if (m_orientation == Orientation::Horizontal) if (m_orientation == Orientation::Horizontal)
set_fixed_height(button_size); set_fixed_height(button_size);
else else
@ -95,11 +97,11 @@ ErrorOr<NonnullRefPtr<GUI::Button>> Toolbar::try_add_action(Action& action)
// This avoids having to untangle the child widget in case of allocation failure. // This avoids having to untangle the child widget in case of allocation failure.
TRY(m_items.try_ensure_capacity(m_items.size() + 1)); TRY(m_items.try_ensure_capacity(m_items.size() + 1));
auto button = TRY(try_add<ToolbarButton>(action)); item->widget = TRY(try_add<ToolbarButton>(action));
button->set_fixed_size(m_button_size, m_button_size); item->widget->set_fixed_size(m_button_size, m_button_size);
m_items.unchecked_append(move(item)); m_items.unchecked_append(move(item));
return button; return *static_cast<Button*>(m_items.last().widget.ptr());
} }
GUI::Button& Toolbar::add_action(Action& action) GUI::Button& Toolbar::add_action(Action& action)
@ -115,7 +117,7 @@ ErrorOr<void> Toolbar::try_add_separator()
auto item = TRY(adopt_nonnull_own_or_enomem(new (nothrow) Item)); auto item = TRY(adopt_nonnull_own_or_enomem(new (nothrow) Item));
item->type = Item::Type::Separator; item->type = Item::Type::Separator;
(void)TRY(try_add<SeparatorWidget>(m_orientation == Gfx::Orientation::Horizontal ? Gfx::Orientation::Vertical : Gfx::Orientation::Horizontal)); item->widget = TRY(try_add<SeparatorWidget>(m_orientation == Gfx::Orientation::Horizontal ? Gfx::Orientation::Vertical : Gfx::Orientation::Horizontal));
m_items.unchecked_append(move(item)); m_items.unchecked_append(move(item));
return {}; return {};
} }
@ -142,4 +144,99 @@ Optional<UISize> Toolbar::calculated_preferred_size() const
VERIFY_NOT_REACHED(); VERIFY_NOT_REACHED();
} }
Optional<UISize> Toolbar::calculated_min_size() const
{
if (m_collapsible) {
if (m_orientation == Gfx::Orientation::Horizontal)
return UISize(m_button_size, SpecialDimension::Shrink);
else
return UISize(SpecialDimension::Shrink, m_button_size);
}
VERIFY(layout());
return { layout()->min_size() };
}
ErrorOr<void> Toolbar::create_overflow_objects()
{
m_overflow_action = Action::create("Overflow Menu", { Mod_Ctrl | Mod_Shift, Key_O }, TRY(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/overflow-menu.png"sv)), [&](auto&) {
m_overflow_menu->popup(m_overflow_button->screen_relative_rect().bottom_left());
});
m_overflow_action->set_status_tip("Show hidden toolbar actions");
m_overflow_action->set_enabled(false);
TRY(layout()->try_add_spacer());
m_overflow_button = TRY(try_add_action(*m_overflow_action));
m_overflow_button->set_visible(false);
m_overflow_button->set_menu_position(Button::MenuPosition::BottomLeft);
return {};
}
ErrorOr<void> Toolbar::update_overflow_menu()
{
if (!m_collapsible)
return {};
Optional<size_t> marginal_index {};
auto position { 0 };
auto is_horizontal { m_orientation == Gfx::Orientation::Horizontal };
auto margin { is_horizontal ? layout()->margins().right() : layout()->margins().bottom() };
auto toolbar_size { is_horizontal ? width() : height() };
for (size_t i = 0; i < m_items.size() - 1; ++i) {
auto& item = m_items.at(i);
auto item_size = is_horizontal ? item.widget->width() : item.widget->height();
if (position + item_size + m_button_size + margin > toolbar_size) {
marginal_index = i;
break;
}
item.widget->set_visible(true);
position += item_size;
}
if (!marginal_index.has_value()) {
if (m_overflow_action) {
m_overflow_action->set_enabled(false);
m_overflow_button->set_visible(false);
}
return {};
}
if (!m_overflow_action)
TRY(create_overflow_objects());
m_overflow_action->set_enabled(true);
m_overflow_button->set_visible(true);
m_overflow_menu = TRY(Menu::try_create());
m_overflow_button->set_menu(m_overflow_menu);
for (size_t i = marginal_index.value(); i < m_items.size(); ++i) {
auto& item = m_items.at(i);
Item* peek_item;
if (i > 0) {
peek_item = &m_items.at(i - 1);
if (peek_item->type == Item::Type::Separator)
peek_item->widget->set_visible(false);
}
if (i < m_items.size() - 1) {
item.widget->set_visible(false);
peek_item = &m_items.at(i + 1);
if (item.action)
TRY(m_overflow_menu->try_add_action(*item.action));
}
if (item.action && peek_item->type == Item::Type::Separator)
TRY(m_overflow_menu->try_add_separator());
}
return {};
}
void Toolbar::resize_event(GUI::ResizeEvent& event)
{
Widget::resize_event(event);
if (auto result = update_overflow_menu(); result.is_error())
warnln("Failed to update overflow menu");
}
} }

View file

@ -9,6 +9,7 @@
#include <AK/NonnullOwnPtrVector.h> #include <AK/NonnullOwnPtrVector.h>
#include <LibGUI/Button.h> #include <LibGUI/Button.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Widget.h> #include <LibGUI/Widget.h>
namespace GUI { namespace GUI {
@ -24,13 +25,20 @@ public:
GUI::Button& add_action(GUI::Action&); GUI::Button& add_action(GUI::Action&);
void add_separator(); void add_separator();
bool is_collapsible() const { return m_collapsible; }
void set_collapsible(bool b) { m_collapsible = b; }
virtual Optional<UISize> calculated_preferred_size() const override; virtual Optional<UISize> calculated_preferred_size() const override;
virtual Optional<UISize> calculated_min_size() const override;
protected: protected:
explicit Toolbar(Gfx::Orientation = Gfx::Orientation::Horizontal, int button_size = 24); explicit Toolbar(Gfx::Orientation = Gfx::Orientation::Horizontal, int button_size = 24);
virtual void paint_event(PaintEvent&) override; virtual void paint_event(PaintEvent&) override;
virtual void resize_event(GUI::ResizeEvent&) override;
ErrorOr<void> update_overflow_menu();
ErrorOr<void> create_overflow_objects();
private: private:
struct Item { struct Item {
@ -41,10 +49,15 @@ private:
}; };
Type type { Type::Invalid }; Type type { Type::Invalid };
RefPtr<Action> action; RefPtr<Action> action;
RefPtr<Widget> widget;
}; };
NonnullOwnPtrVector<Item> m_items; NonnullOwnPtrVector<Item> m_items;
RefPtr<Menu> m_overflow_menu;
RefPtr<Action> m_overflow_action;
RefPtr<Button> m_overflow_button;
const Gfx::Orientation m_orientation; const Gfx::Orientation m_orientation;
int m_button_size { 24 }; int m_button_size { 24 };
bool m_collapsible { false };
}; };
} }