ladybird/UI/Qt/BookmarksBar.cpp
Timothy Flynn fdacf590ef UI/Qt: Retain hovered appearance over bookmark items when right-clicked
It feels a bit off to right-click a bookmark item and have it lose its
hovered appearance. Other browsers also retain this appearance.
2026-05-28 22:04:24 +02:00

426 lines
15 KiB
C++

/*
* Copyright (c) 2026, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ScopeGuard.h>
#include <LibWebView/Application.h>
#include <LibWebView/BookmarkStore.h>
#include <UI/Qt/BookmarksBar.h>
#include <UI/Qt/ChromeStyle.h>
#include <UI/Qt/Icon.h>
#include <UI/Qt/Menu.h>
#include <UI/Qt/StringUtils.h>
#include <QAction>
#include <QEvent>
#include <QIcon>
#include <QMenu>
#include <QMouseEvent>
#include <QPointer>
#include <QStyle>
#include <QStyleOptionToolButton>
#include <QStylePainter>
#include <QToolButton>
namespace Ladybird {
static constexpr int BOOKMARK_BUTTON_MAX_WIDTH = 150;
static constexpr int BOOKMARK_BUTTON_ICON_SIZE = 16;
static constexpr int BOOKMARK_BUTTON_MIN_HEIGHT = 24;
static constexpr int BOOKMARK_BUTTON_VERTICAL_PADDING = 8;
static constexpr int BOOKMARK_BUTTON_HORIZONTAL_PADDING = 7;
static constexpr int BOOKMARK_BUTTON_ICON_TEXT_SPACING = 6;
static constexpr int BOOKMARK_BUTTON_TEXT_ELISION_PADDING = 2;
static constexpr char const* BOOKMARK_ITEM_PROPERTY = "bookmark_item";
static constexpr char const* BOOKMARK_CONTEXT_MENU_OPEN_PROPERTY = "bookmark_context_menu_open";
static QStyleOptionToolButton bookmark_button_style_option(QToolButton const& button)
{
QStyleOptionToolButton option;
option.initFrom(&button);
option.rect = button.rect();
option.icon = button.icon();
option.iconSize = button.iconSize();
option.text = button.text();
option.toolButtonStyle = button.toolButtonStyle();
option.arrowType = button.arrowType();
option.subControls = QStyle::SC_ToolButton;
if (button.arrowType() != Qt::NoArrow)
option.features |= QStyleOptionToolButton::Arrow;
switch (button.popupMode()) {
case QToolButton::DelayedPopup:
option.features |= QStyleOptionToolButton::PopupDelay;
break;
case QToolButton::MenuButtonPopup:
option.features |= QStyleOptionToolButton::MenuButtonPopup;
option.subControls |= QStyle::SC_ToolButtonMenu;
break;
case QToolButton::InstantPopup:
option.features |= QStyleOptionToolButton::Menu;
break;
}
if (button.autoRaise())
option.state |= QStyle::State_AutoRaise;
if (button.menu())
option.features |= QStyleOptionToolButton::HasMenu;
if (button.isDown())
option.state |= QStyle::State_Sunken;
if (button.property(BOOKMARK_CONTEXT_MENU_OPEN_PROPERTY).toBool())
option.state |= QStyle::State_MouseOver;
return option;
}
struct BookmarkButtonLayout {
QRect content_rect;
QRect icon_rect;
QRect text_rect;
int menu_indicator_width { 0 };
int icon_width { 0 };
int icon_text_spacing { 0 };
int available_text_width { 0 };
int preferred_width { 0 };
};
static BookmarkButtonLayout bookmark_button_layout(QToolButton const& button, QStyleOptionToolButton const& option, QString const& text, QRect const& button_rect)
{
BookmarkButtonLayout layout;
if (button.menu())
layout.menu_indicator_width = button.style()->pixelMetric(QStyle::PM_MenuButtonIndicator, &option, &button);
layout.icon_width = button.icon().isNull() ? 0 : button.iconSize().width();
layout.icon_text_spacing = layout.icon_width > 0 && !text.isEmpty() ? BOOKMARK_BUTTON_ICON_TEXT_SPACING : 0;
layout.content_rect = button_rect;
layout.content_rect.adjust(BOOKMARK_BUTTON_HORIZONTAL_PADDING, 0, -BOOKMARK_BUTTON_HORIZONTAL_PADDING, 0);
layout.content_rect.adjust(0, 0, -layout.menu_indicator_width, 0);
auto text_left = layout.content_rect.left() + layout.icon_width + layout.icon_text_spacing;
layout.available_text_width = max(layout.content_rect.right() - text_left + 1, 0);
layout.text_rect = QRect { text_left, layout.content_rect.top(), layout.available_text_width, layout.content_rect.height() };
if (layout.icon_width > 0) {
auto icon_size = option.iconSize;
layout.icon_rect = QRect {
layout.content_rect.left(),
layout.content_rect.top() + ((layout.content_rect.height() - icon_size.height()) / 2),
icon_size.width(),
icon_size.height(),
};
}
auto preferred_width = (BOOKMARK_BUTTON_HORIZONTAL_PADDING * 2)
+ layout.icon_width
+ layout.icon_text_spacing
+ button.fontMetrics().horizontalAdvance(text)
+ BOOKMARK_BUTTON_TEXT_ELISION_PADDING
+ layout.menu_indicator_width;
layout.preferred_width = min(preferred_width, button_rect.width());
return layout;
}
static void paint_bookmark_button(QToolButton& button)
{
QStylePainter painter(&button);
auto option = bookmark_button_style_option(button);
auto layout = bookmark_button_layout(button, option, option.text, button.rect());
auto frame_option = option;
frame_option.icon = {};
frame_option.text.clear();
painter.drawComplexControl(QStyle::CC_ToolButton, frame_option);
if (layout.icon_width > 0) {
auto mode = button.isEnabled() ? QIcon::Normal : QIcon::Disabled;
if (button.isEnabled() && (option.state & QStyle::State_MouseOver))
mode = QIcon::Active;
option.icon.paint(&painter, layout.icon_rect, Qt::AlignCenter, mode, button.isChecked() ? QIcon::On : QIcon::Off);
}
auto elided_text = button.fontMetrics().elidedText(option.text, Qt::ElideRight, layout.available_text_width);
button.style()->drawItemText(
&painter,
layout.text_rect,
Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine,
option.palette,
button.isEnabled(),
elided_text,
QPalette::ButtonText);
}
static void install_menu_event_filter(QObject* filter, QMenu* menu)
{
menu->installEventFilter(filter);
for (auto* action : menu->actions()) {
if (auto* submenu = action->menu())
install_menu_event_filter(filter, submenu);
}
}
BookmarksBar::BookmarksBar(QWidget* parent)
: QToolBar(parent)
{
setObjectName("LadybirdBookmarksBar");
setIconSize({ BOOKMARK_BUTTON_ICON_SIZE, BOOKMARK_BUTTON_ICON_SIZE });
setVisible(WebView::Application::settings().show_bookmarks_bar());
setMovable(false);
setFloatable(false);
update_chrome_style();
installEventFilter(this);
rebuild();
}
bool BookmarksBar::event(QEvent* event)
{
if (event->type() == QEvent::PaletteChange)
update_chrome_style();
return QToolBar::event(event);
}
void BookmarksBar::update_chrome_style()
{
if (m_is_updating_chrome_style)
return;
m_is_updating_chrome_style = true;
setStyleSheet(ChromeStyle::bookmarks_bar_style_sheet(palette()));
m_is_updating_chrome_style = false;
}
void BookmarksBar::rebuild()
{
for (auto* action : actions()) {
if (auto* menu = action->menu())
menu->close();
}
clear();
auto set_button_properties = [&](QToolButton* button, QString const& title) {
button->setProperty(BOOKMARK_ITEM_PROPERTY, true);
button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
button->ensurePolished();
auto option = bookmark_button_style_option(*button);
auto bookmark_button_height = max(BOOKMARK_BUTTON_MIN_HEIGHT, max(button->fontMetrics().height(), button->iconSize().height()) + BOOKMARK_BUTTON_VERTICAL_PADDING);
auto max_size_rect = QRect { 0, 0, BOOKMARK_BUTTON_MAX_WIDTH, bookmark_button_height };
auto layout = bookmark_button_layout(*button, option, title, max_size_rect);
auto available_title_width = max(layout.available_text_width - BOOKMARK_BUTTON_TEXT_ELISION_PADDING, 0);
auto text = button->fontMetrics().elidedText(title, Qt::ElideRight, available_title_width);
button->setText(text);
layout = bookmark_button_layout(*button, option, text, max_size_rect);
button->setFixedWidth(layout.preferred_width);
button->setMaximumWidth(BOOKMARK_BUTTON_MAX_WIDTH);
button->setFixedHeight(bookmark_button_height);
button->installEventFilter(this);
};
for (auto const& item : WebView::Application::the().bookmarks_menu().items()) {
item.visit(
[&](NonnullRefPtr<WebView::Action> const& bookmark) {
if (bookmark->id() != WebView::ActionID::BookmarkItem)
return;
auto* action = create_application_action(*this, *bookmark);
addAction(action);
if (auto* button = as_if<QToolButton>(widgetForAction(action)))
set_button_properties(button, qstring_from_ak_string(bookmark->text()));
},
[&](NonnullRefPtr<WebView::Menu> const& folder) {
auto title = qstring_from_ak_string(folder->title());
auto* submenu = create_application_menu(*this, *folder);
install_menu_event_filter(this, submenu);
auto* action = new QAction(title, this);
action->setIcon(create_tvg_icon_with_theme_colors("folder", palette()));
action->setProperty("id", submenu->property("id"));
action->setProperty("type", submenu->property("type"));
action->setProperty("target_folder_id", submenu->property("target_folder_id"));
action->setMenu(submenu);
addAction(action);
if (auto* button = as_if<QToolButton>(widgetForAction(action))) {
button->setPopupMode(QToolButton::InstantPopup);
set_button_properties(button, title);
}
},
[](WebView::Separator) {
});
}
}
void BookmarksBar::show_context_menu(QPoint position, Optional<WebView::BookmarkItem const&> item, Optional<String const&> target_folder_id)
{
if (item.has_value()) {
m_selected_bookmark_menu_item_id = item->id;
m_selected_bookmark_menu_target_folder_id = target_folder_id.copy();
if (item->is_bookmark())
bookmark_context_menu().exec(position);
else if (item->is_folder())
bookmark_folder_context_menu().exec(position);
} else {
m_selected_bookmark_menu_item_id = {};
m_selected_bookmark_menu_target_folder_id = {};
bookmarks_bar_context_menu().exec(position);
}
}
bool BookmarksBar::eventFilter(QObject* object, QEvent* event)
{
if (event->type() == QEvent::Paint) {
if (auto* button = as_if<QToolButton>(object); button && button->property(BOOKMARK_ITEM_PROPERTY).toBool()) {
paint_bookmark_button(*button);
return true;
}
}
if (event->type() == QEvent::MouseButtonPress) {
auto& mouse_event = as<QMouseEvent>(*event);
if (mouse_event.button() == Qt::LeftButton)
return handle_left_mouse_click(&mouse_event, object);
if (mouse_event.button() == Qt::MiddleButton)
return handle_middle_mouse_click(&mouse_event, object);
if (mouse_event.button() == Qt::RightButton)
return handle_right_mouse_click(&mouse_event, object);
}
return QToolBar::eventFilter(object, event);
}
bool BookmarksBar::handle_left_mouse_click(QMouseEvent* event, QObject* item)
{
if (event->modifiers().testFlag(Qt::ControlModifier))
return handle_middle_mouse_click(event, item);
return false;
}
bool BookmarksBar::handle_middle_mouse_click(QMouseEvent* event, QObject* item)
{
auto activate_tab = event->modifiers().testFlag(Qt::ShiftModifier) ? Web::HTML::ActivateTab::No : Web::HTML::ActivateTab::Yes;
if (auto* button = as_if<QToolButton>(item)) {
auto* action = button->defaultAction();
extract_item_properties(action);
if (m_selected_bookmark_menu_item_type == "bookmark")
WebView::Application::the().open_bookmark_in_new_tab(m_selected_bookmark_menu_item_id, activate_tab);
} else if (auto* menu = as_if<QMenu>(item)) {
if (auto* action = menu->actionAt(event->pos())) {
extract_item_properties(action);
if (m_selected_bookmark_menu_item_type == "bookmark")
WebView::Application::the().open_bookmark_in_new_tab(m_selected_bookmark_menu_item_id, activate_tab);
}
}
return true;
}
bool BookmarksBar::handle_right_mouse_click(QMouseEvent* event, QObject* item)
{
if (is<BookmarksBar>(item)) {
m_selected_bookmark_menu_item_id = {};
m_selected_bookmark_menu_target_folder_id = {};
bookmarks_bar_context_menu().exec(event->globalPosition().toPoint());
} else if (auto* button = as_if<QToolButton>(item)) {
auto* action = button->defaultAction();
extract_item_properties(action);
auto set_button_context_menu_property = [button = QPointer { button }](bool open) {
if (button) {
button->setProperty(BOOKMARK_CONTEXT_MENU_OPEN_PROPERTY, open);
button->update();
}
};
ScopeGuard guard { [&]() { set_button_context_menu_property(false); } };
set_button_context_menu_property(true);
if (m_selected_bookmark_menu_item_type == "bookmark")
bookmark_context_menu().exec(event->globalPosition().toPoint());
else if (m_selected_bookmark_menu_item_type == "folder")
bookmark_folder_context_menu().exec(event->globalPosition().toPoint());
} else if (auto* menu = as_if<QMenu>(item)) {
if (auto* action = menu->actionAt(event->pos())) {
QObject* submenu = action->menu();
extract_item_properties(submenu ?: action);
}
if (m_selected_bookmark_menu_item_type.isEmpty())
extract_item_properties(menu);
// FIXME: We create a temporary context menu parented to the dropdown. Otherwise, Qt complains that the context
// menu's parent does not match the current topmost popup. It would be nice if we could figure out a way
// to avoid this duplicated menu.
QMenu context_menu(menu);
if (m_selected_bookmark_menu_item_type == "bookmark")
repopulate_application_menu(context_menu, context_menu, WebView::Application::the().bookmark_context_menu());
else if (m_selected_bookmark_menu_item_type == "folder")
repopulate_application_menu(context_menu, context_menu, WebView::Application::the().bookmark_folder_context_menu());
if (!context_menu.isEmpty() && context_menu.exec(event->globalPosition().toPoint()))
menu->close();
}
return true;
}
void BookmarksBar::extract_item_properties(QObject* item)
{
m_selected_bookmark_menu_item_id = ak_string_from_qstring(item->property("id").toString());
m_selected_bookmark_menu_item_type = item->property("type").toString();
if (auto value = ak_string_from_qstring(item->property("target_folder_id").toString()); !value.is_empty())
m_selected_bookmark_menu_target_folder_id = AK::move(value);
}
QMenu& BookmarksBar::bookmarks_bar_context_menu()
{
if (!m_bookmarks_bar_context_menu)
m_bookmarks_bar_context_menu = create_application_menu(*this, WebView::Application::the().bookmarks_bar_context_menu());
return *m_bookmarks_bar_context_menu;
}
QMenu& BookmarksBar::bookmark_context_menu()
{
if (!m_bookmark_context_menu)
m_bookmark_context_menu = create_application_menu(*this, WebView::Application::the().bookmark_context_menu());
return *m_bookmark_context_menu;
}
QMenu& BookmarksBar::bookmark_folder_context_menu()
{
if (!m_bookmark_folder_context_menu)
m_bookmark_folder_context_menu = create_application_menu(*this, WebView::Application::the().bookmark_folder_context_menu());
return *m_bookmark_folder_context_menu;
}
}