diff options
Diffstat (limited to 'chromium/chrome/browser/resources/bluetooth_internals')
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" |