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.
This commit is contained in:
Timothy Flynn
2026-04-06 19:08:00 -04:00
committed by Tim Flynn
parent cd76622452
commit b544e42809
Notes: github-actions[bot] 2026-04-09 14:09:13 +00:00
20 changed files with 667 additions and 2 deletions

View File

@@ -0,0 +1,468 @@
<!doctype html>
<html>
<head>
<title>Bookmark Manager</title>
<link rel="stylesheet" type="text/css" href="resource://ladybird/ladybird.css" />
<link rel="stylesheet" type="text/css" href="resource://ladybird/about-pages/webui.css" />
<style>
@media (prefers-color-scheme: light) {
:root {
--tree-hover-color: #e8e8e8;
--tree-selected-color: #d8d8f8;
--empty-text-color: #888;
--url-text-color: #666;
}
}
@media (prefers-color-scheme: dark) {
:root {
--tree-hover-color: #2e2e2e;
--tree-selected-color: #35355a;
--empty-text-color: #777;
--url-text-color: #999;
}
}
.tree-empty {
color: var(--empty-text-color);
padding: 40px 20px;
text-align: center;
font-size: 14px;
}
.tree-item-row {
display: flex;
align-items: center;
gap: 8px;
padding: 14px 12px;
border-radius: 6px;
font-size: 14px;
cursor: grab;
}
.tree-item-row.dragging {
cursor: grabbing;
opacity: 0.4;
}
.tree-item-row.drag-over-inside {
background-color: var(--tree-selected-color);
}
.tree-item-row:hover {
background-color: var(--tree-hover-color);
}
.tree-item-row.expandable {
cursor: pointer;
}
.tree-children {
padding-left: 24px;
}
.drop-indicator {
background-color: var(--violet-100) !important;
height: 2px;
margin: -1px 12px;
pointer-events: none;
}
.expand-toggle {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 20px;
height: 20px;
cursor: pointer;
}
.expand-toggle svg {
width: 14px;
height: 14px;
}
.expand-toggle.collapsed svg {
transform: rotate(-90deg);
}
.tree-spacer {
width: 20px;
flex-shrink: 0;
}
.item-icon {
display: flex;
flex-shrink: 0;
align-items: center;
}
.item-icon img,
.item-icon svg {
width: 16px;
height: 16px;
}
.item-info {
display: flex;
align-items: center;
gap: 8px;
flex-grow: 1;
min-width: 0;
}
.item-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-shrink: 1;
min-width: 0;
}
.item-title a {
color: inherit;
text-decoration: none;
}
.item-title a:hover {
text-decoration: underline;
}
.item-url {
color: var(--url-text-color);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
min-width: 0;
}
</style>
</head>
<body>
<header>
<picture>
<source srcset="resource://icons/128x128/app-browser.png" media="(prefers-color-scheme: dark)" />
<img src="resource://icons/128x128/app-browser-dark.png" />
</picture>
<h1>Bookmarks</h1>
</header>
<div class="card">
<div class="card-body" id="bookmark-tree"></div>
</div>
<script type="module">
// SVG export of UI/Icons/folder.tvg
const FOLDER_SVG = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M 1.96875 5.273438 C 1.523438 5.367188 1.101562 5.695312 0.890625 6.117188 L 0.773438 6.351562 L 0.773438 19.148438 L 0.914062 19.40625 C 1.054688 19.710938 1.3125 19.96875 1.640625 20.109375 L 1.851562 20.226562 L 11.179688 20.226562 C 20.390625 20.25 20.53125 20.25 20.742188 20.15625 C 21.140625 20.015625 21.46875 19.710938 21.632812 19.3125 L 21.75 19.054688 L 21.75 8.648438 L 21.609375 8.390625 C 21.46875 8.0625 21.210938 7.804688 20.90625 7.664062 L 20.648438 7.546875 L 15.960938 7.523438 L 11.25 7.5 L 10.101562 6.375 L 8.976562 5.25 L 5.554688 5.25 C 3.679688 5.25 2.0625 5.273438 1.96875 5.273438 "/>
</svg>`;
// SVG export of UI/Icons/down.tvg
const FOLDER_ARROW_DOWN_SVG = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M 4.5 6 L 12 13.5 L 19.5 6 L 21 9 L 12 18 L 3 9 Z M 4.5 6 "/>
</svg>`;
// SVG export of UI/Icons/globe.tvg
const BOOKMARK_SVG = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M 10.96875 1.40625 C 9.234375 1.570312 7.546875 2.179688 6.09375 3.140625 C 5.507812 3.539062 4.945312 3.984375 4.453125 4.5 C 2.835938 6.117188 1.804688 8.15625 1.453125 10.476562 C 1.359375 11.132812 1.359375 12.867188 1.453125 13.523438 C 1.898438 16.523438 3.585938 19.195312 6.117188 20.882812 C 7.335938 21.703125 8.90625 22.3125 10.453125 22.546875 C 11.15625 22.640625 12.84375 22.640625 13.546875 22.546875 C 15.421875 22.265625 17.203125 21.46875 18.679688 20.296875 C 19.265625 19.804688 19.804688 19.265625 20.296875 18.679688 C 21.492188 17.203125 22.265625 15.421875 22.546875 13.546875 C 22.640625 12.84375 22.640625 11.132812 22.546875 10.453125 C 22.21875 8.203125 21.164062 6.117188 19.546875 4.5 C 19.265625 4.21875 18.984375 3.960938 18.679688 3.703125 C 17.179688 2.53125 15.398438 1.757812 13.523438 1.453125 C 13.03125 1.382812 11.460938 1.335938 10.96875 1.40625 M 12.515625 3.304688 C 12.703125 3.398438 12.867188 3.539062 13.007812 3.679688 C 14.203125 4.875 15.023438 7.476562 15.257812 10.757812 L 15.28125 11.109375 L 8.71875 11.109375 L 8.742188 10.757812 C 8.90625 8.414062 9.328125 6.539062 10.03125 5.109375 C 10.546875 4.054688 11.085938 3.421875 11.671875 3.210938 C 11.90625 3.140625 12.257812 3.164062 12.515625 3.304688 M 8.484375 4.171875 C 7.640625 5.8125 7.078125 8.25 6.9375 10.640625 L 6.914062 11.109375 L 3.1875 11.109375 L 3.210938 10.898438 C 3.304688 10.195312 3.515625 9.421875 3.796875 8.71875 C 4.242188 7.570312 4.828125 6.679688 5.765625 5.765625 C 6.328125 5.179688 6.796875 4.804688 7.453125 4.40625 C 7.828125 4.171875 8.507812 3.84375 8.601562 3.84375 C 8.625 3.84375 8.578125 3.960938 8.484375 4.171875 M 15.796875 4.007812 C 16.335938 4.265625 16.851562 4.570312 17.320312 4.921875 C 17.8125 5.296875 18.703125 6.1875 19.078125 6.679688 C 19.992188 7.921875 20.578125 9.375 20.789062 10.898438 L 20.8125 11.109375 L 17.085938 11.109375 L 17.0625 10.640625 C 16.921875 8.25 16.335938 5.8125 15.515625 4.171875 C 15.421875 3.984375 15.375 3.84375 15.398438 3.84375 C 15.421875 3.84375 15.609375 3.914062 15.796875 4.007812 M 6.9375 13.382812 C 7.078125 15.773438 7.664062 18.1875 8.484375 19.828125 C 8.578125 20.015625 8.648438 20.15625 8.648438 20.179688 C 8.648438 20.226562 7.875 19.851562 7.5 19.617188 C 6.867188 19.242188 6.28125 18.773438 5.765625 18.234375 C 5.460938 17.953125 5.179688 17.648438 4.921875 17.320312 C 4.007812 16.078125 3.421875 14.648438 3.210938 13.101562 L 3.1875 12.914062 L 6.914062 12.914062 L 6.9375 13.382812 M 15.257812 13.265625 C 15.09375 15.679688 14.625 17.671875 13.875 19.078125 C 13.289062 20.203125 12.609375 20.835938 12 20.835938 C 11.039062 20.835938 9.9375 19.265625 9.304688 16.945312 C 9.023438 15.914062 8.835938 14.671875 8.742188 13.265625 L 8.71875 12.914062 L 15.28125 12.914062 L 15.257812 13.265625 M 20.789062 13.101562 C 20.578125 14.625 19.992188 16.078125 19.078125 17.320312 C 18.914062 17.53125 18.539062 17.953125 18.234375 18.234375 C 17.71875 18.773438 17.132812 19.242188 16.5 19.617188 C 16.125 19.851562 15.351562 20.226562 15.351562 20.179688 C 15.351562 20.15625 15.421875 20.015625 15.515625 19.828125 C 16.335938 18.210938 16.921875 15.773438 17.0625 13.382812 L 17.085938 12.914062 L 20.8125 12.914062 L 20.789062 13.101562 "/>
</svg>`;
let BOOKMARKS = [];
let EXPANDED_FOLDERS = new Set();
let DRAGGED_ITEM_ID = null;
const DROP_INDICATOR = document.createElement("div");
DROP_INDICATOR.className = "drop-indicator";
function createElement(tagName, properties = {}, children = []) {
const element = document.createElement(tagName);
Object.assign(element, properties);
element.append(...children);
return element;
}
function createSpacer() {
return createElement("span", { className: "tree-spacer" });
}
function createIcon(markup) {
return createElement("span", { className: "item-icon", innerHTML: markup });
}
function createFaviconIcon(favicon) {
if (!favicon) {
return createIcon(BOOKMARK_SVG);
}
return createElement("span", { className: "item-icon" }, [
createElement("img", { src: `data:image/png;base64,${favicon}` }),
]);
}
function createBookmarkInfo(item) {
const title = createElement("span", { className: "item-title" }, [
createElement("a", { href: item.url, textContent: item.title || item.url }),
]);
const url = createElement("span", { className: "item-url", textContent: item.url });
return createElement("span", { className: "item-info" }, [title, url]);
}
function toggleFolder(folderId) {
if (EXPANDED_FOLDERS.has(folderId)) {
EXPANDED_FOLDERS.delete(folderId);
} else {
EXPANDED_FOLDERS.add(folderId);
}
renderTree();
}
function addContextMenuHandler(element, item, targetFolderId) {
element.addEventListener("contextmenu", event => {
event.preventDefault();
event.stopPropagation();
ladybird.sendMessage("showContextMenu", {
clientX: event.clientX,
clientY: event.clientY,
id: item.id,
targetFolderId,
});
});
}
const DROP_ABOVE = 1;
const DROP_BELOW = 2;
const DROP_INSIDE = 3;
function getDropPosition(event, row, item) {
const rect = row.getBoundingClientRect();
const ratio = (event.clientY - rect.top) / rect.height;
if (item.type === "bookmark") {
return ratio < 0.5 ? DROP_ABOVE : DROP_BELOW;
}
if (ratio < 0.25) {
return DROP_ABOVE;
}
if (ratio > 0.75 && !EXPANDED_FOLDERS.has(item.id)) {
return DROP_BELOW;
}
return DROP_INSIDE;
}
function getDropTarget(item, parentId, index, position) {
if (position === DROP_INSIDE) {
return {
targetFolderId: item.id,
targetIndex: item.children ? item.children.length : 0,
};
}
return {
targetFolderId: parentId || null,
targetIndex: position === DROP_ABOVE ? index : index + 1,
};
}
function moveDraggedItem(targetFolderId, targetIndex) {
ladybird.sendMessage("moveItem", {
id: DRAGGED_ITEM_ID,
index: targetIndex,
targetFolderId,
});
DRAGGED_ITEM_ID = null;
}
function setupDragHandlers(row, item, parentId, index) {
row.draggable = true;
row.addEventListener("dragstart", event => {
DRAGGED_ITEM_ID = item.id;
row.classList.add("dragging");
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", item.id);
});
row.addEventListener("dragend", () => {
DRAGGED_ITEM_ID = null;
row.classList.remove("dragging");
DROP_INDICATOR.remove();
});
row.addEventListener("dragover", event => {
event.preventDefault();
event.stopPropagation();
event.dataTransfer.dropEffect = "move";
DROP_INDICATOR.remove();
const position = getDropPosition(event, row, item);
if (position === DROP_INSIDE) {
row.classList.add("drag-over-inside");
} else {
const wrapper = row.closest(".tree-item");
if (position === DROP_ABOVE) {
wrapper.before(DROP_INDICATOR);
} else {
wrapper.after(DROP_INDICATOR);
}
}
});
row.addEventListener("dragleave", event => {
if (!row.contains(event.relatedTarget)) {
row.classList.remove("drag-over-inside");
}
});
row.addEventListener("drop", event => {
event.preventDefault();
event.stopPropagation();
DROP_INDICATOR.remove();
const position = getDropPosition(event, row, item);
const { targetFolderId, targetIndex } = getDropTarget(item, parentId, index, position);
moveDraggedItem(targetFolderId, targetIndex);
});
}
function renderTree() {
bookmarkTreeContainer.innerHTML = "";
if (BOOKMARKS.length === 0) {
bookmarkTreeContainer.appendChild(
createElement("div", {
className: "tree-empty",
textContent: "No bookmarks yet.",
})
);
return;
}
renderItems(bookmarkTreeContainer, BOOKMARKS, null);
}
function renderItems(container, items, parentId) {
items.forEach((item, index) => {
const wrapper = createElement("div", { className: "tree-item" });
const row = createElement("div", { className: "tree-item-row" });
wrapper.appendChild(row);
if (item.type === "bookmark") {
renderBookmarkRow(row, item, index, parentId);
} else {
renderFolderRow(row, wrapper, item, index, parentId);
}
container.appendChild(wrapper);
});
}
function renderBookmarkRow(row, item, index, parentId) {
row.appendChild(createSpacer());
row.appendChild(createFaviconIcon(item.favicon));
row.appendChild(createBookmarkInfo(item));
setupDragHandlers(row, item, parentId, index);
addContextMenuHandler(row, item, parentId || null);
}
function renderFolderRow(row, wrapper, item, index, parentId) {
const hasChildren = item.children && item.children.length > 0;
const isExpanded = hasChildren && EXPANDED_FOLDERS.has(item.id);
if (hasChildren) {
row.classList.add("expandable");
row.addEventListener("click", () => toggleFolder(item.id));
row.appendChild(
createElement("span", {
className: `expand-toggle ${isExpanded ? "expanded" : "collapsed"}`,
innerHTML: FOLDER_ARROW_DOWN_SVG,
})
);
} else {
row.appendChild(createSpacer());
}
row.appendChild(createIcon(FOLDER_SVG));
row.appendChild(
createElement("span", {
className: "item-title",
textContent: item.title || "(no title)",
})
);
setupDragHandlers(row, item, parentId, index);
addContextMenuHandler(row, item, item.id);
if (isExpanded) {
const childrenContainer = createElement("div", { className: "tree-children" });
renderItems(childrenContainer, item.children, item.id);
wrapper.appendChild(childrenContainer);
}
}
const bookmarkTreeContainer = document.getElementById("bookmark-tree");
bookmarkTreeContainer.addEventListener("dragover", event => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
});
bookmarkTreeContainer.addEventListener("drop", event => {
event.preventDefault();
DROP_INDICATOR.remove();
if (DRAGGED_ITEM_ID) {
moveDraggedItem(null, BOOKMARKS.length);
}
});
bookmarkTreeContainer.addEventListener("contextmenu", event => {
event.preventDefault();
ladybird.sendMessage("showContextMenu", {
clientX: event.clientX,
clientY: event.clientY,
});
});
document.addEventListener("WebUILoaded", () => {
ladybird.sendMessage("loadBookmarks");
});
document.addEventListener("WebUIMessage", event => {
if (event.detail.name === "loadBookmarks") {
BOOKMARKS = event.detail.data;
renderTree();
}
});
</script>
</body>
</html>

View File

@@ -219,13 +219,14 @@ inline URL about_srcdoc() { return URL::about("srcdoc"_string); }
inline URL about_error() { return URL::about("error"_string); }
inline URL about_newtab() { return URL::about("newtab"_string); }
inline URL about_bookmarks() { return URL::about("bookmarks"_string); }
inline URL about_processes() { return URL::about("processes"_string); }
inline URL about_settings() { return URL::about("settings"_string); }
inline URL about_version() { return URL::about("version"_string); }
inline bool is_webui_url(URL const& url)
{
return first_is_one_of(url, about_processes(), about_settings());
return first_is_one_of(url, about_bookmarks(), about_processes(), about_settings());
}
}

View File

@@ -934,6 +934,10 @@ void Application::initialize_actions()
m_motion_menu->items().first().get<NonnullRefPtr<Action>>()->set_checked(true);
m_bookmarks_menu = Menu::create("Bookmarks"sv);
m_bookmarks_menu->add_action(Action::create("Manage Bookmarks"sv, ActionID::ManageBookmarks, [this]() {
open_url_in_new_tab(URL::about_bookmarks(), Web::HTML::ActivateTab::Yes);
}));
m_bookmarks_menu->add_separator();
m_toggle_bookmark_action = Action::create("Toggle Bookmark"sv, ActionID::ToggleBookmark, [this]() {
auto view = active_web_view();

View File

@@ -67,6 +67,8 @@ public:
void bookmarks_changed(Badge<ApplicationBookmarkStoreObserver>);
void show_bookmarks_bar_changed(Badge<ApplicationSettingsObserver>);
virtual void show_bookmark_context_menu(Gfx::IntPoint, Optional<BookmarkItem const&>, [[maybe_unused]] Optional<String const&> target_folder_id) { }
static CookieJar& cookie_jar() { return *the().m_cookie_jar; }
static StorageJar& storage_jar() { return *the().m_storage_jar; }

View File

@@ -29,6 +29,7 @@ set(SOURCES
ViewImplementation.cpp
WebContentClient.cpp
WebUI.cpp
WebUI/BookmarksUI.cpp
WebUI/ProcessesUI.cpp
WebUI/SettingsUI.cpp
)

View File

@@ -27,6 +27,7 @@ class WebUI;
struct Attribute;
struct AutocompleteEngine;
struct BookmarkItem;
struct BrowserOptions;
struct ConsoleOutput;
struct CookieStorageKey;

View File

@@ -37,6 +37,7 @@ enum class ActionID {
TakeVisibleScreenshot,
TakeFullScreenshot,
ManageBookmarks,
ToggleBookmark,
ToggleBookmarkViaToolbar,
ToggleBookmarksBar,

View File

@@ -8,6 +8,7 @@
#include <LibIPC/TransportHandle.h>
#include <LibWebView/WebContentClient.h>
#include <LibWebView/WebUI.h>
#include <LibWebView/WebUI/BookmarksUI.h>
#include <LibWebView/WebUI/ProcessesUI.h>
#include <LibWebView/WebUI/SettingsUI.h>
@@ -29,7 +30,9 @@ ErrorOr<RefPtr<WebUI>> WebUI::create(WebContentClient& client, String host)
{
RefPtr<WebUI> web_ui;
if (host == "processes"sv)
if (host == "bookmarks"sv)
web_ui = TRY(create_web_ui<BookmarksUI>(client, move(host)));
else if (host == "processes"sv)
web_ui = TRY(create_web_ui<ProcessesUI>(client, move(host)));
else if (host == "settings"sv)
web_ui = TRY(create_web_ui<SettingsUI>(client, move(host)));

View File

@@ -0,0 +1,72 @@
/*
* Copyright (c) 2026, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWebView/Application.h>
#include <LibWebView/BookmarkStore.h>
#include <LibWebView/WebUI/BookmarksUI.h>
namespace WebView {
void BookmarksUI::register_interfaces()
{
register_interface("loadBookmarks"sv, [this](auto const&) {
load_bookmarks();
});
register_interface("moveItem"sv, [this](auto const& data) {
move_item(data);
});
register_interface("showContextMenu"sv, [this](auto const& data) {
show_context_menu(data);
});
}
void BookmarksUI::bookmarks_changed()
{
load_bookmarks();
}
void BookmarksUI::load_bookmarks()
{
async_send_message("loadBookmarks"sv, Application::bookmark_store().serialize_items());
}
void BookmarksUI::move_item(JsonValue const& data)
{
if (!data.is_object())
return;
auto const& object = data.as_object();
auto id = object.get_string("id"sv);
auto index = object.get_integer<size_t>("index"sv);
if (!id.has_value() || !index.has_value())
return;
auto target_folder_id = object.get_string("targetFolderId"sv);
Application::bookmark_store().move_item(*id, target_folder_id, *index);
}
void BookmarksUI::show_context_menu(JsonValue const& data)
{
if (!data.is_object())
return;
auto const& object = data.as_object();
auto client_x = object.get_integer<i32>("clientX"sv);
auto client_y = object.get_integer<i32>("clientY"sv);
if (!client_x.has_value() || !client_y.has_value())
return;
if (auto id = object.get_string("id"sv); id.has_value()) {
auto item = Application::bookmark_store().find_item_by_id(*id);
auto target_folder_id = object.get_string("targetFolderId"sv);
Application::the().show_bookmark_context_menu({ *client_x, *client_y }, item, target_folder_id);
} else {
Application::the().show_bookmark_context_menu({ *client_x, *client_y }, {}, {});
}
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (c) 2026, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWebView/BookmarkStore.h>
#include <LibWebView/WebUI.h>
namespace WebView {
class BookmarksUI
: public WebUI
, public BookmarkStoreObserver {
WEB_UI(BookmarksUI);
private:
virtual void register_interfaces() override;
virtual void bookmarks_changed() override;
void load_bookmarks();
void move_item(JsonValue const&);
void show_context_menu(JsonValue const&);
};
}

View File

@@ -33,6 +33,7 @@ private:
virtual void rebuild_bookmarks_menu() const override;
virtual void update_bookmarks_bar_display(bool) const override;
virtual void show_bookmark_context_menu(Gfx::IntPoint, Optional<WebView::BookmarkItem const&>, Optional<String const&> target_folder_id) override;
virtual Optional<BookmarkID> bookmark_item_id_for_context_menu() const override;
virtual NonnullRefPtr<BookmarkPromise> display_add_bookmark_dialog() const override;
virtual NonnullRefPtr<BookmarkPromise> display_edit_bookmark_dialog(WebView::BookmarkItem::Bookmark const& current_bookmark) const override;

View File

@@ -166,6 +166,18 @@ void Application::update_bookmarks_bar_display(bool show_bookmarks_bar) const
[delegate updateBookmarksBarDisplay:show_bookmarks_bar];
}
void Application::show_bookmark_context_menu(Gfx::IntPoint content_position, Optional<WebView::BookmarkItem const&> item, Optional<String const&> target_folder_id)
{
ApplicationDelegate* delegate = [NSApp delegate];
if (auto* tab = [delegate activeTab]) {
[[tab bookmarksBar] showContextMenu:content_position
view:[tab web_view]
bookmarkItem:item
targetFolderID:target_folder_id];
}
}
Optional<Application::BookmarkID> Application::bookmark_item_id_for_context_menu() const
{
ApplicationDelegate* delegate = [NSApp delegate];

View File

@@ -6,6 +6,10 @@
#pragma once
#include <AK/Optional.h>
#include <LibGfx/Point.h>
#include <LibWebView/Forward.h>
#import <Cocoa/Cocoa.h>
@class BookmarkFolderPopover;
@@ -20,6 +24,10 @@
- (void)bookmarkFolderDidClose:(BookmarkFolderPopover*)folder;
- (void)showContextMenu:(id)control event:(NSEvent*)event;
- (void)showContextMenu:(Gfx::IntPoint)content_position
view:(NSView*)view
bookmarkItem:(Optional<WebView::BookmarkItem const&>)item
targetFolderID:(Optional<String const&>)target_folder_id;
@property (nonatomic, strong, readonly) NSString* selected_bookmark_menu_item_id;
@property (nonatomic, strong, readonly) NSString* selected_bookmark_menu_target_folder_id;

View File

@@ -5,10 +5,12 @@
*/
#include <LibWebView/Application.h>
#include <LibWebView/BookmarkStore.h>
#include <LibWebView/Menu.h>
#import <Interface/BookmarkFolder.h>
#import <Interface/BookmarksBar.h>
#import <Interface/Event.h>
#import <Interface/Menu.h>
#import <Utilities/Conversions.h>
@@ -287,6 +289,31 @@ static Optional<WebView::Menu&> find_bookmark_folder_by_id(WebView::Menu& menu,
[NSMenu popUpContextMenu:self.bookmark_folder_context_menu withEvent:event forView:control];
}
- (void)showContextMenu:(Gfx::IntPoint)content_position
view:(NSView*)view
bookmarkItem:(Optional<WebView::BookmarkItem const&>)item
targetFolderID:(Optional<String const&>)target_folder_id
{
auto* event = Ladybird::create_context_menu_mouse_event(view, content_position);
if (item.has_value()) {
self.selected_bookmark_menu_item_id = Ladybird::string_to_ns_string(item->id);
self.selected_bookmark_menu_target_folder_id = target_folder_id.has_value()
? Ladybird::string_to_ns_string(*target_folder_id)
: nil;
if (item->is_bookmark())
[NSMenu popUpContextMenu:self.bookmark_context_menu withEvent:event forView:view];
else if (item->is_folder())
[NSMenu popUpContextMenu:self.bookmark_folder_context_menu withEvent:event forView:view];
} else {
self.selected_bookmark_menu_item_id = @"";
self.selected_bookmark_menu_target_folder_id = nil;
[NSMenu popUpContextMenu:self.bookmarks_bar_context_menu withEvent:event forView:view];
}
}
- (void)showContextMenuForEvent:(NSEvent*)event
{
if (auto* button = [self bookmarkButtonForEvent:event]) {

View File

@@ -236,6 +236,9 @@ static void initialize_native_icon(WebView::Action& action, id control)
set_control_image(control, @"magnifyingglass");
break;
case WebView::ActionID::ManageBookmarks:
set_control_image(control, @"bookmark");
break;
case WebView::ActionID::ToggleBookmark:
[control setKeyEquivalent:@"d"];
break;

View File

@@ -10,6 +10,7 @@
#include <UI/Qt/EventLoopImplementationQt.h>
#include <UI/Qt/Settings.h>
#include <UI/Qt/StringUtils.h>
#include <UI/Qt/WebContentView.h>
#include <QClipboard>
#include <QDesktopServices>
@@ -248,6 +249,14 @@ void Application::update_bookmarks_bar_display(bool show_bookmarks_bar) const
}
}
void Application::show_bookmark_context_menu(Gfx::IntPoint content_position, Optional<WebView::BookmarkItem const&> item, Optional<String const&> target_folder_id)
{
if (auto* active_tab = this->active_tab()) {
auto position = active_tab->view().mapToGlobal(QPoint { content_position.x(), content_position.y() });
active_tab->bookmarks_bar().show_context_menu(position, item, target_folder_id);
}
}
Optional<Application::BookmarkID> Application::bookmark_item_id_for_context_menu() const
{
if (auto* active_tab = this->active_tab()) {

View File

@@ -49,6 +49,7 @@ private:
virtual void rebuild_bookmarks_menu() const override;
virtual void update_bookmarks_bar_display(bool) const override;
virtual void show_bookmark_context_menu(Gfx::IntPoint, Optional<WebView::BookmarkItem const&>, Optional<String const&> target_folder_id) override;
virtual Optional<BookmarkID> bookmark_item_id_for_context_menu() const override;
virtual NonnullRefPtr<BookmarkPromise> display_add_bookmark_dialog() const override;
virtual NonnullRefPtr<BookmarkPromise> display_edit_bookmark_dialog(WebView::BookmarkItem::Bookmark const& current_bookmark) const override;

View File

@@ -5,6 +5,7 @@
*/
#include <LibWebView/Application.h>
#include <LibWebView/BookmarkStore.h>
#include <UI/Qt/BookmarksBar.h>
#include <UI/Qt/Icon.h>
#include <UI/Qt/Menu.h>
@@ -95,6 +96,24 @@ void BookmarksBar::rebuild()
}
}
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) {

View File

@@ -8,6 +8,7 @@
#include <AK/Optional.h>
#include <AK/String.h>
#include <LibWebView/Forward.h>
#include <QToolBar>
@@ -24,6 +25,8 @@ public:
String const& selected_bookmark_menu_item_id() const { return m_selected_bookmark_menu_item_id; }
Optional<String> const& selected_bookmark_menu_target_folder_id() const { return m_selected_bookmark_menu_target_folder_id; }
void show_context_menu(QPoint, Optional<WebView::BookmarkItem const&>, Optional<String const&> target_folder_id);
private:
virtual bool eventFilter(QObject* object, QEvent* event) override;

View File

@@ -70,6 +70,7 @@ list(TRANSFORM INTERNAL_RESOURCES PREPEND "${LADYBIRD_SOURCE_DIR}/Base/res/ladyb
set(ABOUT_PAGES
about.html
bookmarks.html
newtab.html
processes.html
settings.html