UNPKG

@dcloudio/uni-debugger

Version:

uni-app debugger

711 lines (617 loc) 26 kB
// Copyright 2015 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. Devices.DevicesView = class extends UI.VBox { constructor() { super(true); this.registerRequiredCSS('devices/devicesView.css'); this.contentElement.classList.add('devices-view'); const hbox = this.contentElement.createChild('div', 'hbox devices-container'); const sidebar = hbox.createChild('div', 'devices-sidebar'); sidebar.createChild('div', 'devices-view-title').createTextChild(Common.UIString('Devices')); this._sidebarList = sidebar.createChild('div', 'devices-sidebar-list'); this._discoveryView = new Devices.DevicesView.DiscoveryView(); this._sidebarListSpacer = this._sidebarList.createChild('div', 'devices-sidebar-spacer'); this._discoveryListItem = this._sidebarList.createChild('div', 'devices-sidebar-item'); this._discoveryListItem.textContent = Common.UIString('Settings'); this._discoveryListItem.addEventListener( 'click', this._selectSidebarListItem.bind(this, this._discoveryListItem, this._discoveryView)); /** @type {!Map<string, !Devices.DevicesView.DeviceView>} */ this._viewById = new Map(); /** @type {!Array<!Adb.Device>} */ this._devices = []; /** @type {!Map<string, !Element>} */ this._listItemById = new Map(); /** @type {?Element} */ this._selectedListItem = null; /** @type {?UI.Widget} */ this._visibleView = null; this._viewContainer = hbox.createChild('div', 'flex-auto vbox'); const discoveryFooter = this.contentElement.createChild('div', 'devices-footer'); this._deviceCountSpan = discoveryFooter.createChild('span'); discoveryFooter.createChild('span').textContent = Common.UIString(' Read '); discoveryFooter.appendChild(UI.XLink.create( 'https://developers.google.com/chrome-developer-tools/docs/remote-debugging', Common.UIString('remote debugging documentation'))); discoveryFooter.createChild('span').textContent = Common.UIString(' for more information.'); this._updateFooter(); this._selectSidebarListItem(this._discoveryListItem, this._discoveryView); InspectorFrontendHost.events.addEventListener( InspectorFrontendHostAPI.Events.DevicesUpdated, this._devicesUpdated, this); InspectorFrontendHost.events.addEventListener( InspectorFrontendHostAPI.Events.DevicesDiscoveryConfigChanged, this._devicesDiscoveryConfigChanged, this); InspectorFrontendHost.events.addEventListener( InspectorFrontendHostAPI.Events.DevicesPortForwardingStatusChanged, this._devicesPortForwardingStatusChanged, this); this.contentElement.tabIndex = 0; this.setDefaultFocusedElement(this.contentElement); } /** * @return {!Devices.DevicesView} */ static _instance() { if (!Devices.DevicesView._instanceObject) Devices.DevicesView._instanceObject = new Devices.DevicesView(); return Devices.DevicesView._instanceObject; } /** * @param {!Element} listItem * @param {!UI.Widget} view */ _selectSidebarListItem(listItem, view) { if (this._selectedListItem === listItem) return; if (this._selectedListItem) { this._selectedListItem.classList.remove('selected'); this._visibleView.detach(); } this._visibleView = view; this._selectedListItem = listItem; this._visibleView.show(this._viewContainer); this._selectedListItem.classList.add('selected'); } /** * @param {!Common.Event} event */ _devicesUpdated(event) { this._devices = /** @type {!Array.<!Adb.Device>} */ (event.data) .slice() .filter(d => d.adbSerial.toUpperCase() !== 'WEBRTC' && d.adbSerial.toUpperCase() !== 'LOCALHOST'); for (const device of this._devices) { if (!device.adbConnected) device.adbModel = Common.UIString('Unknown'); } const ids = new Set(); for (const device of this._devices) ids.add(device.id); let selectedRemoved = false; for (const deviceId of this._viewById.keys()) { if (!ids.has(deviceId)) { const listItem = /** @type {!Element} */ (this._listItemById.get(deviceId)); this._listItemById.remove(deviceId); this._viewById.remove(deviceId); listItem.remove(); if (listItem === this._selectedListItem) selectedRemoved = true; } } for (const device of this._devices) { let view = this._viewById.get(device.id); let listItem = this._listItemById.get(device.id); if (!view) { view = new Devices.DevicesView.DeviceView(); this._viewById.set(device.id, view); listItem = this._createSidebarListItem(view); this._listItemById.set(device.id, listItem); this._sidebarList.insertBefore(listItem, this._sidebarListSpacer); } listItem._title.textContent = device.adbModel; listItem._status.textContent = device.adbConnected ? Common.UIString('Connected') : Common.UIString('Pending Authorization'); listItem.classList.toggle('device-connected', device.adbConnected); view.update(device); } if (selectedRemoved) this._selectSidebarListItem(this._discoveryListItem, this._discoveryView); this._updateFooter(); } /** * @param {!UI.Widget} view * @return {!Element} */ _createSidebarListItem(view) { const listItem = createElementWithClass('div', 'devices-sidebar-item'); listItem.addEventListener('click', this._selectSidebarListItem.bind(this, listItem, view)); listItem._title = listItem.createChild('div', 'devices-sidebar-item-title'); listItem._status = listItem.createChild('div', 'devices-sidebar-item-status'); return listItem; } /** * @param {!Common.Event} event */ _devicesDiscoveryConfigChanged(event) { const config = /** @type {!Adb.Config} */ (event.data); this._discoveryView.discoveryConfigChanged(config); } /** * @param {!Common.Event} event */ _devicesPortForwardingStatusChanged(event) { const status = /** @type {!Adb.PortForwardingStatus} */ (event.data); for (const deviceId in status) { const view = this._viewById.get(deviceId); if (view) view.portForwardingStatusChanged(status[deviceId]); } for (const deviceId of this._viewById.keys()) { const view = this._viewById.get(deviceId); if (view && !(deviceId in status)) view.portForwardingStatusChanged({ports: {}, browserId: ''}); } } _updateFooter() { this._deviceCountSpan.textContent = !this._devices.length ? Common.UIString('No devices detected.') : this._devices.length === 1 ? Common.UIString('1 device detected.') : Common.UIString('%d devices detected.', this._devices.length); } /** * @override */ wasShown() { super.wasShown(); InspectorFrontendHost.setDevicesUpdatesEnabled(true); } /** * @override */ willHide() { super.willHide(); InspectorFrontendHost.setDevicesUpdatesEnabled(false); } }; Devices.DevicesView.DiscoveryView = class extends UI.VBox { constructor() { super(); this.setMinimumSize(100, 100); this.element.classList.add('discovery-view'); this.contentElement.createChild('div', 'hbox device-text-row').createChild('div', 'view-title').textContent = Common.UIString('Settings'); const discoverUsbDevicesCheckbox = UI.CheckboxLabel.create(Common.UIString('Discover USB devices')); discoverUsbDevicesCheckbox.classList.add('usb-checkbox'); this.element.appendChild(discoverUsbDevicesCheckbox); this._discoverUsbDevicesCheckbox = discoverUsbDevicesCheckbox.checkboxElement; this._discoverUsbDevicesCheckbox.addEventListener('click', () => { this._config.discoverUsbDevices = this._discoverUsbDevicesCheckbox.checked; InspectorFrontendHost.setDevicesDiscoveryConfig(this._config); }, false); const help = this.element.createChild('div', 'discovery-help'); help.createChild('span').textContent = Common.UIString('Need help? Read Chrome '); help.appendChild(UI.XLink.create( 'https://developers.google.com/chrome-developer-tools/docs/remote-debugging', Common.UIString('remote debugging documentation.'))); /** @type {!Adb.Config} */ this._config; this._portForwardingView = new Devices.DevicesView.PortForwardingView((enabled, config) => { this._config.portForwardingEnabled = enabled; this._config.portForwardingConfig = {}; for (const rule of config) this._config.portForwardingConfig[rule.port] = rule.address; InspectorFrontendHost.setDevicesDiscoveryConfig(this._config); }); this._portForwardingView.show(this.element); } /** * @param {!Adb.Config} config */ discoveryConfigChanged(config) { this._config = config; this._discoverUsbDevicesCheckbox.checked = config.discoverUsbDevices; this._portForwardingView.discoveryConfigChanged(config.portForwardingEnabled, config.portForwardingConfig); } }; /** * @implements {UI.ListWidget.Delegate<Adb.PortForwardingRule>} */ Devices.DevicesView.PortForwardingView = class extends UI.VBox { /** * @param {function(boolean, !Array<!Adb.PortForwardingRule>)} callback */ constructor(callback) { super(); this._callback = callback; this.element.classList.add('port-forwarding-view'); const portForwardingHeader = this.element.createChild('div', 'port-forwarding-header'); const portForwardingEnabledCheckbox = UI.CheckboxLabel.create(Common.UIString('Port forwarding')); portForwardingEnabledCheckbox.classList.add('port-forwarding-checkbox'); portForwardingHeader.appendChild(portForwardingEnabledCheckbox); this._portForwardingEnabledCheckbox = portForwardingEnabledCheckbox.checkboxElement; this._portForwardingEnabledCheckbox.addEventListener('click', this._update.bind(this), false); const portForwardingFooter = this.element.createChild('div', 'port-forwarding-footer'); portForwardingFooter.createChild('span').textContent = Common.UIString( 'Define the listening port on your device that maps to a port accessible from your development machine. '); portForwardingFooter.appendChild(UI.XLink.create( 'https://developer.chrome.com/devtools/docs/remote-debugging#port-forwarding', Common.UIString('Learn more'))); /** @type {!UI.ListWidget<!Adb.PortForwardingRule>} */ this._list = new UI.ListWidget(this); this._list.registerRequiredCSS('devices/devicesView.css'); this._list.element.classList.add('port-forwarding-list'); const placeholder = createElementWithClass('div', 'port-forwarding-list-empty'); placeholder.textContent = Common.UIString('No rules'); this._list.setEmptyPlaceholder(placeholder); this._list.show(this.element); /** @type {?UI.ListWidget.Editor<!Adb.PortForwardingRule>} */ this._editor = null; this.element.appendChild( UI.createTextButton(Common.UIString('Add rule'), this._addRuleButtonClicked.bind(this), 'add-rule-button')); /** @type {!Array<!Adb.PortForwardingRule>} */ this._portForwardingConfig = []; } _update() { this._callback.call(null, this._portForwardingEnabledCheckbox.checked, this._portForwardingConfig); } _addRuleButtonClicked() { this._list.addNewItem(this._portForwardingConfig.length, {port: '', address: ''}); } /** * @param {boolean} portForwardingEnabled * @param {!Adb.PortForwardingConfig} portForwardingConfig */ discoveryConfigChanged(portForwardingEnabled, portForwardingConfig) { this._portForwardingEnabledCheckbox.checked = portForwardingEnabled; this._portForwardingConfig = []; this._list.clear(); for (const key of Object.keys(portForwardingConfig)) { const rule = /** @type {!Adb.PortForwardingRule} */ ({port: key, address: portForwardingConfig[key]}); this._portForwardingConfig.push(rule); this._list.appendItem(rule, true); } } /** * @override * @param {!Adb.PortForwardingRule} rule * @param {boolean} editable * @return {!Element} */ renderItem(rule, editable) { const element = createElementWithClass('div', 'port-forwarding-list-item'); const port = element.createChild('div', 'port-forwarding-value port-forwarding-port'); port.createChild('span', 'port-localhost').textContent = Common.UIString('localhost:'); port.createTextChild(rule.port); element.createChild('div', 'port-forwarding-separator'); element.createChild('div', 'port-forwarding-value').textContent = rule.address; return element; } /** * @override * @param {!Adb.PortForwardingRule} rule * @param {number} index */ removeItemRequested(rule, index) { this._portForwardingConfig.splice(index, 1); this._list.removeItem(index); this._update(); } /** * @override * @param {!Adb.PortForwardingRule} rule * @param {!UI.ListWidget.Editor} editor * @param {boolean} isNew */ commitEdit(rule, editor, isNew) { rule.port = editor.control('port').value.trim(); rule.address = editor.control('address').value.trim(); if (isNew) this._portForwardingConfig.push(rule); this._update(); } /** * @override * @param {!Adb.PortForwardingRule} rule * @return {!UI.ListWidget.Editor} */ beginEdit(rule) { const editor = this._createEditor(); editor.control('port').value = rule.port; editor.control('address').value = rule.address; return editor; } /** * @return {!UI.ListWidget.Editor<!Adb.PortForwardingRule>} */ _createEditor() { if (this._editor) return this._editor; const editor = new UI.ListWidget.Editor(); this._editor = editor; const content = editor.contentElement(); const fields = content.createChild('div', 'port-forwarding-edit-row'); fields.createChild('div', 'port-forwarding-value port-forwarding-port') .appendChild(editor.createInput('port', 'text', 'Device port (3333)', portValidator.bind(this))); fields.createChild('div', 'port-forwarding-separator port-forwarding-separator-invisible'); fields.createChild('div', 'port-forwarding-value') .appendChild(editor.createInput('address', 'text', 'Local address (dev.example.corp:3333)', addressValidator)); return editor; /** * @param {!Adb.PortForwardingRule} rule * @param {number} index * @param {!HTMLInputElement|!HTMLSelectElement} input * @this {Devices.DevicesView.PortForwardingView} * @return {boolean} */ function portValidator(rule, index, input) { const value = input.value.trim(); const match = value.match(/^(\d+)$/); if (!match) return false; const port = parseInt(match[1], 10); if (port < 1024 || port > 65535) return false; for (let i = 0; i < this._portForwardingConfig.length; ++i) { if (i !== index && this._portForwardingConfig[i].port === value) return false; } return true; } /** * @param {!Adb.PortForwardingRule} rule * @param {number} index * @param {!HTMLInputElement|!HTMLSelectElement} input * @return {boolean} */ function addressValidator(rule, index, input) { const match = input.value.trim().match(/^([a-zA-Z0-9\.\-_]+):(\d+)$/); if (!match) return false; const port = parseInt(match[2], 10); return port <= 65535; } } }; Devices.DevicesView.DeviceView = class extends UI.VBox { constructor() { super(); this.setMinimumSize(100, 100); this.contentElement.classList.add('device-view'); const topRow = this.contentElement.createChild('div', 'hbox device-text-row'); this._deviceTitle = topRow.createChild('div', 'view-title'); this._deviceSerial = topRow.createChild('div', 'device-serial'); this._portStatus = this.contentElement.createChild('div', 'device-port-status hidden'); this._deviceOffline = this.contentElement.createChild('div'); this._deviceOffline.textContent = Common.UIString('Pending authentication: please accept debugging session on the device.'); this._noBrowsers = this.contentElement.createChild('div'); this._noBrowsers.textContent = Common.UIString('No browsers detected.'); this._browsers = this.contentElement.createChild('div', 'device-browser-list vbox'); /** @type {!Map<string, !Devices.DevicesView.BrowserSection>} */ this._browserById = new Map(); /** @type {?string} */ this._cachedPortStatus = null; /** @type {?Adb.Device} */ this._device = null; } /** * @param {!Adb.Device} device */ update(device) { if (!this._device || this._device.adbModel !== device.adbModel) this._deviceTitle.textContent = device.adbModel; if (!this._device || this._device.adbSerial !== device.adbSerial) this._deviceSerial.textContent = '#' + device.adbSerial; this._deviceOffline.classList.toggle('hidden', device.adbConnected); this._noBrowsers.classList.toggle('hidden', !device.adbConnected || !!device.browsers.length); this._browsers.classList.toggle('hidden', !device.adbConnected || !device.browsers.length); const browserIds = new Set(); for (const browser of device.browsers) browserIds.add(browser.id); for (const browserId of this._browserById.keys()) { if (!browserIds.has(browserId)) { this._browserById.get(browserId).element.remove(); this._browserById.remove(browserId); } } for (const browser of device.browsers) { let section = this._browserById.get(browser.id); if (!section) { section = this._createBrowserSection(); this._browserById.set(browser.id, section); this._browsers.appendChild(section.element); } this._updateBrowserSection(section, browser); } this._device = device; } /** * @return {!Devices.DevicesView.BrowserSection} */ _createBrowserSection() { const element = createElementWithClass('div', 'vbox flex-none'); const topRow = element.createChild('div', ''); const title = topRow.createChild('div', 'device-browser-title'); const newTabRow = element.createChild('div', 'device-browser-new-tab'); newTabRow.createChild('div', '').textContent = Common.UIString('New tab:'); const newTabInput = UI.createInput('', 'text'); newTabRow.appendChild(newTabInput); newTabInput.placeholder = Common.UIString('Enter URL'); newTabInput.addEventListener('keydown', newTabKeyDown, false); const newTabButton = UI.createTextButton(Common.UIString('Open'), openNewTab); newTabRow.appendChild(newTabButton); const pages = element.createChild('div', 'device-page-list vbox'); const viewMore = element.createChild('div', 'device-view-more'); viewMore.addEventListener('click', viewMoreClick, false); updateViewMoreTitle(); const section = { browser: null, element: element, title: title, pages: pages, viewMore: viewMore, newTab: newTabRow, pageSections: new Map() }; return section; function viewMoreClick() { pages.classList.toggle('device-view-more-toggled'); updateViewMoreTitle(); } function updateViewMoreTitle() { viewMore.textContent = pages.classList.contains('device-view-more-toggled') ? Common.UIString('View less tabs\u2026') : Common.UIString('View more tabs\u2026'); } /** * @param {!Event} event */ function newTabKeyDown(event) { if (event.key === 'Enter') { event.consume(true); openNewTab(); } } function openNewTab() { if (section.browser) { InspectorFrontendHost.openRemotePage(section.browser.id, newTabInput.value.trim() || 'about:blank'); newTabInput.value = ''; } } } /** * @param {!Devices.DevicesView.BrowserSection} section * @param {!Adb.Browser} browser */ _updateBrowserSection(section, browser) { if (!section.browser || section.browser.adbBrowserName !== browser.adbBrowserName || section.browser.adbBrowserVersion !== browser.adbBrowserVersion) { if (browser.adbBrowserVersion) section.title.textContent = String.sprintf('%s (%s)', browser.adbBrowserName, browser.adbBrowserVersion); else section.title.textContent = browser.adbBrowserName; } const pageIds = new Set(); for (const page of browser.pages) pageIds.add(page.id); for (const pageId of section.pageSections.keys()) { if (!pageIds.has(pageId)) { section.pageSections.get(pageId).element.remove(); section.pageSections.remove(pageId); } } for (let index = 0; index < browser.pages.length; ++index) { const page = browser.pages[index]; let pageSection = section.pageSections.get(page.id); if (!pageSection) { pageSection = this._createPageSection(); section.pageSections.set(page.id, pageSection); section.pages.appendChild(pageSection.element); } this._updatePageSection(pageSection, page); if (!index && section.pages.firstChild !== pageSection.element) section.pages.insertBefore(pageSection.element, section.pages.firstChild); } const kViewMoreCount = 3; for (let index = 0, element = section.pages.firstChild; element; element = element.nextSibling, ++index) element.classList.toggle('device-view-more-page', index >= kViewMoreCount); section.viewMore.classList.toggle('device-needs-view-more', browser.pages.length > kViewMoreCount); section.newTab.classList.toggle('hidden', !browser.adbBrowserChromeVersion); section.browser = browser; } /** * @return {!Devices.DevicesView.PageSection} */ _createPageSection() { const element = createElementWithClass('div', 'vbox'); const titleRow = element.createChild('div', 'device-page-title-row'); const title = titleRow.createChild('div', 'device-page-title'); const inspect = UI.createTextButton(Common.UIString('Inspect'), doAction.bind(null, 'inspect')); titleRow.appendChild(inspect); const toolbar = new UI.Toolbar(''); toolbar.appendToolbarItem(new UI.ToolbarMenuButton(appendActions)); titleRow.appendChild(toolbar.element); const url = element.createChild('div', 'device-page-url'); const section = {page: null, element: element, title: title, url: url, inspect: inspect}; return section; /** * @param {!UI.ContextMenu} contextMenu */ function appendActions(contextMenu) { contextMenu.defaultSection().appendItem(Common.UIString('Reload'), doAction.bind(null, 'reload')); contextMenu.defaultSection().appendItem(Common.UIString('Focus'), doAction.bind(null, 'activate')); contextMenu.defaultSection().appendItem(Common.UIString('Close'), doAction.bind(null, 'close')); } /** * @param {string} action */ function doAction(action) { if (section.page) InspectorFrontendHost.performActionOnRemotePage(section.page.id, action); } } /** * @param {!Devices.DevicesView.PageSection} section * @param {!Adb.Page} page */ _updatePageSection(section, page) { if (!section.page || section.page.name !== page.name) { section.title.textContent = page.name; section.title.title = page.name; } if (!section.page || section.page.url !== page.url) { section.url.textContent = ''; section.url.appendChild(UI.XLink.create(page.url)); } section.inspect.disabled = page.attached; section.page = page; } /** * @param {!Adb.DevicePortForwardingStatus} status */ portForwardingStatusChanged(status) { const json = JSON.stringify(status); if (json === this._cachedPortStatus) return; this._cachedPortStatus = json; this._portStatus.removeChildren(); this._portStatus.createChild('div', 'device-port-status-text').textContent = Common.UIString('Port Forwarding:'); const connected = []; const transient = []; const error = []; let empty = true; for (const port in status.ports) { if (!status.ports.hasOwnProperty(port)) continue; empty = false; const portStatus = status.ports[port]; const portNumber = createElementWithClass('div', 'device-view-port-number monospace'); portNumber.textContent = ':' + port; if (portStatus >= 0) this._portStatus.appendChild(portNumber); else this._portStatus.insertBefore(portNumber, this._portStatus.firstChild); const portIcon = createElementWithClass('div', 'device-view-port-icon'); if (portStatus >= 0) { connected.push(port); } else if (portStatus === -1 || portStatus === -2) { portIcon.classList.add('device-view-port-icon-transient'); transient.push(port); } else if (portStatus < 0) { portIcon.classList.add('device-view-port-icon-error'); error.push(port); } this._portStatus.insertBefore(portIcon, portNumber); } const title = []; if (connected.length) title.push(Common.UIString('Connected: %s', connected.join(', '))); if (transient.length) title.push(Common.UIString('Transient: %s', transient.join(', '))); if (error.length) title.push(Common.UIString('Error: %s', error.join(', '))); this._portStatus.title = title.join('; '); this._portStatus.classList.toggle('hidden', empty); } }; /** @typedef {!{browser: ?Adb.Browser, element: !Element, title: !Element, pages: !Element, viewMore: !Element, newTab: !Element, pageSections: !Map<string, !Devices.DevicesView.PageSection>}} */ Devices.DevicesView.BrowserSection; /** @typedef {!{page: ?Adb.Page, element: !Element, title: !Element, url: !Element, inspect: !Element}} */ Devices.DevicesView.PageSection;