summaryrefslogtreecommitdiffstats
path: root/chromium/chrome/browser/resources/bluetooth_internals
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/chrome/browser/resources/bluetooth_internals')
-rw-r--r--chromium/chrome/browser/resources/bluetooth_internals/BUILD.gn24
-rw-r--r--chromium/chrome/browser/resources/bluetooth_internals/adapter_broker.js7
-rw-r--r--chromium/chrome/browser/resources/bluetooth_internals/bluetooth_internals.html13
-rw-r--r--chromium/chrome/browser/resources/bluetooth_internals/bluetooth_internals.js8
-rw-r--r--chromium/chrome/browser/resources/bluetooth_internals/debug_log_page.js68
-rw-r--r--chromium/chrome/browser/resources/bluetooth_internals/page.html1
-rw-r--r--chromium/chrome/browser/resources/bluetooth_internals/page.js314
-rw-r--r--chromium/chrome/browser/resources/bluetooth_internals/page_manager.html1
-rw-r--r--chromium/chrome/browser/resources/bluetooth_internals/page_manager.js800
-rw-r--r--chromium/chrome/browser/resources/bluetooth_internals/resources.grd21
10 files changed, 1251 insertions, 6 deletions
diff --git a/chromium/chrome/browser/resources/bluetooth_internals/BUILD.gn b/chromium/chrome/browser/resources/bluetooth_internals/BUILD.gn
index 7ab5c808a39..d53ec0f8316 100644
--- a/chromium/chrome/browser/resources/bluetooth_internals/BUILD.gn
+++ b/chromium/chrome/browser/resources/bluetooth_internals/BUILD.gn
@@ -17,6 +17,7 @@ js_library("bluetooth_internals") {
"adapter_page.js",
"bluetooth_internals.js",
"characteristic_list.js",
+ "debug_log_page.js",
"descriptor_list.js",
"device_broker.js",
"device_collection.js",
@@ -32,14 +33,33 @@ js_library("bluetooth_internals") {
]
deps = [
+ ":page",
+ ":page_manager",
"//chrome/browser/ui/webui/bluetooth_internals:mojo_bindings_js_library_for_compile",
"//ui/webui/resources/js:cr",
"//ui/webui/resources/js:util",
"//ui/webui/resources/js/cr/ui:array_data_model",
"//ui/webui/resources/js/cr/ui:list",
"//ui/webui/resources/js/cr/ui:list_item",
- "//ui/webui/resources/js/cr/ui/page_manager:page",
- "//ui/webui/resources/js/cr/ui/page_manager:page_manager",
+ ]
+}
+
+js_library("page") {
+ deps = [
+ "//ui/webui/resources/js:cr",
+ "//ui/webui/resources/js:util",
+ "//ui/webui/resources/js/cr:event_target",
+ "//ui/webui/resources/js/cr/ui:bubble",
+ "//ui/webui/resources/js/cr/ui:focus_outline_manager",
+ "//ui/webui/resources/js/cr/ui:node_utils",
+ "//ui/webui/resources/js/cr/ui:overlay",
+ ]
+}
+
+js_library("page_manager") {
+ deps = [
+ ":page",
+ "//ui/webui/resources/js:cr",
]
}
diff --git a/chromium/chrome/browser/resources/bluetooth_internals/adapter_broker.js b/chromium/chrome/browser/resources/bluetooth_internals/adapter_broker.js
index 0da84a773fe..1ca5ea685c5 100644
--- a/chromium/chrome/browser/resources/bluetooth_internals/adapter_broker.js
+++ b/chromium/chrome/browser/resources/bluetooth_internals/adapter_broker.js
@@ -154,15 +154,18 @@ cr.define('adapter_broker', function() {
/**
* Initializes an AdapterBroker if one doesn't exist.
+ * @param {!mojom.BluetoothInternalsHandlerRemote=}
+ * opt_bluetoothInternalsHandler
* @return {!Promise<!adapter_broker.AdapterBroker>} resolves with
* AdapterBroker, rejects if Bluetooth is not supported.
*/
- function getAdapterBroker() {
+ function getAdapterBroker(opt_bluetoothInternalsHandler) {
if (adapterBroker) {
return Promise.resolve(adapterBroker);
}
- const bluetoothInternalsHandler =
+ const bluetoothInternalsHandler = opt_bluetoothInternalsHandler ?
+ opt_bluetoothInternalsHandler :
mojom.BluetoothInternalsHandler.getRemote();
// Get an Adapter service.
diff --git a/chromium/chrome/browser/resources/bluetooth_internals/bluetooth_internals.html b/chromium/chrome/browser/resources/bluetooth_internals/bluetooth_internals.html
index fdd34a71f54..1e4063113f5 100644
--- a/chromium/chrome/browser/resources/bluetooth_internals/bluetooth_internals.html
+++ b/chromium/chrome/browser/resources/bluetooth_internals/bluetooth_internals.html
@@ -20,8 +20,10 @@
<link rel="import" href="chrome://resources/html/cr/ui/list_item.html">
<link rel="import" href="chrome://resources/html/cr/ui/list.html">
<link rel="import" href="chrome://resources/html/cr/ui/overlay.html">
- <link rel="import" href="chrome://resources/html/cr/ui/page_manager/page_manager.html">
- <link rel="import" href="chrome://resources/html/cr/ui/page_manager/page.html">
+
+ <link rel="import" href="page_manager.html">
+ <link rel="import" href="page.html">
+
<link rel="import" href="chrome://resources/html/util.html">
<link rel="import" href="chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.html">
@@ -39,6 +41,7 @@
<script src="service_list.js"></script>
<script src="descriptor_list.js"></script>
<script src="adapter_page.js"></script>
+ <script src="debug_log_page.js"></script>
<script src="device_collection.js"></script>
<script src="device_details_page.js"></script>
<script src="device_table.js"></script>
@@ -63,6 +66,9 @@
<button id="scan-btn">Start Scan</button>
</div>
</section>
+ <section id="debug" hidden>
+ <div class="header-extras" id="debug-container"></div>
+ </section>
</div>
<div id="snackbar-container"></div>
<aside id="sidebar">
@@ -79,6 +85,9 @@
<li data-page-name="devices">
<button class="custom-appearance">Devices</button>
</li>
+ <li data-page-name="debug" data-page-name="debug">
+ <button class="custom-appearance">Debug Logs</button>
+ </li>
</ul>
</nav>
</section>
diff --git a/chromium/chrome/browser/resources/bluetooth_internals/bluetooth_internals.js b/chromium/chrome/browser/resources/bluetooth_internals/bluetooth_internals.js
index de4b6d0a4d9..e146a8e1746 100644
--- a/chromium/chrome/browser/resources/bluetooth_internals/bluetooth_internals.js
+++ b/chromium/chrome/browser/resources/bluetooth_internals/bluetooth_internals.js
@@ -19,6 +19,7 @@ cr.define('bluetooth_internals', function() {
const AdapterPage = adapter_page.AdapterPage;
const DeviceDetailsPage = device_details_page.DeviceDetailsPage;
const DevicesPage = devices_page.DevicesPage;
+ const DebugLogPage = debug_log_page.DebugLogPage;
const PageManager = cr.ui.pageManager.PageManager;
const Snackbar = snackbar.Snackbar;
const SnackbarType = snackbar.SnackbarType;
@@ -29,6 +30,8 @@ cr.define('bluetooth_internals', function() {
let adapterPage = null;
/** @type {devices_page.DevicesPage} */
let devicesPage = null;
+ /** @type {debug_log_page.DebugLogPage} */
+ let debugLogPage = null;
/** @type {bluetooth.mojom.DiscoverySessionRemote} */
let discoverySession = null;
@@ -36,6 +39,9 @@ cr.define('bluetooth_internals', function() {
/** @type {boolean} */
let userRequestedScanStop = false;
+ /** @type {!mojom.BluetoothInternalsHandlerRemote} */
+ const bluetoothInternalsHandler = mojom.BluetoothInternalsHandler.getRemote();
+
/**
* Observer for page changes. Used to update page title header.
* @constructor
@@ -258,6 +264,8 @@ cr.define('bluetooth_internals', function() {
PageManager.register(devicesPage);
adapterPage = new AdapterPage();
PageManager.register(adapterPage);
+ debugLogPage = new DebugLogPage(bluetoothInternalsHandler);
+ PageManager.register(debugLogPage);
// Set up hash-based navigation.
window.addEventListener('hashchange', function() {
diff --git a/chromium/chrome/browser/resources/bluetooth_internals/debug_log_page.js b/chromium/chrome/browser/resources/bluetooth_internals/debug_log_page.js
new file mode 100644
index 00000000000..5c13a39a26e
--- /dev/null
+++ b/chromium/chrome/browser/resources/bluetooth_internals/debug_log_page.js
@@ -0,0 +1,68 @@
+// Copyright 2019 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.
+
+/**
+ * Javascript for DebugLogPage, served from chrome://bluetooth-internals/.
+ */
+cr.define('debug_log_page', function() {
+ /** @const {string} */
+ const LOGS_NOT_SUPPORTED_STRING = 'Debug logs not supported';
+
+ /**
+ * Page that allows user to enable/disable debug logs.
+ */
+ class DebugLogPage extends cr.ui.pageManager.Page {
+ /**
+ * @param {!mojom.BluetoothInternalsHandlerRemote} bluetoothInternalsHandler
+ */
+ constructor(bluetoothInternalsHandler) {
+ super('debug', 'Debug Logs', 'debug');
+
+ /**
+ * @private {?mojom.DebugLogsChangeHandlerRemote}
+ */
+ this.debugLogsChangeHandler_ = null;
+
+ /** @private {?HTMLInputElement} */
+ this.inputElement_ = null;
+
+ /** @private {!HTMLDivElement} */
+ this.debugContainer_ =
+ /** @type {!HTMLDivElement} */ ($('debug-container'));
+
+ bluetoothInternalsHandler.getDebugLogsChangeHandler().then((params) => {
+ if (params.handler) {
+ this.setUpInput(params.handler, params.initialToggleValue);
+ } else {
+ this.debugContainer_.textContent = LOGS_NOT_SUPPORTED_STRING;
+ }
+ });
+ }
+
+ /**
+ * @param {!mojom.DebugLogsChangeHandlerRemote} handler
+ * @param {boolean} initialInputValue
+ */
+ setUpInput(handler, initialInputValue) {
+ this.debugLogsChangeHandler_ = handler;
+
+ this.inputElement_ =
+ /** @type {!HTMLInputElement} */ (document.createElement('input'));
+ this.inputElement_.setAttribute('type', 'checkbox');
+ this.inputElement_.checked = initialInputValue;
+ this.inputElement_.addEventListener(
+ 'change', this.onToggleChange.bind(this));
+ this.debugContainer_.appendChild(this.inputElement_);
+ }
+
+ onToggleChange() {
+ this.debugLogsChangeHandler_.changeDebugLogsState(
+ this.inputElement_.checked);
+ }
+ }
+
+ return {
+ DebugLogPage: DebugLogPage,
+ };
+});
diff --git a/chromium/chrome/browser/resources/bluetooth_internals/page.html b/chromium/chrome/browser/resources/bluetooth_internals/page.html
new file mode 100644
index 00000000000..f6e2de85f42
--- /dev/null
+++ b/chromium/chrome/browser/resources/bluetooth_internals/page.html
@@ -0,0 +1 @@
+<script src="page.js"></script>
diff --git a/chromium/chrome/browser/resources/bluetooth_internals/page.js b/chromium/chrome/browser/resources/bluetooth_internals/page.js
new file mode 100644
index 00000000000..6c40a9ea35e
--- /dev/null
+++ b/chromium/chrome/browser/resources/bluetooth_internals/page.js
@@ -0,0 +1,314 @@
+// Copyright 2014 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.
+
+// <include src="../node_utils.js">
+
+cr.define('cr.ui.pageManager', function() {
+ const PageManager = cr.ui.pageManager.PageManager;
+
+ /**
+ * Base class for pages that can be shown and hidden by PageManager. Each Page
+ * is like a node in a forest, corresponding to a particular div. At any
+ * point, one root Page is visible, and any visible Page can show a child Page
+ * as an overlay. The host of the root Page(s) should provide a container div
+ * for each nested level to enforce the stack order of overlays.
+ */
+ class Page extends cr.EventTarget {
+ /**
+ * @param {string} name Page name.
+ * @param {string} title Page title, used for history.
+ * @param {string} pageDivName ID of the div corresponding to the page.
+ */
+ constructor(name, title, pageDivName) {
+ super();
+
+ this.name = name;
+ this.title = title;
+ this.pageDivName = pageDivName;
+ this.pageDiv = getRequiredElement(this.pageDivName);
+ // |pageDiv.page| is set to the page object (this) when the page is
+ // visible to track which page is being shown when multiple pages can
+ // share the same underlying div.
+ this.pageDiv.page = null;
+ this.tab = null;
+ this.lastFocusedElement = null;
+ this.hash = '';
+
+ /**
+ * The parent page of this page; or null for root pages.
+ * @type {cr.ui.pageManager.Page}
+ */
+ this.parentPage = null;
+
+ /**
+ * The section on the parent page that is associated with this page.
+ * Can be null.
+ * @type {Element}
+ */
+ this.associatedSection = null;
+
+ /**
+ * An array of controls that are associated with this page. The first
+ * control should be located on a root page.
+ * @type {Array<Element>}
+ */
+ this.associatedControls = null;
+
+ /**
+ * If true; this page should always be considered the top-most page when
+ * visible.
+ * @private {boolean}
+ */
+ this.alwaysOnTop_ = false;
+
+ /**
+ * Set this to handle cancelling an overlay (and skip some typical steps).
+ * @see {cr.ui.PageManager.prototype.cancelOverlay}
+ * @type {?Function}
+ */
+ this.handleCancel = null;
+
+ /** @type {boolean} */
+ this.isOverlay = false;
+ }
+
+ /**
+ * Initializes page content.
+ */
+ initializePage() {}
+
+ /**
+ * Called by the PageManager when this.hash changes while the page is
+ * already visible. This is analogous to the hashchange DOM event.
+ */
+ didChangeHash() {}
+
+ /**
+ * Sets focus on the first focusable element. Override for a custom focus
+ * strategy.
+ */
+ focus() {
+ cr.ui.setInitialFocus(this.pageDiv);
+ }
+
+ /**
+ * Reverse any buttons strips in this page (only applies to overlays).
+ * @see cr.ui.reverseButtonStrips for an explanation of why this is
+ * necessary and when it's done.
+ */
+ reverseButtonStrip() {
+ assert(this.isOverlay);
+ cr.ui.reverseButtonStrips(this.pageDiv);
+ }
+
+ /**
+ * Whether it should be possible to show the page.
+ * @return {boolean} True if the page should be shown.
+ */
+ canShowPage() {
+ return true;
+ }
+
+ /**
+ * Updates the hash of the current page. If the page is topmost, the history
+ * state is updated.
+ * @param {string} hash The new hash value. Like location.hash, this
+ * should include the leading '#' if not empty.
+ */
+ setHash(hash) {
+ if (this.hash == hash) {
+ return;
+ }
+ this.hash = hash;
+ PageManager.onPageHashChanged(this);
+ }
+
+ /**
+ * Called after the page has been shown.
+ */
+ didShowPage() {}
+
+ /**
+ * Called before the page will be hidden, e.g., when a different root page
+ * will be shown.
+ */
+ willHidePage() {}
+
+ /**
+ * Called after the overlay has been closed.
+ */
+ didClosePage() {}
+
+ /**
+ * Gets the container div for this page if it is an overlay.
+ * @type {HTMLDivElement}
+ */
+ get container() {
+ assert(this.isOverlay);
+ return this.pageDiv.parentNode;
+ }
+
+ /**
+ * Gets page visibility state.
+ * @type {boolean}
+ */
+ get visible() {
+ // If this is an overlay dialog it is no longer considered visible while
+ // the overlay is fading out. See http://crbug.com/118629.
+ if (this.isOverlay && this.container.classList.contains('transparent')) {
+ return false;
+ }
+ if (this.pageDiv.hidden) {
+ return false;
+ }
+ return this.pageDiv.page == this;
+ }
+
+ /**
+ * Sets page visibility.
+ * @type {boolean}
+ */
+ set visible(visible) {
+ if ((this.visible && visible) || (!this.visible && !visible)) {
+ return;
+ }
+
+ // If using an overlay, the visibility of the dialog is toggled at the
+ // same time as the overlay to show the dialog's out transition. This
+ // is handled in setOverlayVisible.
+ if (this.isOverlay) {
+ this.setOverlayVisible_(visible);
+ } else {
+ this.pageDiv.page = this;
+ this.pageDiv.hidden = !visible;
+ PageManager.onPageVisibilityChanged(this);
+ }
+
+ cr.dispatchPropertyChange(this, 'visible', visible, !visible);
+ }
+
+ /**
+ * Whether the page is considered 'sticky', such that it will remain a root
+ * page even if sub-pages change.
+ * @type {boolean} True if this page is sticky.
+ */
+ get sticky() {
+ return false;
+ }
+
+ /**
+ * @type {boolean} True if this page should always be considered the
+ * top-most page when visible.
+ */
+ get alwaysOnTop() {
+ return this.alwaysOnTop_;
+ }
+
+ /**
+ * @type {boolean} True if this page should always be considered the
+ * top-most page when visible. Only overlays can be always on top.
+ */
+ set alwaysOnTop(value) {
+ assert(this.isOverlay);
+ this.alwaysOnTop_ = value;
+ }
+
+ /**
+ * Shows or hides an overlay (including any visible dialog).
+ * @param {boolean} visible Whether the overlay should be visible or not.
+ * @private
+ */
+ setOverlayVisible_(visible) {
+ assert(this.isOverlay);
+ const pageDiv = this.pageDiv;
+ const container = this.container;
+
+ if (container.hidden != visible) {
+ if (visible) {
+ // If the container is set hidden and then immediately set visible
+ // again, the fadeCompleted_ callback would cause it to be erroneously
+ // hidden again. Removing the transparent tag avoids that.
+ container.classList.remove('transparent');
+
+ // Hide all dialogs in this container since a different one may have
+ // been previously visible before fading out.
+ const pages = container.querySelectorAll('.page');
+ for (let i = 0; i < pages.length; i++) {
+ pages[i].hidden = true;
+ }
+ // Show the new dialog.
+ pageDiv.hidden = false;
+ pageDiv.page = this;
+ }
+ return;
+ }
+
+ const self = this;
+ const loading = PageManager.isLoading();
+ if (!loading) {
+ // TODO(flackr): Use an event delegate to avoid having to subscribe and
+ // unsubscribe for transitionend events.
+ container.addEventListener('transitionend', function f(e) {
+ const propName = e.propertyName;
+ if (e.target != e.currentTarget ||
+ (propName && propName != 'opacity')) {
+ return;
+ }
+ container.removeEventListener('transitionend', f);
+ self.fadeCompleted_();
+ });
+ // transition is 200ms. Let's wait for 400ms.
+ ensureTransitionEndEvent(container, 400);
+ }
+
+ if (visible) {
+ container.hidden = false;
+ pageDiv.hidden = false;
+ pageDiv.page = this;
+ // NOTE: This is a hacky way to force the container to layout which
+ // will allow us to trigger the transition.
+ /** @suppress {uselessCode} */
+ container.scrollTop;
+
+ this.pageDiv.removeAttribute('aria-hidden');
+ if (this.parentPage) {
+ this.parentPage.pageDiv.parentElement.setAttribute(
+ 'aria-hidden', true);
+ }
+ container.classList.remove('transparent');
+ PageManager.onPageVisibilityChanged(this);
+ } else {
+ // Kick change events for text fields.
+ if (pageDiv.contains(document.activeElement)) {
+ document.activeElement.blur();
+ }
+ container.classList.add('transparent');
+ }
+
+ if (loading) {
+ this.fadeCompleted_();
+ }
+ }
+
+ /**
+ * Called when a container opacity transition finishes.
+ * @private
+ */
+ fadeCompleted_() {
+ if (this.container.classList.contains('transparent')) {
+ this.pageDiv.hidden = true;
+ this.container.hidden = true;
+
+ if (this.parentPage) {
+ this.parentPage.pageDiv.parentElement.removeAttribute('aria-hidden');
+ }
+
+ PageManager.onPageVisibilityChanged(this);
+ }
+ }
+ }
+
+ // Export
+ return {Page: Page};
+});
diff --git a/chromium/chrome/browser/resources/bluetooth_internals/page_manager.html b/chromium/chrome/browser/resources/bluetooth_internals/page_manager.html
new file mode 100644
index 00000000000..5ce86ea9086
--- /dev/null
+++ b/chromium/chrome/browser/resources/bluetooth_internals/page_manager.html
@@ -0,0 +1 @@
+<script src="page_manager.js"></script>
diff --git a/chromium/chrome/browser/resources/bluetooth_internals/page_manager.js b/chromium/chrome/browser/resources/bluetooth_internals/page_manager.js
new file mode 100644
index 00000000000..bca2b6dbb76
--- /dev/null
+++ b/chromium/chrome/browser/resources/bluetooth_internals/page_manager.js
@@ -0,0 +1,800 @@
+// Copyright 2014 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.
+
+cr.define('cr.ui.pageManager', function() {
+ /**
+ * PageManager contains a list of root Page and overlay Page objects and
+ * handles "navigation" by showing and hiding these pages and overlays. On
+ * initial load, PageManager can use the path to open the correct hierarchy
+ * of pages and overlay(s). Handlers for user events, like pressing buttons,
+ * can call into PageManager to open a particular overlay or cancel an
+ * existing overlay.
+ */
+ const PageManager = {
+ /**
+ * True if page is served from a dialog.
+ * @type {boolean}
+ */
+ isDialog: false,
+
+ /**
+ * Offset of page container in pixels. Uber pages that use the side menu
+ * can override this with the setter.
+ * @type {number}
+ */
+ horizontalOffset_: 23,
+
+ /**
+ * Root pages. Maps lower-case page names to the respective page object.
+ * @type {!Object<!cr.ui.pageManager.Page>}
+ */
+ registeredPages: {},
+
+ /**
+ * Pages which are meant to behave like modal dialogs. Maps lower-case
+ * overlay names to the respective overlay object.
+ * @type {!Object<!cr.ui.pageManager.Page>}
+ * @private
+ */
+ registeredOverlayPages: {},
+
+ /**
+ * Observers will be notified when opening and closing overlays.
+ * @type {!Array<!cr.ui.pageManager.PageManager.Observer>}
+ */
+ observers_: [],
+
+ /**
+ * Initializes the complete page.
+ * @param {cr.ui.pageManager.Page} defaultPage The page to be shown when no
+ * page is specified in the path.
+ */
+ initialize: function(defaultPage) {
+ this.defaultPage_ = defaultPage;
+
+ cr.ui.FocusOutlineManager.forDocument(document);
+ document.addEventListener('scroll', this.handleScroll_.bind(this));
+
+ // Trigger the scroll handler manually to set the initial state.
+ this.handleScroll_();
+
+ // Shake the dialog if the user clicks outside the dialog bounds.
+ const containers = /** @type {!NodeList<!HTMLElement>} */ (
+ document.querySelectorAll('body > .overlay'));
+ for (let i = 0; i < containers.length; i++) {
+ const overlay = containers[i];
+ cr.ui.overlay.setupOverlay(overlay);
+ overlay.addEventListener(
+ 'cancelOverlay', this.cancelOverlay.bind(this));
+ }
+
+ cr.ui.overlay.globalInitialization();
+ },
+
+ /**
+ * Registers new page.
+ * @param {!cr.ui.pageManager.Page} page Page to register.
+ */
+ register: function(page) {
+ this.registeredPages[page.name.toLowerCase()] = page;
+ page.initializePage();
+ },
+
+ /**
+ * Unregisters an existing page.
+ * @param {!cr.ui.pageManager.Page} page Page to unregister.
+ */
+ unregister: function(page) {
+ delete this.registeredPages[page.name.toLowerCase()];
+ },
+
+ /**
+ * Registers a new Overlay page.
+ * @param {!cr.ui.pageManager.Page} overlay Overlay to register.
+ * @param {cr.ui.pageManager.Page} parentPage Associated parent page for
+ * this overlay.
+ * @param {Array} associatedControls Array of control elements associated
+ * with this page.
+ */
+ registerOverlay: function(overlay, parentPage, associatedControls) {
+ this.registeredOverlayPages[overlay.name.toLowerCase()] = overlay;
+ overlay.parentPage = parentPage;
+ if (associatedControls) {
+ overlay.associatedControls = associatedControls;
+ if (associatedControls.length) {
+ overlay.associatedSection =
+ this.findSectionForNode_(associatedControls[0]);
+ }
+
+ // Sanity check.
+ for (let i = 0; i < associatedControls.length; ++i) {
+ assert(associatedControls[i], 'Invalid element passed.');
+ }
+ }
+
+ overlay.tab = undefined;
+ overlay.isOverlay = true;
+
+ overlay.reverseButtonStrip();
+ overlay.initializePage();
+ },
+
+ /**
+ * Shows the default page.
+ * @param {boolean=} opt_updateHistory If we should update the history after
+ * showing the page (defaults to true).
+ */
+ showDefaultPage: function(opt_updateHistory) {
+ assert(
+ this.defaultPage_ instanceof cr.ui.pageManager.Page,
+ 'PageManager must be initialized with a default page.');
+ this.showPageByName(this.defaultPage_.name, opt_updateHistory);
+ },
+
+ /**
+ * Shows a registered page. This handles both root and overlay pages.
+ * @param {string} pageName Page name.
+ * @param {boolean=} opt_updateHistory If we should update the history after
+ * showing the page (defaults to true).
+ * @param {Object=} opt_propertyBag An optional bag of properties including
+ * replaceState (if history state should be replaced instead of pushed).
+ * hash (a hash state to attach to the page).
+ */
+ showPageByName: function(pageName, opt_updateHistory, opt_propertyBag) {
+ opt_updateHistory = opt_updateHistory !== false;
+ opt_propertyBag = opt_propertyBag || {};
+
+ // If a bubble is currently being shown, hide it.
+ this.hideBubble();
+
+ // Find the currently visible root-level page.
+ let rootPage = null;
+ for (const name in this.registeredPages) {
+ const page = this.registeredPages[name];
+ if (page.visible && !page.parentPage) {
+ rootPage = page;
+ break;
+ }
+ }
+
+ // Find the target page.
+ let targetPage = this.registeredPages[pageName.toLowerCase()];
+ if (!targetPage || !targetPage.canShowPage()) {
+ // If it's not a page, try it as an overlay.
+ const hash = opt_propertyBag.hash || '';
+ if (!targetPage && this.showOverlay_(pageName, hash, rootPage)) {
+ if (opt_updateHistory) {
+ this.updateHistoryState_(!!opt_propertyBag.replaceState);
+ }
+ this.updateTitle_();
+ return;
+ }
+ targetPage = this.defaultPage_;
+ }
+
+ pageName = targetPage.name.toLowerCase();
+ const targetPageWasVisible = targetPage.visible;
+
+ // Determine if the root page is 'sticky', meaning that it
+ // shouldn't change when showing an overlay. This can happen for special
+ // pages like Search.
+ const isRootPageLocked =
+ rootPage && rootPage.sticky && targetPage.parentPage;
+
+ // Notify pages if they will be hidden.
+ this.forEachPage_(!isRootPageLocked, function(page) {
+ if (page.name != pageName && !this.isAncestorOfPage(page, targetPage)) {
+ page.willHidePage();
+ }
+ });
+
+ // Update the page's hash.
+ targetPage.hash = opt_propertyBag.hash || '';
+
+ // Update visibilities to show only the hierarchy of the target page.
+ this.forEachPage_(!isRootPageLocked, function(page) {
+ page.visible =
+ page.name == pageName || this.isAncestorOfPage(page, targetPage);
+ });
+
+ // Update the history and current location.
+ if (opt_updateHistory) {
+ this.updateHistoryState_(!!opt_propertyBag.replaceState);
+ }
+
+ // Update focus if any other control was focused on the previous page,
+ // or the previous page is not known.
+ if (document.activeElement != document.body &&
+ (!rootPage || rootPage.pageDiv.contains(document.activeElement))) {
+ targetPage.focus();
+ }
+
+ // Notify pages if they were shown.
+ this.forEachPage_(!isRootPageLocked, function(page) {
+ if (!targetPageWasVisible &&
+ (page.name == pageName ||
+ this.isAncestorOfPage(page, targetPage))) {
+ page.didShowPage();
+ }
+ });
+
+ // If the target page was already visible, notify it that its hash
+ // changed externally.
+ if (targetPageWasVisible) {
+ targetPage.didChangeHash();
+ }
+
+ // Update the document title. Do this after didShowPage was called, in
+ // case a page decides to change its title.
+ this.updateTitle_();
+ },
+
+ /**
+ * Returns the name of the page from the current path.
+ * @return {string} Name of the page specified by the current path.
+ */
+ getPageNameFromPath: function() {
+ const path = location.pathname;
+ if (path.length <= 1) {
+ return this.defaultPage_.name;
+ }
+
+ // Skip starting slash and remove trailing slash (if any).
+ return path.slice(1).replace(/\/$/, '');
+ },
+
+ /**
+ * Gets the level of the page. Root pages (e.g., BrowserOptions) are at
+ * level 0.
+ * @return {number} How far down this page is from the root page.
+ */
+ getNestingLevel: function(page) {
+ let level = 0;
+ let parent = page.parentPage;
+ while (parent) {
+ level++;
+ parent = parent.parentPage;
+ }
+ return level;
+ },
+
+ /**
+ * Checks whether one page is an ancestor of the other page in terms of
+ * subpage nesting.
+ * @param {cr.ui.pageManager.Page} potentialAncestor Potential ancestor.
+ * @param {cr.ui.pageManager.Page} potentialDescendent Potential descendent.
+ * @return {boolean} True if |potentialDescendent| is nested under
+ * |potentialAncestor|.
+ */
+ isAncestorOfPage: function(potentialAncestor, potentialDescendent) {
+ let parent = potentialDescendent.parentPage;
+ while (parent) {
+ if (parent == potentialAncestor) {
+ return true;
+ }
+ parent = parent.parentPage;
+ }
+ return false;
+ },
+
+ /**
+ * Returns true if the page is a direct descendent of a root page, or if
+ * the page is considered always on top. Doesn't consider visibility.
+ * @param {cr.ui.pageManager.Page} page Page to check.
+ * @return {boolean} True if |page| is a top-level overlay.
+ */
+ isTopLevelOverlay: function(page) {
+ return page.isOverlay &&
+ (page.alwaysOnTop || this.getNestingLevel(page) == 1);
+ },
+
+ /**
+ * Called when an page is shown or hidden to update the root page
+ * based on the page's new visibility.
+ * @param {cr.ui.pageManager.Page} page The page being made visible or
+ * invisible.
+ */
+ onPageVisibilityChanged: function(page) {
+ this.updateRootPageFreezeState();
+
+ for (let i = 0; i < this.observers_.length; ++i) {
+ this.observers_[i].onPageVisibilityChanged(page);
+ }
+
+ if (!page.visible && this.isTopLevelOverlay(page)) {
+ this.updateScrollPosition_();
+ }
+ },
+
+ /**
+ * Called when a page's hash changes. If the page is the topmost visible
+ * page, the history state is updated.
+ * @param {cr.ui.pageManager.Page} page The page whose hash has changed.
+ */
+ onPageHashChanged: function(page) {
+ if (page == this.getTopmostVisiblePage()) {
+ this.updateHistoryState_(false);
+ }
+ },
+
+ /**
+ * Returns the topmost visible page, or null if no page is visible.
+ * @return {cr.ui.pageManager.Page} The topmost visible page.
+ */
+ getTopmostVisiblePage: function() {
+ // Check overlays first since they're top-most if visible.
+ return this.getVisibleOverlay_() ||
+ this.getTopmostVisibleNonOverlayPage_();
+ },
+
+ /**
+ * Closes the visible overlay. Updates the history state after closing the
+ * overlay.
+ */
+ closeOverlay: function() {
+ const overlay = this.getVisibleOverlay_();
+ if (!overlay) {
+ return;
+ }
+
+ overlay.visible = false;
+ overlay.didClosePage();
+
+ this.updateHistoryState_(false);
+ this.updateTitle_();
+
+ this.restoreLastFocusedElement_();
+ },
+
+ /**
+ * Closes all overlays and updates the history after each closed overlay.
+ */
+ closeAllOverlays: function() {
+ while (this.isOverlayVisible_()) {
+ this.closeOverlay();
+ }
+ },
+
+ /**
+ * Cancels (closes) the overlay, due to the user pressing <Esc>.
+ */
+ cancelOverlay: function() {
+ // Blur the active element to ensure any changed pref value is saved.
+ document.activeElement.blur();
+ const overlay = this.getVisibleOverlay_();
+ if (!overlay) {
+ return;
+ }
+ // Let the overlay handle the <Esc> if it wants to.
+ if (overlay.handleCancel) {
+ overlay.handleCancel();
+ this.restoreLastFocusedElement_();
+ } else {
+ this.closeOverlay();
+ }
+ },
+
+ /**
+ * Shows an informational bubble displaying |content| and pointing at the
+ * |target| element. If |content| has focusable elements, they join the
+ * current page's tab order as siblings of |domSibling|.
+ * @param {HTMLDivElement} content The content of the bubble.
+ * @param {HTMLElement} target The element at which the bubble points.
+ * @param {HTMLElement} domSibling The element after which the bubble is
+ * added to the DOM.
+ * @param {cr.ui.ArrowLocation} location The arrow location.
+ */
+ showBubble: function(content, target, domSibling, location) {
+ this.hideBubble();
+
+ const bubble = new cr.ui.AutoCloseBubble;
+ bubble.anchorNode = target;
+ bubble.domSibling = domSibling;
+ bubble.arrowLocation = location;
+ bubble.content = content;
+ bubble.show();
+ this.bubble_ = bubble;
+ },
+
+ /**
+ * Hides the currently visible bubble, if any.
+ */
+ hideBubble: function() {
+ if (this.bubble_) {
+ this.bubble_.hide();
+ }
+ },
+
+ /**
+ * Returns the currently visible bubble, or null if no bubble is visible.
+ * @return {cr.ui.AutoCloseBubble} The bubble currently being shown.
+ */
+ getVisibleBubble: function() {
+ const bubble = this.bubble_;
+ return bubble && !bubble.hidden ? bubble : null;
+ },
+
+ /**
+ * Callback for window.onpopstate to handle back/forward navigations.
+ * @param {string} pageName The current page name.
+ * @param {string} hash The hash to pass into the page.
+ * @param {Object} data State data pushed into history.
+ */
+ setState: function(pageName, hash, data) {
+ const currentOverlay = this.getVisibleOverlay_();
+ const lowercaseName = pageName.toLowerCase();
+ const newPage = this.registeredPages[lowercaseName] ||
+ this.registeredOverlayPages[lowercaseName] || this.defaultPage_;
+ if (currentOverlay && !this.isAncestorOfPage(currentOverlay, newPage)) {
+ currentOverlay.visible = false;
+ currentOverlay.didClosePage();
+ }
+ this.showPageByName(pageName, false, {hash: hash});
+ },
+
+
+ /**
+ * Whether the page is still loading (i.e. onload hasn't finished running).
+ * @return {boolean} Whether the page is still loading.
+ */
+ isLoading: function() {
+ return document.documentElement.classList.contains('loading');
+ },
+
+ /**
+ * Callback for window.onbeforeunload. Used to notify overlays that they
+ * will be closed.
+ */
+ willClose: function() {
+ const overlay = this.getVisibleOverlay_();
+ if (overlay) {
+ overlay.didClosePage();
+ }
+ },
+
+ /**
+ * Freezes/unfreezes the scroll position of the root page based on the
+ * current page stack.
+ */
+ updateRootPageFreezeState: function() {
+ const topPage = this.getTopmostVisiblePage();
+ if (topPage) {
+ this.setRootPageFrozen_(topPage.isOverlay);
+ }
+ },
+
+ /**
+ * Change the horizontal offset used to reposition elements while showing an
+ * overlay from the default.
+ */
+ set horizontalOffset(value) {
+ this.horizontalOffset_ = value;
+ },
+
+ /**
+ * @param {!cr.ui.pageManager.PageManager.Observer} observer The observer to
+ * register.
+ */
+ addObserver: function(observer) {
+ this.observers_.push(observer);
+ },
+
+ /**
+ * Shows a registered overlay page. Does not update history.
+ * @param {string} overlayName Page name.
+ * @param {string} hash The hash state to associate with the overlay.
+ * @param {cr.ui.pageManager.Page} rootPage The currently visible root-level
+ * page.
+ * @return {boolean} Whether we showed an overlay.
+ * @private
+ */
+ showOverlay_: function(overlayName, hash, rootPage) {
+ const overlay = this.registeredOverlayPages[overlayName.toLowerCase()];
+ if (!overlay || !overlay.canShowPage()) {
+ return false;
+ }
+
+ const focusOutlineManager =
+ cr.ui.FocusOutlineManager.forDocument(document);
+
+ // Save the currently focused element in the page for restoration later.
+ const currentPage = this.getTopmostVisiblePage();
+ if (currentPage && focusOutlineManager.visible) {
+ currentPage.lastFocusedElement = document.activeElement;
+ }
+
+ if ((!rootPage || !rootPage.sticky) && overlay.parentPage &&
+ !overlay.parentPage.visible) {
+ this.showPageByName(overlay.parentPage.name, false);
+ }
+
+ overlay.hash = hash;
+ if (!overlay.visible) {
+ overlay.visible = true;
+ overlay.didShowPage();
+ } else {
+ overlay.didChangeHash();
+ }
+
+ if (focusOutlineManager.visible) {
+ overlay.focus();
+ }
+
+ if (!overlay.pageDiv.contains(document.activeElement)) {
+ document.activeElement.blur();
+ }
+
+ if ($('search-field') && $('search-field').value == '') {
+ const section = overlay.associatedSection;
+ if (section) {
+ /** @suppress {checkTypes|checkVars} */
+ (function() {
+ options.BrowserOptions.scrollToSection(section);
+ })();
+ }
+ }
+
+ return true;
+ },
+
+ /**
+ * Returns whether or not an overlay is visible.
+ * @return {boolean} True if an overlay is visible.
+ * @private
+ */
+ isOverlayVisible_: function() {
+ return this.getVisibleOverlay_() != null;
+ },
+
+ /**
+ * Returns the currently visible overlay, or null if no page is visible.
+ * @return {cr.ui.pageManager.Page} The visible overlay.
+ * @private
+ */
+ getVisibleOverlay_: function() {
+ let topmostPage = null;
+ for (const name in this.registeredOverlayPages) {
+ const page = this.registeredOverlayPages[name];
+ if (!page.visible) {
+ continue;
+ }
+
+ if (page.alwaysOnTop) {
+ return page;
+ }
+
+ if (!topmostPage ||
+ this.getNestingLevel(page) > this.getNestingLevel(topmostPage)) {
+ topmostPage = page;
+ }
+ }
+ return topmostPage;
+ },
+
+ /**
+ * Returns the topmost visible page (overlays excluded).
+ * @return {cr.ui.pageManager.Page} The topmost visible page aside from any
+ * overlays.
+ * @private
+ */
+ getTopmostVisibleNonOverlayPage_: function() {
+ for (const name in this.registeredPages) {
+ const page = this.registeredPages[name];
+ if (page.visible) {
+ return page;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Scrolls the page to the correct position (the top when opening an
+ * overlay, or the old scroll position a previously hidden overlay
+ * becomes visible).
+ * @private
+ */
+ updateScrollPosition_: function() {
+ const container = $('page-container');
+ const scrollTop = container.oldScrollTop || 0;
+ container.oldScrollTop = undefined;
+ window.scroll(scrollLeftForDocument(document), scrollTop);
+ },
+
+ /**
+ * Updates the title to the title of the current page, or of the topmost
+ * visible page with a non-empty title.
+ * @private
+ */
+ updateTitle_: function() {
+ let page = this.getTopmostVisiblePage();
+ while (page) {
+ if (page.title) {
+ for (let i = 0; i < this.observers_.length; ++i) {
+ this.observers_[i].updateTitle(page.title);
+ }
+ return;
+ }
+ page = page.parentPage;
+ }
+ },
+
+ /**
+ * Constructs a new path to push onto the history stack, using observers
+ * to update the history.
+ * @param {boolean} replace If true, handlers should replace the current
+ * history event rather than create new ones.
+ * @private
+ */
+ updateHistoryState_: function(replace) {
+ if (this.isDialog) {
+ return;
+ }
+
+ const page = this.getTopmostVisiblePage();
+ let path = window.location.pathname + window.location.hash;
+ if (path) {
+ // Remove trailing slash.
+ path = path.slice(1).replace(/\/(?:#|$)/, '');
+ }
+
+ // If the page is already in history (the user may have clicked the same
+ // link twice, or this is the initial load), do nothing.
+ const newPath = (page == this.defaultPage_ ? '' : page.name) + page.hash;
+ if (path == newPath) {
+ return;
+ }
+
+ for (let i = 0; i < this.observers_.length; ++i) {
+ this.observers_[i].updateHistory(newPath, replace);
+ }
+ },
+
+ /**
+ * Restores the last focused element on a given page.
+ * @private
+ */
+ restoreLastFocusedElement_: function() {
+ const currentPage = this.getTopmostVisiblePage();
+
+ if (!currentPage.lastFocusedElement) {
+ return;
+ }
+
+ if (cr.ui.FocusOutlineManager.forDocument(document).visible) {
+ currentPage.lastFocusedElement.focus();
+ }
+
+ currentPage.lastFocusedElement = null;
+ },
+
+ /**
+ * Find an enclosing section for an element if it exists.
+ * @param {Node} node Element to search.
+ * @return {Node} The section element, or null.
+ * @private
+ */
+ findSectionForNode_: function(node) {
+ while (node = node.parentNode) {
+ if (node.nodeName == 'SECTION') {
+ return node;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Freezes/unfreezes the scroll position of the root page container.
+ * @param {boolean} freeze Whether the page should be frozen.
+ * @private
+ */
+ setRootPageFrozen_: function(freeze) {
+ const container = $('page-container');
+ if (container.classList.contains('frozen') == freeze) {
+ return;
+ }
+
+ if (freeze) {
+ // Lock the width, since auto width computation may change.
+ container.style.width = window.getComputedStyle(container).width;
+ container.oldScrollTop = scrollTopForDocument(document);
+ container.classList.add('frozen');
+ const verticalPosition =
+ container.getBoundingClientRect().top - container.oldScrollTop;
+ container.style.top = verticalPosition + 'px';
+ this.updateFrozenElementHorizontalPosition_(container);
+ } else {
+ container.classList.remove('frozen');
+ container.style.top = '';
+ container.style.left = '';
+ container.style.right = '';
+ container.style.width = '';
+ }
+ },
+
+ /**
+ * Called when the page is scrolled; moves elements that are position:fixed
+ * but should only behave as if they are fixed for vertical scrolling.
+ * @private
+ */
+ handleScroll_: function() {
+ this.updateAllFrozenElementPositions_();
+ },
+
+ /**
+ * Updates all frozen pages to match the horizontal scroll position.
+ * @private
+ */
+ updateAllFrozenElementPositions_: function() {
+ const frozenElements = document.querySelectorAll('.frozen');
+ for (let i = 0; i < frozenElements.length; i++) {
+ this.updateFrozenElementHorizontalPosition_(frozenElements[i]);
+ }
+ },
+
+ /**
+ * Updates the given frozen element to match the horizontal scroll position.
+ * @param {HTMLElement} e The frozen element to update.
+ * @private
+ */
+ updateFrozenElementHorizontalPosition_: function(e) {
+ if (isRTL()) {
+ e.style.right = this.horizontalOffset + 'px';
+ } else {
+ const scrollLeft = scrollLeftForDocument(document);
+ e.style.left = this.horizontalOffset - scrollLeft + 'px';
+ }
+ },
+
+ /**
+ * Calls the given callback with each registered page.
+ * @param {boolean} includeRootPages Whether the callback should be called
+ * for the root pages.
+ * @param {function(cr.ui.pageManager.Page)} callback The callback.
+ * @private
+ */
+ forEachPage_: function(includeRootPages, callback) {
+ let pageNames = Object.keys(this.registeredOverlayPages);
+ if (includeRootPages) {
+ pageNames = Object.keys(this.registeredPages).concat(pageNames);
+ }
+
+ pageNames.forEach(function(name) {
+ callback.call(
+ this,
+ this.registeredOverlayPages[name] || this.registeredPages[name]);
+ }, this);
+ },
+ };
+
+ /**
+ * An observer of PageManager.
+ * @constructor
+ */
+ PageManager.Observer = function() {};
+
+ PageManager.Observer.prototype = {
+ /**
+ * Called when a page is being shown or has been hidden.
+ * @param {cr.ui.pageManager.Page} page The page being shown or hidden.
+ */
+ onPageVisibilityChanged: function(page) {},
+
+ /**
+ * Called when a new title should be set.
+ * @param {string} title The title to set.
+ */
+ updateTitle: function(title) {},
+
+ /**
+ * Called when a page is navigated to.
+ * @param {string} path The path of the page being visited.
+ * @param {boolean} replace If true, allow no history events to be created.
+ */
+ updateHistory: function(path, replace) {},
+ };
+
+ // Export
+ return {PageManager: PageManager};
+});
diff --git a/chromium/chrome/browser/resources/bluetooth_internals/resources.grd b/chromium/chrome/browser/resources/bluetooth_internals/resources.grd
index 786dc15be70..4cd5644cc98 100644
--- a/chromium/chrome/browser/resources/bluetooth_internals/resources.grd
+++ b/chromium/chrome/browser/resources/bluetooth_internals/resources.grd
@@ -29,6 +29,10 @@
file="adapter_page.js"
type="BINDATA"
compress="gzip" />
+ <include name="IDR_BLUETOOTH_INTERNALS_DEBUG_LOG_PAGE_JS"
+ file="debug_log_page.js"
+ type="BINDATA"
+ compress="gzip" />
<include name="IDR_BLUETOOTH_INTERNALS_CHARACTERISTIC_LIST_JS"
file="characteristic_list.js"
type="BINDATA"
@@ -89,6 +93,23 @@
file="object_fieldset.js"
type="BINDATA"
compress="gzip" />
+ <include name="IDR_BLUETOOTH_INTERNALS_PAGE_MANAGER_HTML"
+ file="page_manager.html"
+ type="BINDATA"
+ compress="gzip" />
+ <include name="IDR_BLUETOOTH_INTERNALS_PAGE_MANAGER_JS"
+ file="page_manager.js"
+ type="BINDATA"
+ compress="gzip" />
+ <include name="IDR_BLUETOOTH_INTERNALS_PAGE_HTML"
+ file="page.html"
+ type="BINDATA"
+ compress="gzip" />
+ <include name="IDR_BLUETOOTH_INTERNALS_PAGE_JS"
+ file="page.js"
+ type="BINDATA"
+ compress="gzip" />
+
<include name="IDR_BLUETOOTH_INTERNALS_SERVICE_LIST_JS"
file="service_list.js"
type="BINDATA"