
Instead of letting buttons determine the relative position of their menus, a workaround only used by Statusbar segments, open them all uniformly for a nice, consistent UI. Passing a rect to popup() now routes to open_button_menu(), an analog to open_menubar_menu(), which adjusts the menu's popup position in the same way. Fixes button menus obscuring the buttons which spawn them and jutting out at odd corners depending on screen position.
291 lines
8 KiB
C++
291 lines
8 KiB
C++
/*
|
|
* Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/StringBuilder.h>
|
|
#include <LibGUI/Action.h>
|
|
#include <LibGUI/ActionGroup.h>
|
|
#include <LibGUI/Button.h>
|
|
#include <LibGUI/Menu.h>
|
|
#include <LibGUI/Painter.h>
|
|
#include <LibGUI/Window.h>
|
|
#include <LibGfx/Font/Font.h>
|
|
#include <LibGfx/Palette.h>
|
|
#include <LibGfx/StylePainter.h>
|
|
|
|
REGISTER_WIDGET(GUI, Button)
|
|
REGISTER_WIDGET(GUI, DialogButton)
|
|
|
|
namespace GUI {
|
|
|
|
Button::Button(String text)
|
|
: AbstractButton(move(text))
|
|
{
|
|
set_min_size({ 40, 22 });
|
|
set_preferred_size({ SpecialDimension::OpportunisticGrow, 22 });
|
|
set_focus_policy(GUI::FocusPolicy::StrongFocus);
|
|
|
|
on_focus_change = [this](bool has_focus, auto) {
|
|
if (!is_default())
|
|
return;
|
|
if (!has_focus && is<Button>(window()->focused_widget()))
|
|
m_another_button_has_focus = true;
|
|
else
|
|
m_another_button_has_focus = false;
|
|
update();
|
|
};
|
|
|
|
REGISTER_ENUM_PROPERTY(
|
|
"button_style", button_style, set_button_style, Gfx::ButtonStyle,
|
|
{ Gfx::ButtonStyle::Normal, "Normal" },
|
|
{ Gfx::ButtonStyle::Coolbar, "Coolbar" });
|
|
|
|
REGISTER_STRING_PROPERTY("icon", icon, set_icon_from_path);
|
|
REGISTER_BOOL_PROPERTY("default", is_default, set_default);
|
|
}
|
|
|
|
Button::~Button()
|
|
{
|
|
if (m_action)
|
|
m_action->unregister_button({}, *this);
|
|
}
|
|
|
|
void Button::paint_event(PaintEvent& event)
|
|
{
|
|
Painter painter(*this);
|
|
painter.add_clip_rect(event.rect());
|
|
|
|
bool paint_pressed = is_being_pressed() || is_mimic_pressed() || (m_menu && m_menu->is_visible());
|
|
|
|
Gfx::StylePainter::paint_button(painter, rect(), palette(), m_button_style, paint_pressed, is_hovered(), is_checked(), is_enabled(), is_focused(), is_default() && !another_button_has_focus());
|
|
|
|
if (text().is_empty() && !m_icon)
|
|
return;
|
|
|
|
auto content_rect = rect().shrunken(8, 2);
|
|
auto icon_location = m_icon ? content_rect.center().translated(-(m_icon->width() / 2), -(m_icon->height() / 2)) : Gfx::IntPoint();
|
|
if (m_icon && !text().is_empty())
|
|
icon_location.set_x(content_rect.x());
|
|
|
|
if (paint_pressed || is_checked()) {
|
|
painter.translate(1, 1);
|
|
} else if (m_icon && is_enabled() && is_hovered() && button_style() == Gfx::ButtonStyle::Coolbar) {
|
|
auto shadow_color = palette().button().darkened(0.7f);
|
|
painter.blit_filtered(icon_location.translated(1, 1), *m_icon, m_icon->rect(), [&shadow_color](auto) {
|
|
return shadow_color;
|
|
});
|
|
icon_location.translate_by(-1, -1);
|
|
}
|
|
|
|
if (m_icon) {
|
|
auto solid_color = m_icon->solid_color(60);
|
|
bool should_invert_icon = false;
|
|
if (solid_color.has_value()) {
|
|
auto contrast_ratio = palette().button().contrast_ratio(*solid_color);
|
|
// Note: 4.5 is the minimum recommended contrast ratio for text on the web:
|
|
// (https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Perceivable/Color_contrast)
|
|
// Reusing that threshold here as it seems to work reasonably well.
|
|
should_invert_icon = contrast_ratio < 4.5f && contrast_ratio < palette().button().contrast_ratio(solid_color->inverted());
|
|
}
|
|
if (should_invert_icon)
|
|
m_icon->invert();
|
|
if (is_enabled()) {
|
|
if (is_hovered())
|
|
painter.blit_brightened(icon_location, *m_icon, m_icon->rect());
|
|
else
|
|
painter.blit(icon_location, *m_icon, m_icon->rect());
|
|
} else {
|
|
painter.blit_disabled(icon_location, *m_icon, m_icon->rect(), palette());
|
|
}
|
|
if (should_invert_icon)
|
|
m_icon->invert();
|
|
}
|
|
auto& font = is_checked() ? this->font().bold_variant() : this->font();
|
|
if (m_icon && !text().is_empty()) {
|
|
content_rect.translate_by(m_icon->width() + icon_spacing(), 0);
|
|
content_rect.set_width(content_rect.width() - m_icon->width() - icon_spacing());
|
|
}
|
|
|
|
Gfx::IntRect text_rect { 0, 0, font.width(text()), font.glyph_height() };
|
|
if (text_rect.width() > content_rect.width())
|
|
text_rect.set_width(content_rect.width());
|
|
text_rect.align_within(content_rect, text_alignment());
|
|
paint_text(painter, text_rect, font, text_alignment());
|
|
|
|
if (is_focused()) {
|
|
Gfx::IntRect focus_rect;
|
|
if (m_icon && !text().is_empty())
|
|
focus_rect = text_rect.inflated(4, 4);
|
|
else
|
|
focus_rect = rect().shrunken(8, 8);
|
|
painter.draw_focus_rect(focus_rect, palette().focus_outline());
|
|
}
|
|
}
|
|
|
|
void Button::click(unsigned modifiers)
|
|
{
|
|
if (!is_enabled())
|
|
return;
|
|
|
|
NonnullRefPtr protector = *this;
|
|
|
|
if (is_checkable()) {
|
|
if (is_checked() && !is_uncheckable())
|
|
return;
|
|
set_checked(!is_checked());
|
|
}
|
|
if (on_click)
|
|
on_click(modifiers);
|
|
if (m_action)
|
|
m_action->activate(this);
|
|
}
|
|
|
|
void Button::middle_mouse_click(unsigned int modifiers)
|
|
{
|
|
if (!is_enabled())
|
|
return;
|
|
|
|
NonnullRefPtr protector = *this;
|
|
|
|
if (on_middle_mouse_click)
|
|
on_middle_mouse_click(modifiers);
|
|
}
|
|
|
|
void Button::context_menu_event(ContextMenuEvent& context_menu_event)
|
|
{
|
|
if (!is_enabled())
|
|
return;
|
|
if (on_context_menu_request)
|
|
on_context_menu_request(context_menu_event);
|
|
}
|
|
|
|
void Button::set_action(Action& action)
|
|
{
|
|
m_action = action;
|
|
action.register_button({}, *this);
|
|
set_enabled(action.is_enabled());
|
|
set_checkable(action.is_checkable());
|
|
if (action.is_checkable())
|
|
set_checked(action.is_checked());
|
|
}
|
|
|
|
void Button::set_icon(RefPtr<Gfx::Bitmap> icon)
|
|
{
|
|
if (m_icon == icon)
|
|
return;
|
|
m_icon = move(icon);
|
|
update();
|
|
}
|
|
|
|
void Button::set_icon_from_path(String const& path)
|
|
{
|
|
auto maybe_bitmap = Gfx::Bitmap::try_load_from_file(path);
|
|
if (maybe_bitmap.is_error()) {
|
|
dbgln("Unable to load bitmap `{}` for button icon", path);
|
|
return;
|
|
}
|
|
set_icon(maybe_bitmap.release_value());
|
|
}
|
|
|
|
bool Button::is_uncheckable() const
|
|
{
|
|
if (!m_action)
|
|
return true;
|
|
if (!m_action->group())
|
|
return true;
|
|
return m_action->group()->is_unchecking_allowed();
|
|
}
|
|
|
|
void Button::set_menu(RefPtr<GUI::Menu> menu)
|
|
{
|
|
if (m_menu == menu)
|
|
return;
|
|
if (m_menu)
|
|
m_menu->on_visibility_change = nullptr;
|
|
m_menu = menu;
|
|
if (m_menu) {
|
|
m_menu->on_visibility_change = [&](bool) {
|
|
update();
|
|
};
|
|
}
|
|
}
|
|
|
|
void Button::mousedown_event(MouseEvent& event)
|
|
{
|
|
if (m_menu) {
|
|
m_menu->popup(screen_relative_rect().bottom_left(), {}, rect());
|
|
update();
|
|
return;
|
|
}
|
|
AbstractButton::mousedown_event(event);
|
|
}
|
|
|
|
void Button::mousemove_event(MouseEvent& event)
|
|
{
|
|
if (m_menu) {
|
|
return;
|
|
}
|
|
AbstractButton::mousemove_event(event);
|
|
}
|
|
|
|
bool Button::is_default() const
|
|
{
|
|
if (!window())
|
|
return false;
|
|
return this == window()->default_return_key_widget();
|
|
}
|
|
|
|
void Button::set_default(bool default_button)
|
|
{
|
|
deferred_invoke([this, default_button] {
|
|
VERIFY(window());
|
|
window()->set_default_return_key_widget(default_button ? this : nullptr);
|
|
});
|
|
}
|
|
|
|
void Button::set_mimic_pressed(bool mimic_pressed)
|
|
{
|
|
if (!is_being_pressed()) {
|
|
m_mimic_pressed = mimic_pressed;
|
|
|
|
stop_timer();
|
|
start_timer(80, Core::TimerShouldFireWhenNotVisible::Yes);
|
|
|
|
update();
|
|
}
|
|
}
|
|
|
|
void Button::timer_event(Core::TimerEvent&)
|
|
{
|
|
if (is_mimic_pressed()) {
|
|
m_mimic_pressed = false;
|
|
|
|
update();
|
|
}
|
|
}
|
|
|
|
Optional<UISize> Button::calculated_min_size() const
|
|
{
|
|
int horizontal = 0, vertical = 0;
|
|
|
|
if (!text().is_empty()) {
|
|
auto& font = this->font();
|
|
horizontal = font.width(text()) + 2;
|
|
vertical = font.glyph_height() + 4; // FIXME: Use actual maximum total height
|
|
}
|
|
|
|
if (m_icon) {
|
|
vertical = max(vertical, m_icon->height());
|
|
|
|
horizontal += m_icon->width() + icon_spacing();
|
|
}
|
|
|
|
horizontal += 8;
|
|
vertical += 4;
|
|
|
|
return UISize(horizontal, vertical);
|
|
}
|
|
|
|
}
|