From 86f1dc0c399ea80108f2d202ddbdc4d4bdff2adc Mon Sep 17 00:00:00 2001 From: Simon Hong Date: Wed, 19 Jan 2022 17:34:27 +0900 Subject: [PATCH] Introduce sidebar bookmarks page fix https://github.com/brave/brave-browser/issues/20500 Applied bookmark part of chromium's side panel to sidebar bookmark panel. sidebar.mojom is used for interacting between sidebar's bookmarks panel with SidebarBookmarksPageHandler. Most of webui impls in browser/resources/sidebar/bookmarks are copied from upstream and modified to show bookmark part only. (side panel's page shows read later and bookmark part both.) Bookmarks page uses custom context menu by asking to native via Sidebar:ShowCustomContextMenu interface. --- app/theme/brave_theme_resources.grd | 4 + .../brave_bookmark_folder_open-lin-dark.png | Bin 0 -> 680 bytes .../brave_bookmark_folder_open-lin-light.png | Bin 0 -> 650 bytes .../brave_bookmark_folder_open-win-dark.png | Bin 0 -> 623 bytes .../brave_bookmark_folder_open-win-light.png | Bin 0 -> 623 bytes .../brave_bookmark_folder_open-lin-dark.png | Bin 0 -> 1657 bytes .../brave_bookmark_folder_open-lin-light.png | Bin 0 -> 1535 bytes .../brave_bookmark_folder_open-win-dark.png | Bin 0 -> 1476 bytes .../brave_bookmark_folder_open-win-light.png | Bin 0 -> 1476 bytes brave_paks.gni | 8 + browser/brave_content_browser_client.cc | 14 + browser/resources/resource_ids | 5 +- browser/resources/sidebar/BUILD.gn | 34 ++ browser/resources/sidebar/bookmarks/BUILD.gn | 85 +++++ .../sidebar/bookmarks/bookmark_folder.html | 239 +++++++++++++ .../sidebar/bookmarks/bookmark_folder.ts | 232 +++++++++++++ .../sidebar/bookmarks/bookmarks.html | 34 ++ .../sidebar/bookmarks/bookmarks_api_proxy.ts | 78 +++++ .../bookmarks/bookmarks_drag_manager.ts | 266 +++++++++++++++ .../sidebar/bookmarks/bookmarks_list.html | 4 + .../sidebar/bookmarks/bookmarks_list.ts | 314 ++++++++++++++++++ .../sidebar/bookmarks/tsconfig_base.json | 9 + browser/ui/BUILD.gn | 12 + browser/ui/sidebar/sidebar.h | 14 + .../views/sidebar/sidebar_container_view.cc | 32 ++ .../ui/views/sidebar/sidebar_container_view.h | 14 + .../webui/brave_web_ui_controller_factory.cc | 16 + browser/ui/webui/sidebar/BUILD.gn | 17 + browser/ui/webui/sidebar/sidebar.mojom | 30 ++ .../sidebar/sidebar_bookmarks_page_handler.cc | 153 +++++++++ .../sidebar/sidebar_bookmarks_page_handler.h | 37 +++ .../ui/webui/sidebar/sidebar_bookmarks_ui.cc | 70 ++++ .../ui/webui/sidebar/sidebar_bookmarks_ui.h | 44 +++ chromium_src/python_modules/tools/__init__.py | 0 .../tools/json_schema_compiler/__init__.py | 0 .../feature_compiler_helper.py | 20 ++ common/extensions/api/_api_features.json | 16 +- common/webui_url_constants.cc | 1 + common/webui_url_constants.h | 1 + components/sidebar/constants.h | 5 + components/sidebar/sidebar_service.cc | 4 +- ..._schema_compiler-feature_compiler.py.patch | 14 +- 42 files changed, 1811 insertions(+), 15 deletions(-) create mode 100644 app/theme/default_100_percent/common/brave_bookmark_folder_open-lin-dark.png create mode 100644 app/theme/default_100_percent/common/brave_bookmark_folder_open-lin-light.png create mode 100644 app/theme/default_100_percent/common/brave_bookmark_folder_open-win-dark.png create mode 100644 app/theme/default_100_percent/common/brave_bookmark_folder_open-win-light.png create mode 100644 app/theme/default_200_percent/common/brave_bookmark_folder_open-lin-dark.png create mode 100644 app/theme/default_200_percent/common/brave_bookmark_folder_open-lin-light.png create mode 100644 app/theme/default_200_percent/common/brave_bookmark_folder_open-win-dark.png create mode 100644 app/theme/default_200_percent/common/brave_bookmark_folder_open-win-light.png create mode 100644 browser/resources/sidebar/BUILD.gn create mode 100644 browser/resources/sidebar/bookmarks/BUILD.gn create mode 100644 browser/resources/sidebar/bookmarks/bookmark_folder.html create mode 100644 browser/resources/sidebar/bookmarks/bookmark_folder.ts create mode 100644 browser/resources/sidebar/bookmarks/bookmarks.html create mode 100644 browser/resources/sidebar/bookmarks/bookmarks_api_proxy.ts create mode 100644 browser/resources/sidebar/bookmarks/bookmarks_drag_manager.ts create mode 100644 browser/resources/sidebar/bookmarks/bookmarks_list.html create mode 100644 browser/resources/sidebar/bookmarks/bookmarks_list.ts create mode 100644 browser/resources/sidebar/bookmarks/tsconfig_base.json create mode 100644 browser/ui/webui/sidebar/BUILD.gn create mode 100644 browser/ui/webui/sidebar/sidebar.mojom create mode 100644 browser/ui/webui/sidebar/sidebar_bookmarks_page_handler.cc create mode 100644 browser/ui/webui/sidebar/sidebar_bookmarks_page_handler.h create mode 100644 browser/ui/webui/sidebar/sidebar_bookmarks_ui.cc create mode 100644 browser/ui/webui/sidebar/sidebar_bookmarks_ui.h create mode 100644 chromium_src/python_modules/tools/__init__.py create mode 100644 chromium_src/python_modules/tools/json_schema_compiler/__init__.py create mode 100644 chromium_src/python_modules/tools/json_schema_compiler/feature_compiler_helper.py 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 0000000000000000000000000000000000000000..bb9048be9ffe0ba753e6032d019765602783f30e GIT binary patch literal 680 zcmV;Z0$2TsP)Q{*1A>hFekdt3Rq3As~k3~T|NQGj& ziO)v!x-Yvs>&(7Cc?JSKh|61M*xmW9610RGVsOLlp9xG2zIFxVE(y%tj5_1=RJ zgh`hC)8AU(AB{#Q7XeadN%CN=(+MCYwSq=0y?;*jF-=?%?Eg)q2THO0ZR< zzR$j~_QjPrel{EqQKeZ1ZCc#e>_WVE1KO=9FbWdce3;R zmrvx>8H}ybf25$f+2hfM&#xw0k8%Za)p&VKEs8Y&GdV|Sw>L=z1~h<0nQ5+f#uDWZ^Paq z={5i*$@dQd==QBUBunKT?Xe16RLfS(>Xox3H_P<5@p$|>Jdxe2*J4IyMogV`y8?iH zME7?yG83{?El9Qg#Q6z8UrtYsuM-_kcEti+C9MR-n$I&Mpy|)Ew}fHKhH3Wu{d-&6 zy*+1KP)KPMXpkQV{TR55Y0Y7Hqq%e^20N>#C(dstlgUqq7?Cf;BoAZ~sA7sz zEGU5(pg4vo3|DxZGh)Zt+~1SYjtno$B_6#l`K#5o)e&}y|5P@ajd zEG>x4B+Bd^$%^9y_BS`*$pwdGbvm8XJ5_%`Zx0!{P8Afc8cE zqpw|NEg)4N3B`u|iGt~Lsy-ps?RHDp2a1v7xR@ZUHv&B{*PNd*viXnw7X-Jz$!G;M zo6UZVMx$%&qZWbXJs~EVsRlQDQ~-;ISL=UY$!G#1W5}o1Nc9cTrOw7k`}P+?duGs? kYy^YB;A*v6ad?*F4)!4BQDM?>7XSbN07*qoM6N<$f|oHG{{R30 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4e38c4ea0919a0157adaab772c0f06e18fca2c5b GIT binary patch literal 623 zcmV-#0+9WQP)-+2Bt^jfC?+q#CLI1&*lk*7F{MB&R z7<|rT$S*;h6co< zYK&V+-WUh#!cGrf&VER3ByL?zp-l>ej9_k7Je7<|$a5YVNh+*$uv*(GcUKxz(9gP5 zdlCo1S2b9KQ#T|&dUOuT!jKAo7_PB*eMcmG4b&jdUFJHgSUc8bG_fOsb#7s?(C@+^ zrCA0)Pc$g%u1Y+e>Ncm^01&kT8@$#AzAiL8c~Kk%f{(|)-u_;edxL%}QxZ3TC_Sh- zi+%iFko@D>`ssK)Hj~LDG@Wfg)@#P68R&W>npKj0BQtOr>klY!NdDWHOhW(w002ov JPDHLkV1m8Q2G{@q literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4e38c4ea0919a0157adaab772c0f06e18fca2c5b GIT binary patch literal 623 zcmV-#0+9WQP)-+2Bt^jfC?+q#CLI1&*lk*7F{MB&R z7<|rT$S*;h6co< zYK&V+-WUh#!cGrf&VER3ByL?zp-l>ej9_k7Je7<|$a5YVNh+*$uv*(GcUKxz(9gP5 zdlCo1S2b9KQ#T|&dUOuT!jKAo7_PB*eMcmG4b&jdUFJHgSUc8bG_fOsb#7s?(C@+^ zrCA0)Pc$g%u1Y+e>Ncm^01&kT8@$#AzAiL8c~Kk%f{(|)-u_;edxL%}QxZ3TC_Sh- zi+%iFko@D>`ssK)Hj~LDG@Wfg)@#P68R&W>npKj0BQtOr>klY!NdDWHOhW(w002ov JPDHLkV1m8Q2G{@q literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..63c72f0453bc4a7dbec6bc9340a0081dd4dbee0a GIT binary patch literal 1657 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 + } +#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)