diff --git a/app/theme/brave_theme_resources.grd b/app/theme/brave_theme_resources.grd index 58da4529ee29..d00e0d525ec3 100644 --- a/app/theme/brave_theme_resources.grd +++ b/app/theme/brave_theme_resources.grd @@ -21,6 +21,10 @@ + + + + diff --git a/app/theme/default_100_percent/common/brave_bookmark_folder_open-lin-dark.png b/app/theme/default_100_percent/common/brave_bookmark_folder_open-lin-dark.png new file mode 100644 index 000000000000..bb9048be9ffe Binary files /dev/null and b/app/theme/default_100_percent/common/brave_bookmark_folder_open-lin-dark.png differ diff --git a/app/theme/default_100_percent/common/brave_bookmark_folder_open-lin-light.png b/app/theme/default_100_percent/common/brave_bookmark_folder_open-lin-light.png new file mode 100644 index 000000000000..9a4541f0d717 Binary files /dev/null and b/app/theme/default_100_percent/common/brave_bookmark_folder_open-lin-light.png differ diff --git a/app/theme/default_100_percent/common/brave_bookmark_folder_open-win-dark.png b/app/theme/default_100_percent/common/brave_bookmark_folder_open-win-dark.png new file mode 100644 index 000000000000..4e38c4ea0919 Binary files /dev/null and b/app/theme/default_100_percent/common/brave_bookmark_folder_open-win-dark.png differ diff --git a/app/theme/default_100_percent/common/brave_bookmark_folder_open-win-light.png b/app/theme/default_100_percent/common/brave_bookmark_folder_open-win-light.png new file mode 100644 index 000000000000..4e38c4ea0919 Binary files /dev/null and b/app/theme/default_100_percent/common/brave_bookmark_folder_open-win-light.png differ diff --git a/app/theme/default_200_percent/common/brave_bookmark_folder_open-lin-dark.png b/app/theme/default_200_percent/common/brave_bookmark_folder_open-lin-dark.png new file mode 100644 index 000000000000..63c72f0453bc Binary files /dev/null and b/app/theme/default_200_percent/common/brave_bookmark_folder_open-lin-dark.png differ diff --git a/app/theme/default_200_percent/common/brave_bookmark_folder_open-lin-light.png b/app/theme/default_200_percent/common/brave_bookmark_folder_open-lin-light.png new file mode 100644 index 000000000000..68fb716a17ae Binary files /dev/null and b/app/theme/default_200_percent/common/brave_bookmark_folder_open-lin-light.png differ diff --git a/app/theme/default_200_percent/common/brave_bookmark_folder_open-win-dark.png b/app/theme/default_200_percent/common/brave_bookmark_folder_open-win-dark.png new file mode 100644 index 000000000000..38e3de4d6f08 Binary files /dev/null and b/app/theme/default_200_percent/common/brave_bookmark_folder_open-win-dark.png differ diff --git a/app/theme/default_200_percent/common/brave_bookmark_folder_open-win-light.png b/app/theme/default_200_percent/common/brave_bookmark_folder_open-win-light.png new file mode 100644 index 000000000000..38e3de4d6f08 Binary files /dev/null and b/app/theme/default_200_percent/common/brave_bookmark_folder_open-win-light.png differ diff --git a/brave_paks.gni b/brave_paks.gni index 93850571adc5..1c86ea2ac010 100644 --- a/brave_paks.gni +++ b/brave_paks.gni @@ -3,6 +3,7 @@ # You can obtain one at http://mozilla.org/MPL/2.0/. import("//brave/components/brave_webtorrent/browser/buildflags/buildflags.gni") +import("//brave/components/sidebar/buildflags/buildflags.gni") import("//brave/components/tor/buildflags/buildflags.gni") import("//build/config/locales.gni") import("//chrome/common/features.gni") @@ -83,6 +84,13 @@ template("brave_extra_paks") { ] } + if (enable_sidebar) { + sources += [ + "$root_gen_dir/brave/browser/resources/sidebar/sidebar_resources.pak", + ] + deps += [ "//brave/browser/resources/sidebar:resources" ] + } + if (enable_tor) { sources += [ "$root_gen_dir/brave/components/tor/resources/tor_resources.pak" ] diff --git a/browser/brave_content_browser_client.cc b/browser/brave_content_browser_client.cc index 02c787250aed..cbf85f9cc640 100644 --- a/browser/brave_content_browser_client.cc +++ b/browser/brave_content_browser_client.cc @@ -61,6 +61,7 @@ #include "brave/components/ftx/browser/buildflags/buildflags.h" #include "brave/components/gemini/browser/buildflags/buildflags.h" #include "brave/components/ipfs/buildflags/buildflags.h" +#include "brave/components/sidebar/buildflags/buildflags.h" #include "brave/components/skus/common/skus_sdk.mojom.h" #include "brave/components/speedreader/buildflags.h" #include "brave/components/speedreader/speedreader_util.h" @@ -176,6 +177,12 @@ using extensions::ChromeContentBrowserClientExtensionsPart; #include "brave/browser/ethereum_remote_client/ethereum_remote_client_service_factory.h" #endif +#if BUILDFLAG(ENABLE_SIDEBAR) +#include "brave/browser/ui/webui/sidebar/sidebar.mojom.h" +#include "brave/browser/ui/webui/sidebar/sidebar_bookmarks_ui.h" +#include "brave/components/sidebar/features.h" +#endif + #if !defined(OS_ANDROID) #include "brave/browser/new_tab/new_tab_shows_navigation_throttle.h" #include "brave/browser/ui/webui/brave_shields/shields_panel_ui.h" @@ -493,6 +500,13 @@ void BraveContentBrowserClient::RegisterBrowserInterfaceBindersForFrame( } #endif +#if BUILDFLAG(ENABLE_SIDEBAR) + if (base::FeatureList::IsEnabled(sidebar::kSidebarFeature)) { + chrome::internal::RegisterWebUIControllerInterfaceBinder< + sidebar::mojom::BookmarksPageHandlerFactory, SidebarBookmarksUI>(map); + } +#endif + // Brave News #if !defined(OS_ANDROID) if (base::FeatureList::IsEnabled(brave_today::features::kBraveNewsFeature)) { diff --git a/browser/resources/resource_ids b/browser/resources/resource_ids index 170b4e6d523b..5af5e35e3df8 100644 --- a/browser/resources/resource_ids +++ b/browser/resources/resource_ids @@ -1,4 +1,4 @@ -# Resource ids starting at 31000 are reserved for projects built on Chromium. +# Resource ids starting at 38000 are reserved for projects built on Chromium. { "SRCDIR": "../../..", "brave/common/extensions/api/brave_api_resources.grd": { @@ -127,4 +127,7 @@ "<(ROOT_GEN_DIR)/brave/web-ui-trezor_bridge/trezor_bridge.grd": { "includes": [51250] }, + "<(SHARED_INTERMEDIATE_DIR)/brave/browser/resources/sidebar/sidebar_resources.grd": { + "includes": [51500] + }, } diff --git a/browser/resources/sidebar/BUILD.gn b/browser/resources/sidebar/BUILD.gn new file mode 100644 index 000000000000..94a6ecb5a1f9 --- /dev/null +++ b/browser/resources/sidebar/BUILD.gn @@ -0,0 +1,34 @@ +# Copyright (c) 2022 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +import("//chrome/common/features.gni") +import("//tools/grit/grit_rule.gni") +import("//ui/webui/resources/tools/generate_grd.gni") + +grit("resources") { + defines = chrome_grit_defines + defines += + [ "SHARED_INTERMEDIATE_DIR=" + rebase_path(root_gen_dir, root_build_dir) ] + + enable_input_discovery_for_gn_analyze = false + source = "$target_gen_dir/sidebar_resources.grd" + deps = [ ":build_grd" ] + + outputs = [ + "grit/sidebar_resources.h", + "grit/sidebar_resources_map.cc", + "grit/sidebar_resources_map.h", + "sidebar_resources.pak", + ] + output_dir = "$root_gen_dir/brave/browser/resources/sidebar" + resource_ids = "//brave/browser/resources/resource_ids" +} + +generate_grd("build_grd") { + grdp_files = [ "$target_gen_dir/bookmarks/resources.grdp" ] + deps = [ "bookmarks:build_grdp" ] + grd_prefix = "sidebar" + out_grd = "$target_gen_dir/${grd_prefix}_resources.grd" +} diff --git a/browser/resources/sidebar/bookmarks/BUILD.gn b/browser/resources/sidebar/bookmarks/BUILD.gn new file mode 100644 index 000000000000..b227f7ebcec3 --- /dev/null +++ b/browser/resources/sidebar/bookmarks/BUILD.gn @@ -0,0 +1,85 @@ +# Copyright (c) 2022 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +import("//tools/grit/preprocess_if_expr.gni") +import("//tools/polymer/html_to_js.gni") +import("//tools/typescript/ts_library.gni") +import("//ui/webui/resources/tools/generate_grd.gni") + +preprocess_folder = + "$root_gen_dir/brave/browser/resources/sidebar/bookmarks/preprocessed" + +generate_grd("build_grdp") { + grd_prefix = "sidebar_bookmarks" + out_grd = "$target_gen_dir/resources.grdp" + deps = [ ":build_ts" ] + manifest_files = [ + "$root_gen_dir/brave/browser/resources/sidebar/bookmarks/tsconfig.manifest", + ] + input_files = [ "bookmarks.html" ] + input_files_base_dir = rebase_path(".", "//") +} + +preprocess_if_expr("preprocess") { + in_folder = "./" + out_folder = preprocess_folder + in_files = [ + "bookmarks_api_proxy.ts", + "bookmarks_drag_manager.ts", + ] +} + +preprocess_if_expr("preprocess_generated") { + deps = [ ":web_components" ] + in_folder = target_gen_dir + out_folder = preprocess_folder + in_files = [ + "bookmark_folder.ts", + "bookmarks_list.ts", + ] +} + +preprocess_if_expr("preprocess_mojo") { + deps = [ "//brave/browser/ui/webui/sidebar:mojo_bindings_webui_js" ] + in_folder = "$root_gen_dir/mojom-webui/brave/browser/ui/webui/sidebar/" + out_folder = preprocess_folder + out_manifest = "$target_gen_dir/preprocessed_mojo_manifest.json" + in_files = [ "sidebar.mojom-webui.js" ] +} + +html_to_js("web_components") { + js_files = [ + "bookmark_folder.ts", + "bookmarks_list.ts", + ] +} + +ts_library("build_ts") { + tsconfig_base = "tsconfig_base.json" + root_dir = "$target_gen_dir/preprocessed" + out_dir = "$target_gen_dir/tsc" + in_files = [ + "bookmark_folder.ts", + "bookmarks_list.ts", + "bookmarks_api_proxy.ts", + "bookmarks_drag_manager.ts", + "sidebar.mojom-webui.js", + ] + definitions = [ + "//tools/typescript/definitions/bookmark_manager_private.d.ts", + "//tools/typescript/definitions/bookmarks.d.ts", + "//tools/typescript/definitions/chrome_event.d.ts", + ] + deps = [ + "//third_party/polymer/v3_0:library", + "//ui/webui/resources:library", + "//ui/webui/resources/mojo:library", + ] + extra_deps = [ + ":preprocess", + ":preprocess_generated", + ":preprocess_mojo", + ] +} diff --git a/browser/resources/sidebar/bookmarks/bookmark_folder.html b/browser/resources/sidebar/bookmarks/bookmark_folder.html new file mode 100644 index 000000000000..43d9ddc7d08a --- /dev/null +++ b/browser/resources/sidebar/bookmarks/bookmark_folder.html @@ -0,0 +1,239 @@ + +
+ + + +
\ No newline at end of file diff --git a/browser/resources/sidebar/bookmarks/bookmark_folder.ts b/browser/resources/sidebar/bookmarks/bookmark_folder.ts new file mode 100644 index 000000000000..bedae0319c1b --- /dev/null +++ b/browser/resources/sidebar/bookmarks/bookmark_folder.ts @@ -0,0 +1,232 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.m.js'; +import 'chrome://resources/cr_elements/shared_vars_css.m.js'; +import 'chrome://resources/cr_elements/mwb_element_shared_style.js'; + +import {getFaviconForPageURL} from 'chrome://resources/js/icon.js'; +import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; + +import {BookmarksApiProxy} from './bookmarks_api_proxy.js'; + +/** Event interface for dom-repeat. */ +interface RepeaterMouseEvent extends MouseEvent { + clientX: number; + clientY: number; + model: { + item: chrome.bookmarks.BookmarkTreeNode, + }; +} + +export interface BookmarkFolderElement { + $: { + children: HTMLElement, + }; +} + +// Event name for open state of a folder being changed. +export const FOLDER_OPEN_CHANGED_EVENT = 'bookmark-folder-open-changed'; + +export class BookmarkFolderElement extends PolymerElement { + static get is() { + return 'bookmark-folder'; + } + + static get template() { + return html`{__html_template__}`; + } + + static get properties() { + return { + childDepth_: { + type: Number, + value: 1, + }, + + depth: { + type: Number, + observer: 'onDepthChanged_', + value: 0, + }, + + folder: Object, + + open_: { + type: Boolean, + value: false, + }, + + openFolders: { + type: Array, + observer: 'onOpenFoldersChanged_', + }, + }; + } + + private childDepth_: number; + depth: number; + folder: chrome.bookmarks.BookmarkTreeNode; + private open_: boolean; + openFolders: string[]; + private bookmarksApi_: BookmarksApiProxy = BookmarksApiProxy.getInstance(); + + static get observers() { + return [ + 'onChildrenLengthChanged_(folder.children.length)', + ]; + } + + private getAriaExpanded_(): string|undefined { + if (!this.folder.children || this.folder.children.length === 0) { + // Remove the attribute for empty folders that cannot be expanded. + return undefined; + } + + return this.open_ ? 'true' : 'false'; + } + + private onBookmarkAuxClick_(event: RepeaterMouseEvent) { + if (event.button !== 1) { + // Not a middle click. + return; + } + + event.preventDefault(); + event.stopPropagation(); + this.bookmarksApi_.openBookmark(event.model.item.url!, this.depth, { + middleButton: true, + altKey: event.altKey, + ctrlKey: event.ctrlKey, + metaKey: event.metaKey, + shiftKey: event.shiftKey, + }); + } + + private onBookmarkClick_(event: RepeaterMouseEvent) { + event.preventDefault(); + event.stopPropagation(); + this.bookmarksApi_.openBookmark(event.model.item.url!, this.depth, { + middleButton: false, + altKey: event.altKey, + ctrlKey: event.ctrlKey, + metaKey: event.metaKey, + shiftKey: event.shiftKey, + }); + } + + private onBookmarkContextMenu_(event: RepeaterMouseEvent) { + event.preventDefault(); + event.stopPropagation(); + this.bookmarksApi_.showContextMenu( + event.model.item.id, event.clientX, event.clientY); + } + + private onFolderContextMenu_(event: MouseEvent) { + event.preventDefault(); + event.stopPropagation(); + this.bookmarksApi_.showContextMenu( + this.folder.id, event.clientX, event.clientY); + } + + private getBookmarkIcon_(url: string): string { + return getFaviconForPageURL(url, false); + } + + private onChildrenLengthChanged_() { + if (this.folder.children) { + this.style.setProperty( + '--child-count', this.folder.children!.length.toString()); + } else { + this.style.setProperty('--child-count', '0'); + } + } + + private onDepthChanged_() { + this.childDepth_ = this.depth + 1; + this.style.setProperty('--node-depth', `${this.depth}`); + this.style.setProperty('--child-depth', `${this.childDepth_}`); + } + + private onFolderClick_(event: Event) { + event.preventDefault(); + event.stopPropagation(); + + if (!this.folder.children || this.folder.children.length === 0) { + // No reason to open if there are no children to show. + return; + } + + this.open_ = !this.open_; + this.dispatchEvent(new CustomEvent(FOLDER_OPEN_CHANGED_EVENT, { + bubbles: true, + composed: true, + detail: { + id: this.folder.id, + open: this.open_, + } + })); + } + + private onOpenFoldersChanged_() { + this.open_ = + Boolean(this.openFolders) && this.openFolders.includes(this.folder.id); + } + + private getFocusableRows_(): HTMLElement[] { + return Array.from( + this.shadowRoot!.querySelectorAll('.row, bookmark-folder')); + } + + moveFocus(delta: -1|1): boolean { + const currentFocus = this.shadowRoot!.activeElement; + if (currentFocus instanceof BookmarkFolderElement && + currentFocus.moveFocus(delta)) { + // If focus is already inside a nested folder, delegate the focus to the + // nested folder and return early if successful. + return true; + } + + let moveFocusTo = null; + const focusableRows = this.getFocusableRows_(); + if (currentFocus) { + // If focus is in this folder, move focus to the next or previous + // focusable row. + const currentFocusIndex = + focusableRows.indexOf(currentFocus as HTMLElement); + moveFocusTo = focusableRows[currentFocusIndex + delta]; + } else { + // If focus is not in this folder yet, move focus to either end. + moveFocusTo = delta === 1 ? focusableRows[0] : + focusableRows[focusableRows.length - 1]; + } + + if (moveFocusTo instanceof BookmarkFolderElement) { + return moveFocusTo.moveFocus(delta); + } else if (moveFocusTo) { + moveFocusTo.focus(); + return true; + } else { + return false; + } + } +} + +customElements.define(BookmarkFolderElement.is, BookmarkFolderElement); + +interface DraggableElement extends HTMLElement { + dataBookmark: chrome.bookmarks.BookmarkTreeNode; +} + +export function getBookmarkFromElement(element: HTMLElement) { + return (element as DraggableElement).dataBookmark; +} + +export function isValidDropTarget(element: HTMLElement) { + return element.id === 'folder' || element.classList.contains('bookmark'); +} + +export function isBookmarkFolderElement(element: HTMLElement): boolean { + return element.id === 'folder'; +} \ No newline at end of file diff --git a/browser/resources/sidebar/bookmarks/bookmarks.html b/browser/resources/sidebar/bookmarks/bookmarks.html new file mode 100644 index 000000000000..f720ed28842a --- /dev/null +++ b/browser/resources/sidebar/bookmarks/bookmarks.html @@ -0,0 +1,34 @@ + + + + + $i18n{bookmarksTitle} + + + + + + + + + + diff --git a/browser/resources/sidebar/bookmarks/bookmarks_api_proxy.ts b/browser/resources/sidebar/bookmarks/bookmarks_api_proxy.ts new file mode 100644 index 000000000000..545934ef6bf1 --- /dev/null +++ b/browser/resources/sidebar/bookmarks/bookmarks_api_proxy.ts @@ -0,0 +1,78 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.js'; +import 'chrome://resources/mojo/url/mojom/url.mojom-lite.js'; + +import {ChromeEvent} from '/tools/typescript/definitions/chrome_event.js'; +import {ClickModifiers} from 'chrome://resources/mojo/ui/base/mojom/window_open_disposition.mojom-webui.js'; + +import {BookmarksPageHandlerFactory, BookmarksPageHandlerRemote} from './sidebar.mojom-webui.js'; + +let instance: BookmarksApiProxy|null = null; + +export class BookmarksApiProxy { + callbackRouter: {[key: string]: ChromeEvent}; + handler: BookmarksPageHandlerRemote; + + constructor() { + this.callbackRouter = { + onChanged: chrome.bookmarks.onChanged, + onChildrenReordered: chrome.bookmarks.onChildrenReordered, + onCreated: chrome.bookmarks.onCreated, + onMoved: chrome.bookmarks.onMoved, + onRemoved: chrome.bookmarks.onRemoved, + }; + + this.handler = new BookmarksPageHandlerRemote(); + + const factory = BookmarksPageHandlerFactory.getRemote(); + factory.createBookmarksPageHandler( + this.handler.$.bindNewPipeAndPassReceiver()); + } + + cutBookmark(id: string): Promise { + chrome.bookmarkManagerPrivate.cut([id]); + return Promise.resolve(); + } + + copyBookmark(id: string): Promise { + return new Promise(resolve => { + chrome.bookmarkManagerPrivate.copy([id], resolve); + }); + } + + getFolders(): Promise { + return new Promise(resolve => chrome.bookmarks.getTree(results => { + if (results[0] && results[0].children) { + resolve(results[0].children); + return; + } + resolve([]); + })); + } + + openBookmark(url: string, depth: number, clickModifiers: ClickModifiers) { + this.handler.openBookmark({url}, depth, clickModifiers); + } + + pasteToBookmark(parentId: string, destinationId?: string): Promise { + const destination = destinationId ? [destinationId] : []; + return new Promise(resolve => { + chrome.bookmarkManagerPrivate.paste(parentId, destination, resolve); + }); + } + + showContextMenu(id: string, x: number, y: number) { + this.handler.showContextMenu(id, {x, y}); + } + + static getInstance() { + return instance || (instance = new BookmarksApiProxy()); + } + + static setInstance(obj: BookmarksApiProxy) { + instance = obj; + } +} \ No newline at end of file diff --git a/browser/resources/sidebar/bookmarks/bookmarks_drag_manager.ts b/browser/resources/sidebar/bookmarks/bookmarks_drag_manager.ts new file mode 100644 index 000000000000..04cabff4f163 --- /dev/null +++ b/browser/resources/sidebar/bookmarks/bookmarks_drag_manager.ts @@ -0,0 +1,266 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../strings.m.js'; + +import {EventTracker} from 'chrome://resources/js/event_tracker.m.js'; +import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js'; + +import {getBookmarkFromElement, isBookmarkFolderElement, isValidDropTarget} from './bookmark_folder.js'; + +export const DROP_POSITION_ATTR = 'drop-position'; + +const ROOT_FOLDER_ID = '0'; + +// Ms to wait during a dragover to open closed folder. +let folderOpenerTimeoutDelay = 1000; +export function overrideFolderOpenerTimeoutDelay(ms: number) { + folderOpenerTimeoutDelay = ms; +} + +export enum DropPosition { + ABOVE = 'above', + INTO = 'into', + BELOW = 'below', +} + +interface BookmarksDragDelegate extends HTMLElement { + getAscendants(bookmarkId: string): string[]; + getIndex(bookmark: chrome.bookmarks.BookmarkTreeNode): number; + isFolderOpen(bookmark: chrome.bookmarks.BookmarkTreeNode): boolean; + openFolder(folderId: string): void; +} + +class DragSession { + private delegate_: BookmarksDragDelegate; + private dragData_: chrome.bookmarkManagerPrivate.DragData; + private lastDragOverElement_: HTMLElement|null = null; + private lastPointerWasTouch_ = false; + private folderOpenerTimeout_: number|null = null; + + constructor( + delegate: BookmarksDragDelegate, + dragData: chrome.bookmarkManagerPrivate.DragData) { + this.delegate_ = delegate; + this.dragData_ = dragData; + } + + start(e: DragEvent) { + chrome.bookmarkManagerPrivate.startDrag( + this.dragData_.elements!.map(bookmark => bookmark.id), 0, + this.lastPointerWasTouch_, e.clientX, e.clientY); + } + + update(e: DragEvent) { + const dragOverElement = e.composedPath().find(target => { + return target instanceof HTMLElement && isValidDropTarget(target); + }) as HTMLElement; + if (!dragOverElement) { + return; + } + + if (dragOverElement !== this.lastDragOverElement_) { + this.resetState_(); + } + + const dragOverBookmark = getBookmarkFromElement(dragOverElement); + const ascendants = this.delegate_.getAscendants(dragOverBookmark.id); + const isInvalidDragOverTarget = dragOverBookmark.unmodifiable || + this.dragData_.elements && + this.dragData_.elements.some( + element => ascendants.indexOf(element.id) !== -1); + if (isInvalidDragOverTarget) { + this.lastDragOverElement_ = null; + return; + } + + const isDraggingOverFolder = isBookmarkFolderElement(dragOverElement); + const dragOverElRect = dragOverElement.getBoundingClientRect(); + const dragOverYRatio = + (e.clientY - dragOverElRect.top) / dragOverElRect.height; + + let dropPosition; + if (isDraggingOverFolder) { + const folderIsOpen = this.delegate_.isFolderOpen(dragOverBookmark); + if (dragOverBookmark.parentId === ROOT_FOLDER_ID) { + // Cannot drag above or below children of root folder. + dropPosition = DropPosition.INTO; + } else if (dragOverYRatio <= .25) { + dropPosition = DropPosition.ABOVE; + } else if (dragOverYRatio <= .75) { + dropPosition = DropPosition.INTO; + } else if (folderIsOpen) { + // If a folder is open, its child bookmarks appear immediately below it + // so it should not be possible to drop a bookmark right below an open + // folder. + dropPosition = DropPosition.INTO; + } else { + dropPosition = DropPosition.BELOW; + } + } else { + dropPosition = + dragOverYRatio <= .5 ? DropPosition.ABOVE : DropPosition.BELOW; + } + dragOverElement.setAttribute(DROP_POSITION_ATTR, dropPosition); + + if (dropPosition === DropPosition.INTO && + !this.delegate_.isFolderOpen(dragOverBookmark) && + !this.folderOpenerTimeout_) { + // Queue a timeout to auto-open the dragged over folder. + this.folderOpenerTimeout_ = setTimeout(() => { + this.delegate_.openFolder(dragOverBookmark.id); + this.folderOpenerTimeout_ = null; + }, folderOpenerTimeoutDelay); + } + + this.lastDragOverElement_ = dragOverElement; + } + + cancel() { + this.resetState_(); + this.lastDragOverElement_ = null; + } + + finish() { + if (!this.lastDragOverElement_) { + return; + } + + const dropTargetBookmark = + getBookmarkFromElement(this.lastDragOverElement_); + const dropPosition = this.lastDragOverElement_.getAttribute( + DROP_POSITION_ATTR) as DropPosition; + this.resetState_(); + + if (isBookmarkFolderElement(this.lastDragOverElement_) && + dropPosition === DropPosition.INTO) { + chrome.bookmarkManagerPrivate.drop( + dropTargetBookmark.id, /* index */ undefined, + /* callback */ undefined); + return; + } + + let toIndex = this.delegate_.getIndex(dropTargetBookmark); + toIndex += dropPosition === DropPosition.BELOW ? 1 : 0; + chrome.bookmarkManagerPrivate.drop( + dropTargetBookmark.parentId!, toIndex, /* callback */ undefined); + } + + private resetState_() { + if (this.lastDragOverElement_) { + this.lastDragOverElement_.removeAttribute(DROP_POSITION_ATTR); + } + + if (this.folderOpenerTimeout_ !== null) { + clearTimeout(this.folderOpenerTimeout_); + this.folderOpenerTimeout_ = null; + } + } + + static createFromBookmark( + delegate: BookmarksDragDelegate, + bookmark: chrome.bookmarks.BookmarkTreeNode) { + return new DragSession(delegate, { + elements: [bookmark], + sameProfile: true, + }); + } +} + +export class BookmarksDragManager { + private delegate_: BookmarksDragDelegate; + private dragSession_: DragSession|null; + private eventTracker_: EventTracker = new EventTracker(); + + constructor(delegate: BookmarksDragDelegate) { + this.delegate_ = delegate; + } + + startObserving() { + this.eventTracker_.add( + this.delegate_, 'dragstart', e => this.onDragStart_(e as DragEvent)); + this.eventTracker_.add( + this.delegate_, 'dragover', e => this.onDragOver_(e as DragEvent)); + this.eventTracker_.add( + this.delegate_, 'dragleave', () => this.onDragLeave_()); + this.eventTracker_.add(this.delegate_, 'dragend', () => this.cancelDrag_()); + this.eventTracker_.add( + this.delegate_, 'drop', e => this.onDrop_(e as DragEvent)); + + if (loadTimeData.getBoolean('bookmarksDragAndDropEnabled')) { + chrome.bookmarkManagerPrivate.onDragEnter.addListener( + (dragData: chrome.bookmarkManagerPrivate.DragData) => + this.onChromeDragEnter_(dragData)); + chrome.bookmarkManagerPrivate.onDragLeave.addListener( + () => this.cancelDrag_()); + } + } + + stopObserving() { + this.eventTracker_.removeAll(); + } + + private cancelDrag_() { + if (!this.dragSession_) { + return; + } + this.dragSession_.cancel(); + this.dragSession_ = null; + } + + private onChromeDragEnter_(dragData: chrome.bookmarkManagerPrivate.DragData) { + if (this.dragSession_) { + // A drag session is already in flight. + return; + } + + this.dragSession_ = new DragSession(this.delegate_, dragData); + } + + private onDragStart_(e: DragEvent) { + e.preventDefault(); + if (!loadTimeData.getBoolean('bookmarksDragAndDropEnabled')) { + return; + } + + const bookmark = getBookmarkFromElement( + e.composedPath().find(target => (target as HTMLElement).draggable) as + HTMLElement); + if (!bookmark || + /* Cannot drag root's children. */ bookmark.parentId === + ROOT_FOLDER_ID || + bookmark.unmodifiable) { + return; + } + + this.dragSession_ = + DragSession.createFromBookmark(this.delegate_, bookmark); + this.dragSession_.start(e); + } + + private onDragOver_(e: DragEvent) { + e.preventDefault(); + if (!this.dragSession_) { + return; + } + this.dragSession_.update(e); + } + + private onDragLeave_() { + if (!this.dragSession_) { + return; + } + + this.dragSession_.cancel(); + } + + private onDrop_(e: DragEvent) { + if (!this.dragSession_) { + return; + } + + e.preventDefault(); + this.dragSession_.finish(); + } +} \ No newline at end of file diff --git a/browser/resources/sidebar/bookmarks/bookmarks_list.html b/browser/resources/sidebar/bookmarks/bookmarks_list.html new file mode 100644 index 000000000000..e24c1622e104 --- /dev/null +++ b/browser/resources/sidebar/bookmarks/bookmarks_list.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/browser/resources/sidebar/bookmarks/bookmarks_list.ts b/browser/resources/sidebar/bookmarks/bookmarks_list.ts new file mode 100644 index 000000000000..c012428c19a2 --- /dev/null +++ b/browser/resources/sidebar/bookmarks/bookmarks_list.ts @@ -0,0 +1,314 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; + +import {BookmarkFolderElement, FOLDER_OPEN_CHANGED_EVENT, getBookmarkFromElement, isBookmarkFolderElement} from './bookmark_folder.js'; +import {BookmarksApiProxy} from './bookmarks_api_proxy.js'; +import {BookmarksDragManager} from './bookmarks_drag_manager.js'; + +// Key for localStorage object that refers to all the open folders. +export const LOCAL_STORAGE_OPEN_FOLDERS_KEY = 'openFolders'; + +export class BookmarksListElement extends PolymerElement { + static get is() { + return 'bookmarks-list'; + } + + static get template() { + return html`{__html_template__}`; + } + + static get properties() { + return { + folders_: { + type: Array, + value: () => [], + }, + + openFolders_: { + type: Array, + value: () => [], + }, + }; + } + + private bookmarksApi_: BookmarksApiProxy = BookmarksApiProxy.getInstance(); + private bookmarksDragManager_: BookmarksDragManager = + new BookmarksDragManager(this); + private listeners_ = new Map(); + private folders_: chrome.bookmarks.BookmarkTreeNode[]; + private openFolders_: string[]; + + ready() { + super.ready(); + this.addEventListener( + FOLDER_OPEN_CHANGED_EVENT, + e => this.onFolderOpenChanged_( + e as CustomEvent<{id: string, open: boolean}>)); + this.addEventListener('keydown', e => this.onKeydown_(e)); + } + + connectedCallback() { + super.connectedCallback(); + this.setAttribute('role', 'tree'); + this.bookmarksApi_.getFolders().then(folders => { + this.folders_ = folders; + + this.addListener_( + 'onChildrenReordered', + (id: string, reorderedInfo: chrome.bookmarks.ReorderInfo) => + this.onChildrenReordered_(id, reorderedInfo)); + this.addListener_( + 'onChanged', + (id: string, changedInfo: chrome.bookmarks.ChangeInfo) => + this.onChanged_(id, changedInfo)); + this.addListener_( + 'onCreated', + (_id: string, node: chrome.bookmarks.BookmarkTreeNode) => + this.onCreated_(node)); + this.addListener_( + 'onMoved', + (_id: string, movedInfo: chrome.bookmarks.MoveInfo) => + this.onMoved_(movedInfo)); + this.addListener_('onRemoved', (id: string) => this.onRemoved_(id)); + + try { + const openFolders = window.localStorage[LOCAL_STORAGE_OPEN_FOLDERS_KEY]; + this.openFolders_ = JSON.parse(openFolders); + } catch (error) { + this.openFolders_ = [this.folders_[0]!.id]; + window.localStorage[LOCAL_STORAGE_OPEN_FOLDERS_KEY] = + JSON.stringify(this.openFolders_); + } + + this.bookmarksDragManager_.startObserving(); + }); + } + + disconnectedCallback() { + for (const [eventName, callback] of this.listeners_.entries()) { + this.bookmarksApi_.callbackRouter[eventName]!.removeListener(callback); + } + this.bookmarksDragManager_.stopObserving(); + } + + /** BookmarksDragDelegate */ + getAscendants(bookmarkId: string): string[] { + const path = this.findPathToId_(bookmarkId); + return path.map(bookmark => bookmark.id); + } + + /** BookmarksDragDelegate */ + getIndex(bookmark: chrome.bookmarks.BookmarkTreeNode): number { + const path = this.findPathToId_(bookmark.id); + const parent = path[path.length - 2]; + if (!parent || !parent.children) { + return -1; + } + return parent.children.findIndex((child) => child.id === bookmark.id); + } + /** BookmarksDragDelegate */ + isFolderOpen(bookmark: chrome.bookmarks.BookmarkTreeNode): boolean { + return this.openFolders_.some(id => bookmark.id === id); + } + + /** BookmarksDragDelegate */ + openFolder(folderId: string) { + this.changeFolderOpenStatus_(folderId, true); + } + + private addListener_(eventName: string, callback: Function): void { + this.bookmarksApi_.callbackRouter[eventName]!.addListener(callback); + this.listeners_.set(eventName, callback); + } + + /** + * Finds the node within the nested array of folders and returns the path to + * the node in the tree. + */ + private findPathToId_(id: string): chrome.bookmarks.BookmarkTreeNode[] { + const path: chrome.bookmarks.BookmarkTreeNode[] = []; + + function findPathByIdInternal_( + id: string, node: chrome.bookmarks.BookmarkTreeNode) { + if (node.id === id) { + path.push(node); + return true; + } + + if (!node.children) { + return false; + } + + path.push(node); + const foundInChildren = + node.children.some(child => findPathByIdInternal_(id, child)); + if (!foundInChildren) { + path.pop(); + } + + return foundInChildren; + } + + this.folders_.some(folder => findPathByIdInternal_(id, folder)); + return path; + } + + /** + * Reduces an array of nodes to a string to notify Polymer of changes to the + * nested array. + */ + private getPathString_(path: chrome.bookmarks.BookmarkTreeNode[]): string { + return path.reduce((reducedString, pathItem, index) => { + if (index === 0) { + return `folders_.${this.folders_.indexOf(pathItem)}`; + } + + const parent = path[index - 1]; + return `${reducedString}.children.${parent!.children!.indexOf(pathItem)}`; + }, ''); + } + + private onChanged_(id: string, changedInfo: chrome.bookmarks.ChangeInfo) { + const path = this.findPathToId_(id); + Object.assign(path[path.length - 1], changedInfo); + + const pathString = this.getPathString_(path); + Object.keys(changedInfo) + .forEach(key => this.notifyPath(`${pathString}.${key}`)); + } + + private onChildrenReordered_( + id: string, reorderedInfo: chrome.bookmarks.ReorderInfo) { + const path = this.findPathToId_(id); + const parent = path[path.length - 1]; + const childById = parent!.children!.reduce((map, node) => { + map.set(node.id, node); + return map; + }, new Map()); + parent!.children = reorderedInfo.childIds.map(id => childById.get(id)); + const pathString = this.getPathString_(path); + this.notifyPath(`${pathString}.children`); + } + + private onCreated_(node: chrome.bookmarks.BookmarkTreeNode) { + const pathToParent = this.findPathToId_(node.parentId as string); + const pathToParentString = this.getPathString_(pathToParent); + const parent = pathToParent[pathToParent.length - 1]; + if (parent && !parent.children) { + // Newly created folders in this session may not have an array of + // children yet, so create an empty one. + parent.children = []; + } + this.splice(`${pathToParentString}.children`, node.index!, 0, node); + } + + private changeFolderOpenStatus_(id: string, open: boolean) { + const alreadyOpenIndex = this.openFolders_.indexOf(id); + if (open && alreadyOpenIndex === -1) { + this.openFolders_.push(id); + } else if (!open) { + this.openFolders_.splice(alreadyOpenIndex, 1); + } + + // Assign to a new array so that listeners are triggered. + this.openFolders_ = [...this.openFolders_]; + window.localStorage[LOCAL_STORAGE_OPEN_FOLDERS_KEY] = + JSON.stringify(this.openFolders_); + } + + private onFolderOpenChanged_(event: CustomEvent) { + const {id, open} = event.detail; + this.changeFolderOpenStatus_(id, open); + } + + private onKeydown_(event: KeyboardEvent) { + if (['ArrowDown', 'ArrowUp'].includes(event.key)) { + this.handleArrowKeyNavigation_(event); + return; + } + + if (!event.ctrlKey && !event.metaKey) { + return; + } + + event.preventDefault(); + const eventTarget = event.composedPath()[0] as HTMLElement; + const bookmarkData = getBookmarkFromElement(eventTarget); + if (!bookmarkData) { + return; + } + + if (event.key === 'x') { + this.bookmarksApi_.cutBookmark(bookmarkData.id); + } else if (event.key === 'c') { + this.bookmarksApi_.copyBookmark(bookmarkData.id); + } else if (event.key === 'v') { + if (isBookmarkFolderElement(eventTarget)) { + this.bookmarksApi_.pasteToBookmark(bookmarkData.id); + } else { + this.bookmarksApi_.pasteToBookmark( + bookmarkData.parentId!, bookmarkData.id); + } + } + } + + private handleArrowKeyNavigation_(event: KeyboardEvent) { + if (!(this.shadowRoot!.activeElement instanceof BookmarkFolderElement)) { + // If the key event did not happen within a BookmarkFolderElement, do + // not do anything. + return; + } + + // Prevent arrow keys from causing scroll. + event.preventDefault(); + + const allFolderElements: BookmarkFolderElement[] = + Array.from(this.shadowRoot!.querySelectorAll('bookmark-folder')); + + const delta = event.key === 'ArrowUp' ? -1 : 1; + let currentIndex = + allFolderElements.indexOf(this.shadowRoot!.activeElement); + let focusHasMoved = false; + while (!focusHasMoved) { + focusHasMoved = allFolderElements[currentIndex]!.moveFocus(delta); + currentIndex = (currentIndex + delta + allFolderElements.length) % + allFolderElements.length; + } + } + + private onMoved_(movedInfo: chrome.bookmarks.MoveInfo) { + // Get old path and remove node from oldParent at oldIndex. + const oldParentPath = this.findPathToId_(movedInfo.oldParentId); + const oldParentPathString = this.getPathString_(oldParentPath); + const oldParent = oldParentPath[oldParentPath.length - 1]; + const movedNode = oldParent!.children![movedInfo.oldIndex]; + Object.assign( + movedNode, {index: movedInfo.index, parentId: movedInfo.parentId}); + this.splice(`${oldParentPathString}.children`, movedInfo.oldIndex, 1); + + // Get new parent's path and add the node to the new parent at index. + const newParentPath = this.findPathToId_(movedInfo.parentId); + const newParentPathString = this.getPathString_(newParentPath); + const newParent = newParentPath[newParentPath.length - 1]; + if (newParent && !newParent.children) { + newParent.children = []; + } + this.splice( + `${newParentPathString}.children`, movedInfo.index, 0, movedNode); + } + + private onRemoved_(id: string) { + const oldPath = this.findPathToId_(id); + const removedNode = oldPath.pop()!; + const oldParent = oldPath[oldPath.length - 1]!; + const oldParentPathString = this.getPathString_(oldPath); + this.splice( + `${oldParentPathString}.children`, + oldParent.children!.indexOf(removedNode), 1); + } +} + +customElements.define(BookmarksListElement.is, BookmarksListElement); diff --git a/browser/resources/sidebar/bookmarks/tsconfig_base.json b/browser/resources/sidebar/bookmarks/tsconfig_base.json new file mode 100644 index 000000000000..7ceb2c0b2e52 --- /dev/null +++ b/browser/resources/sidebar/bookmarks/tsconfig_base.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../../../tools/typescript/tsconfig_base.json", + "compilerOptions": { + "allowJs": true, + "noPropertyAccessFromIndexSignature": false, + "noUnusedLocals": false, + "strictPropertyInitialization": false + } +} diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index 27fdc6cb4c83..74525f961b6a 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -357,8 +357,16 @@ source_set("ui") { if (enable_sidebar) { deps += [ + "//brave/browser/resources/sidebar:resources", "//brave/browser/ui/sidebar", + "//brave/browser/ui/webui/sidebar:mojo_bindings", "//brave/components/sidebar", + "//chrome/app:generated_resources", + "//components/bookmarks/browser", + "//components/favicon_base", + "//mojo/public/cpp/bindings", + "//ui/base/mojom", + "//ui/webui", ] sources += [ @@ -390,6 +398,10 @@ source_set("ui") { "views/sidebar/sidebar_items_scroll_view.h", "views/sidebar/sidebar_show_options_event_detect_widget.cc", "views/sidebar/sidebar_show_options_event_detect_widget.h", + "webui/sidebar/sidebar_bookmarks_page_handler.cc", + "webui/sidebar/sidebar_bookmarks_page_handler.h", + "webui/sidebar/sidebar_bookmarks_ui.cc", + "webui/sidebar/sidebar_bookmarks_ui.h", ] } diff --git a/browser/ui/sidebar/sidebar.h b/browser/ui/sidebar/sidebar.h index b30e1816dae8..36c4164e34d7 100644 --- a/browser/ui/sidebar/sidebar.h +++ b/browser/ui/sidebar/sidebar.h @@ -6,8 +6,18 @@ #ifndef BRAVE_BROWSER_UI_SIDEBAR_SIDEBAR_H_ #define BRAVE_BROWSER_UI_SIDEBAR_SIDEBAR_H_ +#include + #include "brave/components/sidebar/sidebar_service.h" +namespace gfx { +class Point; +} // namespace gfx + +namespace ui { +class MenuModel; +} // namespace ui + namespace sidebar { // Interact with UI layer. @@ -17,6 +27,10 @@ class Sidebar { SidebarService::ShowSidebarOption show_option) = 0; // Update current sidebar UI. virtual void UpdateSidebar() = 0; + virtual void ShowCustomContextMenu( + const gfx::Point& point, + std::unique_ptr menu_model) = 0; + virtual void HideCustomContextMenu() = 0; protected: virtual ~Sidebar() {} diff --git a/browser/ui/views/sidebar/sidebar_container_view.cc b/browser/ui/views/sidebar/sidebar_container_view.cc index b2d261576541..f2d8f2c91a42 100644 --- a/browser/ui/views/sidebar/sidebar_container_view.cc +++ b/browser/ui/views/sidebar/sidebar_container_view.cc @@ -5,6 +5,8 @@ #include "brave/browser/ui/views/sidebar/sidebar_container_view.h" +#include + #include "base/bind.h" #include "brave/browser/themes/theme_properties.h" #include "brave/browser/ui/brave_browser.h" @@ -18,11 +20,14 @@ #include "chrome/browser/ui/browser_window.h" #include "chrome/browser/ui/views/frame/browser_view.h" #include "content/public/browser/browser_context.h" +#include "content/public/browser/web_contents.h" +#include "ui/base/models/menu_model.h" #include "ui/base/theme_provider.h" #include "ui/events/event_observer.h" #include "ui/events/types/event_type.h" #include "ui/gfx/geometry/point.h" #include "ui/views/border.h" +#include "ui/views/controls/menu/menu_runner.h" #include "ui/views/controls/webview/webview.h" #include "ui/views/event_monitor.h" #include "ui/views/widget/widget.h" @@ -109,6 +114,33 @@ void SidebarContainerView::UpdateSidebar() { sidebar_control_view_->Update(); } +void SidebarContainerView::ShowCustomContextMenu( + const gfx::Point& point, + std::unique_ptr menu_model) { + // Show context menu at in screen coordinates. + gfx::Point screen_point = point; + ConvertPointToScreen(sidebar_panel_view_, &screen_point); + context_menu_model_ = std::move(menu_model); + context_menu_runner_ = std::make_unique( + context_menu_model_.get(), + views::MenuRunner::HAS_MNEMONICS | views::MenuRunner::CONTEXT_MENU); + const int active_index = sidebar_model_->active_index(); + if (active_index == -1) { + LOG(ERROR) << __func__ + << " sidebar panel UI is loaded at non sidebar panel!"; + return; + } + context_menu_runner_->RunMenuAt( + GetWidget(), nullptr, gfx::Rect(screen_point, gfx::Size()), + views::MenuAnchorPosition::kTopLeft, ui::MENU_SOURCE_MOUSE, + sidebar_model_->GetWebContentsAt(active_index)->GetContentNativeView()); +} + +void SidebarContainerView::HideCustomContextMenu() { + if (context_menu_runner_) + context_menu_runner_->Cancel(); +} + void SidebarContainerView::UpdateBackgroundAndBorder() { if (const ui::ThemeProvider* theme_provider = GetThemeProvider()) { constexpr int kBorderThickness = 1; diff --git a/browser/ui/views/sidebar/sidebar_container_view.h b/browser/ui/views/sidebar/sidebar_container_view.h index cf434bbbc27c..cce208015c1b 100644 --- a/browser/ui/views/sidebar/sidebar_container_view.h +++ b/browser/ui/views/sidebar/sidebar_container_view.h @@ -22,6 +22,14 @@ class EventMonitor; class WebView; } // namespace views +namespace ui { +class MenuModel; +} // namespace ui + +namespace views { +class MenuRunner; +} // namespace views + class BraveBrowser; class SidebarControlView; @@ -46,6 +54,10 @@ class SidebarContainerView void SetSidebarShowOption( sidebar::SidebarService::ShowSidebarOption show_option) override; void UpdateSidebar() override; + void ShowCustomContextMenu( + const gfx::Point& point, + std::unique_ptr menu_model) override; + void HideCustomContextMenu() override; // views::View overrides: void Layout() override; @@ -100,6 +112,8 @@ class SidebarContainerView base::ScopedObservation observed_{this}; + std::unique_ptr context_menu_runner_; + std::unique_ptr context_menu_model_; }; #endif // BRAVE_BROWSER_UI_VIEWS_SIDEBAR_SIDEBAR_CONTAINER_VIEW_H_ diff --git a/browser/ui/webui/brave_web_ui_controller_factory.cc b/browser/ui/webui/brave_web_ui_controller_factory.cc index 1fa1189992ec..0462fdabda46 100644 --- a/browser/ui/webui/brave_web_ui_controller_factory.cc +++ b/browser/ui/webui/brave_web_ui_controller_factory.cc @@ -20,6 +20,7 @@ #include "brave/common/webui_url_constants.h" #include "brave/components/brave_vpn/buildflags/buildflags.h" #include "brave/components/ipfs/buildflags/buildflags.h" +#include "brave/components/sidebar/buildflags/buildflags.h" #include "brave/components/tor/buildflags/buildflags.h" #include "build/build_config.h" #include "chrome/browser/profiles/profile.h" @@ -60,6 +61,12 @@ #include "brave/browser/ui/webui/tor_internals_ui.h" #endif +#if BUILDFLAG(ENABLE_SIDEBAR) +#include "brave/browser/ui/webui/sidebar/sidebar_bookmarks_ui.h" +#include "brave/components/sidebar/constants.h" +#include "brave/components/sidebar/features.h" +#endif + using content::WebUI; using content::WebUIController; @@ -127,6 +134,11 @@ WebUIController* NewWebUI(WebUI* web_ui, const GURL& url) { #if BUILDFLAG(ENABLE_TOR) } else if (host == kTorInternalsHost) { return new TorInternalsUI(web_ui, url.host()); +#endif +#if BUILDFLAG(ENABLE_SIDEBAR) + } else if (host == kSidebarBookmarksHost && + base::FeatureList::IsEnabled(sidebar::kSidebarFeature)) { + return new SidebarBookmarksUI(web_ui); #endif } return nullptr; @@ -151,6 +163,10 @@ WebUIFactoryFunction GetWebUIFactoryFunction(WebUI* web_ui, #if BUILDFLAG(ENABLE_BRAVE_VPN) && !defined(OS_ANDROID) (url.host_piece() == kVPNPanelHost && brave_vpn::IsBraveVPNEnabled()) || #endif +#if BUILDFLAG(ENABLE_SIDEBAR) + (url.host_piece() == kSidebarBookmarksHost && + base::FeatureList::IsEnabled(sidebar::kSidebarFeature)) || +#endif #if !defined(OS_ANDROID) url.host_piece() == kWalletPanelHost || url.host_piece() == kWalletPageHost || diff --git a/browser/ui/webui/sidebar/BUILD.gn b/browser/ui/webui/sidebar/BUILD.gn new file mode 100644 index 000000000000..117cebd22a03 --- /dev/null +++ b/browser/ui/webui/sidebar/BUILD.gn @@ -0,0 +1,17 @@ +# Copyright (c) 2022 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +import("//mojo/public/tools/bindings/mojom.gni") + +mojom("mojo_bindings") { + sources = [ "sidebar.mojom" ] + webui_module_path = "/" + public_deps = [ + "//mojo/public/mojom/base", + "//ui/base/mojom", + "//ui/gfx/geometry/mojom", + "//url/mojom:url_mojom_gurl", + ] +} diff --git a/browser/ui/webui/sidebar/sidebar.mojom b/browser/ui/webui/sidebar/sidebar.mojom new file mode 100644 index 000000000000..cff4cd6423ff --- /dev/null +++ b/browser/ui/webui/sidebar/sidebar.mojom @@ -0,0 +1,30 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at http://mozilla.org/MPL/2.0/. + +module sidebar.mojom; + +import "ui/base/mojom/window_open_disposition.mojom"; +import "ui/gfx/geometry/mojom/geometry.mojom"; +import "url/mojom/url.mojom"; + +// Used by the sidebar's bookmarks WebUI page (for the side panel) to bootstrap +// bidirectional communication. +interface BookmarksPageHandlerFactory { + // The WebUI calls this method when the page is first initialized. + CreateBookmarksPageHandler(pending_receiver handler); +}; + +// Browser-side handler for requests from WebUI page. +interface BookmarksPageHandler { + // Opens a bookmark by URL and passes the parent folder depth for metrics + // collection. + OpenBookmark(url.mojom.Url url, int32 parent_folder_depth, + ui.mojom.ClickModifiers click_modifiers); + + // Opens a context menu for a bookmark node. The id parameter is internally + // an int64 but gets passed as a string from the chrome.bookmarks Extension + // API. + ShowContextMenu(string id, gfx.mojom.Point point); +}; diff --git a/browser/ui/webui/sidebar/sidebar_bookmarks_page_handler.cc b/browser/ui/webui/sidebar/sidebar_bookmarks_page_handler.cc new file mode 100644 index 000000000000..5b3d2babf01a --- /dev/null +++ b/browser/ui/webui/sidebar/sidebar_bookmarks_page_handler.cc @@ -0,0 +1,153 @@ +/* Copyright (c) 2022 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "brave/browser/ui/webui/sidebar/sidebar_bookmarks_page_handler.h" + +#include +#include + +#include "base/memory/weak_ptr.h" +#include "base/strings/string_number_conversions.h" +#include "brave/browser/ui/brave_browser.h" +#include "brave/browser/ui/sidebar/sidebar.h" +#include "brave/browser/ui/sidebar/sidebar_controller.h" +#include "chrome/app/chrome_command_ids.h" +#include "chrome/browser/bookmarks/bookmark_model_factory.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/bookmarks/bookmark_context_menu_controller.h" +#include "chrome/browser/ui/bookmarks/bookmark_stats.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_window.h" +#include "components/bookmarks/browser/bookmark_model.h" +#include "components/bookmarks/browser/bookmark_node.h" +#include "components/bookmarks/browser/bookmark_utils.h" +#include "ui/base/models/simple_menu_model.h" +#include "ui/base/mojom/window_open_disposition.mojom.h" +#include "ui/base/window_open_disposition.h" +#include "url/gurl.h" + +namespace { + +class BookmarkContextMenu : public ui::SimpleMenuModel, + public ui::SimpleMenuModel::Delegate, + public BookmarkContextMenuControllerDelegate { + public: + explicit BookmarkContextMenu(Browser* browser, + sidebar::Sidebar* sidebar, + const bookmarks::BookmarkNode* bookmark) + : ui::SimpleMenuModel(this), + controller_(base::WrapUnique(new BookmarkContextMenuController( + browser->window()->GetNativeWindow(), + this, + browser, + browser->profile(), + base::BindRepeating( + [](content::PageNavigator* navigator) { return navigator; }, + browser), + // Do we need our own histogram enum? + BOOKMARK_LAUNCH_LOCATION_SIDE_PANEL_CONTEXT_MENU, + bookmark->parent(), + {bookmark}))), + sidebar_(sidebar) { + AddItem(IDC_BOOKMARK_BAR_OPEN_ALL); + AddItem(IDC_BOOKMARK_BAR_OPEN_ALL_NEW_WINDOW); + AddItem(IDC_BOOKMARK_BAR_OPEN_ALL_INCOGNITO); + AddSeparator(ui::NORMAL_SEPARATOR); + + AddItem(bookmark->is_folder() ? IDC_BOOKMARK_BAR_RENAME_FOLDER + : IDC_BOOKMARK_BAR_EDIT); + AddSeparator(ui::NORMAL_SEPARATOR); + + AddItem(IDC_CUT); + AddItem(IDC_COPY); + AddItem(IDC_PASTE); + AddSeparator(ui::NORMAL_SEPARATOR); + + AddItem(IDC_BOOKMARK_BAR_REMOVE); + AddSeparator(ui::NORMAL_SEPARATOR); + + AddItem(IDC_BOOKMARK_BAR_ADD_NEW_BOOKMARK); + AddItem(IDC_BOOKMARK_BAR_NEW_FOLDER); + AddSeparator(ui::NORMAL_SEPARATOR); + + AddItem(IDC_BOOKMARK_MANAGER); + } + ~BookmarkContextMenu() override = default; + + void ExecuteCommand(int command_id, int event_flags) override { + controller_->ExecuteCommand(command_id, event_flags); + } + + bool IsCommandIdEnabled(int command_id) const override { + return controller_->IsCommandIdEnabled(command_id); + } + + bool IsCommandIdVisible(int command_id) const override { + return controller_->IsCommandIdVisible(command_id); + } + + // BookmarkContextMenuControllerDelegate: + void CloseMenu() override { sidebar_->HideCustomContextMenu(); } + + private: + void AddItem(int command_id) { + ui::SimpleMenuModel::AddItem( + command_id, + controller_->menu_model()->GetLabelAt( + controller_->menu_model()->GetIndexOfCommandId(command_id))); + } + + std::unique_ptr controller_; + sidebar::Sidebar* sidebar_ = nullptr; +}; + +} // namespace + +SidebarBookmarksPageHandler::SidebarBookmarksPageHandler( + mojo::PendingReceiver receiver) + : receiver_(this, std::move(receiver)) {} + +SidebarBookmarksPageHandler::~SidebarBookmarksPageHandler() = default; + +void SidebarBookmarksPageHandler::OpenBookmark( + const GURL& url, + int32_t parent_folder_depth, + ui::mojom::ClickModifiersPtr click_modifiers) { + Browser* browser = chrome::FindLastActive(); + if (!browser) + return; + + WindowOpenDisposition open_location = ui::DispositionFromClick( + click_modifiers->middle_button, click_modifiers->alt_key, + click_modifiers->ctrl_key, click_modifiers->meta_key, + click_modifiers->shift_key); + content::OpenURLParams params(url, content::Referrer(), open_location, + ui::PAGE_TRANSITION_AUTO_BOOKMARK, false); + browser->OpenURL(params); +} + +void SidebarBookmarksPageHandler::ShowContextMenu(const std::string& id_string, + const gfx::Point& point) { + int64_t id; + if (!base::StringToInt64(id_string, &id)) + return; + + Browser* browser = chrome::FindLastActive(); + if (!browser) + return; + + bookmarks::BookmarkModel* bookmark_model = + BookmarkModelFactory::GetForBrowserContext(browser->profile()); + const bookmarks::BookmarkNode* bookmark = + bookmarks::GetBookmarkNodeByID(bookmark_model, id); + if (!bookmark) + return; + + auto* sidebar = + static_cast(browser)->sidebar_controller()->sidebar(); + sidebar->ShowCustomContextMenu( + point, std::make_unique(browser, sidebar, bookmark)); +} diff --git a/browser/ui/webui/sidebar/sidebar_bookmarks_page_handler.h b/browser/ui/webui/sidebar/sidebar_bookmarks_page_handler.h new file mode 100644 index 000000000000..0acd51ef2cab --- /dev/null +++ b/browser/ui/webui/sidebar/sidebar_bookmarks_page_handler.h @@ -0,0 +1,37 @@ +/* Copyright (c) 2022 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_BROWSER_UI_WEBUI_SIDEBAR_SIDEBAR_BOOKMARKS_PAGE_HANDLER_H_ +#define BRAVE_BROWSER_UI_WEBUI_SIDEBAR_SIDEBAR_BOOKMARKS_PAGE_HANDLER_H_ + +#include + +#include "brave/browser/ui/webui/sidebar/sidebar.mojom.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/receiver.h" + +class GURL; + +class SidebarBookmarksPageHandler + : public sidebar::mojom::BookmarksPageHandler { + public: + explicit SidebarBookmarksPageHandler( + mojo::PendingReceiver receiver); + ~SidebarBookmarksPageHandler() override; + SidebarBookmarksPageHandler(const SidebarBookmarksPageHandler&) = delete; + SidebarBookmarksPageHandler& operator=(const SidebarBookmarksPageHandler&) = + delete; + + private: + // sidebar::mojom::BookmarksPageHandler: + void OpenBookmark(const GURL& url, + int32_t parent_folder_depth, + ui::mojom::ClickModifiersPtr click_modifiers) override; + void ShowContextMenu(const std::string& id, const gfx::Point& point) override; + + mojo::Receiver receiver_; +}; + +#endif // BRAVE_BROWSER_UI_WEBUI_SIDEBAR_SIDEBAR_BOOKMARKS_PAGE_HANDLER_H_ diff --git a/browser/ui/webui/sidebar/sidebar_bookmarks_ui.cc b/browser/ui/webui/sidebar/sidebar_bookmarks_ui.cc new file mode 100644 index 000000000000..5b499f59688f --- /dev/null +++ b/browser/ui/webui/sidebar/sidebar_bookmarks_ui.cc @@ -0,0 +1,70 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +#include "brave/browser/ui/webui/sidebar/sidebar_bookmarks_ui.h" + +#include +#include + +#include "brave/browser/resources/sidebar/grit/sidebar_resources.h" +#include "brave/browser/resources/sidebar/grit/sidebar_resources_map.h" +#include "brave/browser/ui/webui/sidebar/sidebar_bookmarks_page_handler.h" +#include "brave/common/webui_url_constants.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/webui/favicon_source.h" +#include "chrome/browser/ui/webui/webui_util.h" +#include "chrome/grit/generated_resources.h" +#include "components/bookmarks/common/bookmark_pref_names.h" +#include "components/favicon_base/favicon_url_parser.h" +#include "components/prefs/pref_service.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_ui.h" +#include "content/public/browser/web_ui_data_source.h" +#include "ui/base/l10n/l10n_util.h" + +SidebarBookmarksUI::SidebarBookmarksUI(content::WebUI* web_ui) + : ui::MojoWebUIController(web_ui) { + content::WebUIDataSource* source = + content::WebUIDataSource::Create(kSidebarBookmarksHost); + static constexpr webui::LocalizedString kLocalizedStrings[] = { + {"bookmarksTitle", IDS_BOOKMARK_MANAGER_TITLE}, + }; + for (const auto& str : kLocalizedStrings) { + std::u16string l10n_str = l10n_util::GetStringUTF16(str.id); + source->AddString(str.name, l10n_str); + } + + Profile* const profile = Profile::FromWebUI(web_ui); + PrefService* prefs = profile->GetPrefs(); + source->AddBoolean( + "bookmarksDragAndDropEnabled", + prefs->GetBoolean(bookmarks::prefs::kEditBookmarksEnabled)); + + content::URLDataSource::Add( + profile, std::make_unique( + profile, chrome::FaviconUrlFormat::kFavicon2)); + webui::SetupWebUIDataSource( + source, base::make_span(kSidebarResources, kSidebarResourcesSize), + IDR_SIDEBAR_BOOKMARKS_BOOKMARKS_HTML); + content::WebUIDataSource::Add(web_ui->GetWebContents()->GetBrowserContext(), + source); +} + +SidebarBookmarksUI::~SidebarBookmarksUI() = default; + +WEB_UI_CONTROLLER_TYPE_IMPL(SidebarBookmarksUI) + +void SidebarBookmarksUI::BindInterface( + mojo::PendingReceiver + receiver) { + bookmarks_page_factory_receiver_.reset(); + bookmarks_page_factory_receiver_.Bind(std::move(receiver)); +} + +void SidebarBookmarksUI::CreateBookmarksPageHandler( + mojo::PendingReceiver receiver) { + bookmarks_page_handler_ = + std::make_unique(std::move(receiver)); +} diff --git a/browser/ui/webui/sidebar/sidebar_bookmarks_ui.h b/browser/ui/webui/sidebar/sidebar_bookmarks_ui.h new file mode 100644 index 000000000000..95b931c3b37e --- /dev/null +++ b/browser/ui/webui/sidebar/sidebar_bookmarks_ui.h @@ -0,0 +1,44 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_UI_WEBUI_SIDEBAR_SIDEBAR_BOOKMARKS_UI_H_ +#define BRAVE_BROWSER_UI_WEBUI_SIDEBAR_SIDEBAR_BOOKMARKS_UI_H_ + +#include + +#include "brave/browser/ui/webui/sidebar/sidebar.mojom.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "ui/webui/mojo_web_ui_controller.h" + +class SidebarBookmarksPageHandler; + +class SidebarBookmarksUI : public ui::MojoWebUIController, + public sidebar::mojom::BookmarksPageHandlerFactory { + public: + explicit SidebarBookmarksUI(content::WebUI* web_ui); + ~SidebarBookmarksUI() override; + SidebarBookmarksUI(const SidebarBookmarksUI&) = delete; + SidebarBookmarksUI& operator=(const SidebarBookmarksUI&) = delete; + + void BindInterface( + mojo::PendingReceiver + receiver); + + private: + // sidebar::mojom::BookmarksPageHandlerFactory + void CreateBookmarksPageHandler( + mojo::PendingReceiver receiver) + override; + + std::unique_ptr bookmarks_page_handler_; + mojo::Receiver + bookmarks_page_factory_receiver_{this}; + + WEB_UI_CONTROLLER_TYPE_DECL(); +}; + +#endif // BRAVE_BROWSER_UI_WEBUI_SIDEBAR_SIDEBAR_BOOKMARKS_UI_H_ diff --git a/chromium_src/python_modules/tools/__init__.py b/chromium_src/python_modules/tools/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/chromium_src/python_modules/tools/json_schema_compiler/__init__.py b/chromium_src/python_modules/tools/json_schema_compiler/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/chromium_src/python_modules/tools/json_schema_compiler/feature_compiler_helper.py b/chromium_src/python_modules/tools/json_schema_compiler/feature_compiler_helper.py new file mode 100644 index 000000000000..ce7da7bbe1d1 --- /dev/null +++ b/chromium_src/python_modules/tools/json_schema_compiler/feature_compiler_helper.py @@ -0,0 +1,20 @@ +# Copyright (c) 2022 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +# When update this method, feature_compiler.py should be touched to make +# feature_compiler.py run. json_feature target doesn't have this in its +# dependency. +# below discard list comes from common/extensions/api/_api_features.json. +def DiscardBraveOverridesFromDupes(dupes): + dupes.discard('topSites') + dupes.discard('extension.inIncognitoContext') + dupes.discard('bookmarkManagerPrivate') + dupes.discard('bookmarks') + dupes.discard('settingsPrivate') + dupes.discard('sockets') + dupes.discard('sockets.tcp') + dupes.discard('sockets.udp') + dupes.discard('sockets.tcpServer') + dupes.discard('tabs') diff --git a/common/extensions/api/_api_features.json b/common/extensions/api/_api_features.json index a289f97ee899..539ea3d8df5e 100644 --- a/common/extensions/api/_api_features.json +++ b/common/extensions/api/_api_features.json @@ -26,6 +26,19 @@ "chrome://newtab/*" ] }], + "bookmarkManagerPrivate": [{ + "dependencies": ["permission:bookmarkManagerPrivate"], + "contexts": ["blessed_extension"], + "default_parent": true + }, { + "channel": "stable", + "contexts": ["webui"], + "matches": [ + "chrome://bookmarks/*", + "chrome://read-later.top-chrome/*", + "chrome://sidebar-bookmarks.top-chrome/*" + ] + }], "bookmarks": [{ "dependencies": ["permission:bookmarks"], "contexts": ["blessed_extension"], @@ -35,7 +48,8 @@ "contexts": ["webui"], "matches": [ "chrome://bookmarks/*", - "chrome://read-later.top-chrome/*" + "chrome://read-later.top-chrome/*", + "chrome://sidebar-bookmarks.top-chrome/*" ], "default_parent": true }, { diff --git a/common/webui_url_constants.cc b/common/webui_url_constants.cc index 5e0c92315323..330be9692037 100644 --- a/common/webui_url_constants.cc +++ b/common/webui_url_constants.cc @@ -37,3 +37,4 @@ const char kUntrustedTrezorHost[] = "trezor-bridge"; const char kUntrustedTrezorURL[] = "chrome-untrusted://trezor-bridge/"; const char kShieldsPanelURL[] = "chrome://brave-shields.top-chrome"; const char kShieldsPanelHost[] = "brave-shields.top-chrome"; +const char kSidebarBookmarksHost[] = "sidebar-bookmarks.top-chrome"; diff --git a/common/webui_url_constants.h b/common/webui_url_constants.h index 89222610143c..94d6133edbc3 100644 --- a/common/webui_url_constants.h +++ b/common/webui_url_constants.h @@ -39,5 +39,6 @@ extern const char kUntrustedTrezorHost[]; extern const char kUntrustedTrezorURL[]; extern const char kShieldsPanelURL[]; extern const char kShieldsPanelHost[]; +extern const char kSidebarBookmarksHost[]; #endif // BRAVE_COMMON_WEBUI_URL_CONSTANTS_H_ diff --git a/components/sidebar/constants.h b/components/sidebar/constants.h index 8528199d14bd..9065a5d2108e 100644 --- a/components/sidebar/constants.h +++ b/components/sidebar/constants.h @@ -14,6 +14,11 @@ constexpr char kSidebarItemBuiltInItemTypeKey[] = "built_in_item_type"; constexpr char kSidebarItemTitleKey[] = "title"; constexpr char kSidebarItemOpenInPanelKey[] = "open_in_panel"; +// TODO(simonhong): Move this to //brave/common/webui_url_constants.h when +// default builtin items list is provided from chrome layer. +constexpr char kSidebarBookmarksURL[] = + "chrome://sidebar-bookmarks.top-chrome/"; + } // namespace sidebar #endif // BRAVE_COMPONENTS_SIDEBAR_CONSTANTS_H_ diff --git a/components/sidebar/sidebar_service.cc b/components/sidebar/sidebar_service.cc index 3e6aff629caf..ab9d54c05c03 100644 --- a/components/sidebar/sidebar_service.cc +++ b/components/sidebar/sidebar_service.cc @@ -42,7 +42,7 @@ SidebarItem GetBuiltInItemForType(SidebarItem::BuiltInItemType type) { break; case SidebarItem::BuiltInItemType::kBookmarks: return SidebarItem::Create( - GURL("chrome://bookmarks/"), + GURL(kSidebarBookmarksURL), l10n_util::GetStringUTF16(IDS_SIDEBAR_BOOKMARKS_ITEM_TITLE), SidebarItem::Type::kTypeBuiltIn, SidebarItem::BuiltInItemType::kBookmarks, true); @@ -67,7 +67,7 @@ SidebarItem::BuiltInItemType GetBuiltInItemTypeForURL(const std::string& url) { if (url == "chrome://wallet/") return SidebarItem::BuiltInItemType::kWallet; - if (url == "chrome://bookmarks/") + if (url == kSidebarBookmarksURL || url == "chrome://bookmarks/") return SidebarItem::BuiltInItemType::kBookmarks; if (url == "chrome://history/") diff --git a/patches/tools-json_schema_compiler-feature_compiler.py.patch b/patches/tools-json_schema_compiler-feature_compiler.py.patch index f0adb529c30a..186782059546 100644 --- a/patches/tools-json_schema_compiler-feature_compiler.py.patch +++ b/patches/tools-json_schema_compiler-feature_compiler.py.patch @@ -1,20 +1,12 @@ diff --git a/tools/json_schema_compiler/feature_compiler.py b/tools/json_schema_compiler/feature_compiler.py -index b23dca5689042e1e6f0742ea79c3dcfcc65d168c..2c11ef5d4124c65d23cd5566cd886299fd5b5e05 100644 +index b23dca5689042e1e6f0742ea79c3dcfcc65d168c..026b32fa299ddf69f91fe5fb1020239d1b5184b2 100644 --- a/tools/json_schema_compiler/feature_compiler.py +++ b/tools/json_schema_compiler/feature_compiler.py -@@ -771,6 +771,15 @@ class FeatureCompiler(object): +@@ -771,6 +771,7 @@ class FeatureCompiler(object): abs_source_file) raise dupes = set(f_json) & set(self._json) -+ dupes.discard('topSites') -+ dupes.discard('extension.inIncognitoContext') -+ dupes.discard('bookmarks') -+ dupes.discard('settingsPrivate') -+ dupes.discard('sockets') -+ dupes.discard('sockets.tcp') -+ dupes.discard('sockets.udp') -+ dupes.discard('sockets.tcpServer') -+ dupes.discard('tabs') ++ from tools.json_schema_compiler import feature_compiler_helper; feature_compiler_helper.DiscardBraveOverridesFromDupes(dupes) assert not dupes, 'Duplicate keys found: %s' % list(dupes) self._json.update(f_json)