Files
ladybird/UI/Qt/BookmarksBar.cpp
Timothy Flynn b544e42809 LibWebView+UI: Add an about:bookmarks page to manage bookmarks
This page renders the bookmarks as a tree and hook context menu events
up to the UI's bookmarks bar context menus to allow editing bookmarks.
Users can also drag-and-drop bookmark items around.
2026-04-09 10:08:06 -04:00

234 lines
8.5 KiB
C++

/*
* Copyright (c) 2026, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWebView/Application.h>
#include <LibWebView/BookmarkStore.h>
#include <UI/Qt/BookmarksBar.h>
#include <UI/Qt/Icon.h>
#include <UI/Qt/Menu.h>
#include <UI/Qt/StringUtils.h>
#include <QAction>
#include <QMenu>
#include <QMouseEvent>
#include <QToolButton>
#include <qapplication.h>
namespace Ladybird {
static constexpr int BOOKMARK_BUTTON_MAX_WIDTH = 150;
static constexpr int BOOKMARK_BUTTON_ICON_SIZE = 16;
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)
{
setIconSize({ BOOKMARK_BUTTON_ICON_SIZE, BOOKMARK_BUTTON_ICON_SIZE });
setVisible(WebView::Application::settings().show_bookmarks_bar());
setMovable(false);
installEventFilter(this);
rebuild();
}
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->setText(button->fontMetrics().elidedText(title, Qt::ElideRight, BOOKMARK_BUTTON_MAX_WIDTH - 28));
button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
button->setMaximumWidth(BOOKMARK_BUTTON_MAX_WIDTH);
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::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);
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;
}
}