1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 var MIN_VERSION_TAB_CLOSE = 25;
6 var MIN_VERSION_TARGET_ID = 26;
7 var MIN_VERSION_NEW_TAB = 29;
8 var MIN_VERSION_TAB_ACTIVATE = 30;
10 function inspect(data) {
11 chrome.send('inspect', [data]);
14 function activate(data) {
15 chrome.send('activate', [data]);
18 function close(data) {
19 chrome.send('close', [data]);
22 function reload(data) {
23 chrome.send('reload', [data]);
26 function open(browserId, url) {
27 chrome.send('open', [browserId, url]);
30 function removeChildren(element_id) {
31 var element = $(element_id);
32 element.textContent = '';
36 var tabContents = document.querySelectorAll('#content > div');
37 for (var i = 0; i != tabContents.length; i++) {
38 var tabContent = tabContents[i];
39 var tabName = tabContent.querySelector('.content-header').textContent;
41 var tabHeader = document.createElement('div');
42 tabHeader.className = 'tab-header';
43 var button = document.createElement('button');
44 button.textContent = tabName;
45 tabHeader.appendChild(button);
46 tabHeader.addEventListener('click', selectTab.bind(null, tabContent.id));
47 $('navigation').appendChild(tabHeader);
49 var selectedTabName = window.location.hash.slice(1) || 'devices';
50 selectTab(selectedTabName);
52 chrome.send('init-ui');
55 function selectTab(id) {
56 var tabContents = document.querySelectorAll('#content > div');
57 var tabHeaders = $('navigation').querySelectorAll('.tab-header');
58 for (var i = 0; i != tabContents.length; i++) {
59 var tabContent = tabContents[i];
60 var tabHeader = tabHeaders[i];
61 if (tabContent.id == id) {
62 tabContent.classList.add('selected');
63 tabHeader.classList.add('selected');
65 tabContent.classList.remove('selected');
66 tabHeader.classList.remove('selected');
69 window.location.hash = id;
72 function populateWebContentsTargets(data) {
73 removeChildren('pages-list');
74 removeChildren('extensions-list');
75 removeChildren('apps-list');
76 removeChildren('others-list');
78 for (var i = 0; i < data.length; i++) {
79 if (data[i].type === 'page')
80 addToPagesList(data[i]);
81 else if (data[i].type === 'background_page')
82 addToExtensionsList(data[i]);
83 else if (data[i].type === 'app')
84 addToAppsList(data[i]);
86 addToOthersList(data[i]);
90 function populateWorkerTargets(data) {
91 removeChildren('workers-list');
93 for (var i = 0; i < data.length; i++)
94 addToWorkersList(data[i]);
97 function populateRemoteTargets(devices) {
102 window.holdDevices = devices;
106 function alreadyDisplayed(element, data) {
107 var json = JSON.stringify(data);
108 if (element.cachedJSON == json)
110 element.cachedJSON = json;
114 function insertChildSortedById(parent, child) {
115 for (var sibling = parent.firstElementChild;
117 sibling = sibling.nextElementSibling) {
118 if (sibling.id > child.id) {
119 parent.insertBefore(child, sibling);
123 parent.appendChild(child);
126 var deviceList = $('devices-list');
127 if (alreadyDisplayed(deviceList, devices))
130 function removeObsolete(validIds, section) {
131 if (validIds.indexOf(section.id) < 0)
135 var newDeviceIds = devices.map(function(d) { return d.adbGlobalId });
136 Array.prototype.forEach.call(
137 deviceList.querySelectorAll('.device'),
138 removeObsolete.bind(null, newDeviceIds));
140 for (var d = 0; d < devices.length; d++) {
141 var device = devices[d];
143 var deviceSection = $(device.adbGlobalId);
144 if (!deviceSection) {
145 deviceSection = document.createElement('div');
146 deviceSection.id = device.adbGlobalId;
147 deviceSection.className = 'device';
148 deviceList.appendChild(deviceSection);
150 var deviceHeader = document.createElement('div');
151 deviceHeader.className = 'device-header';
152 deviceSection.appendChild(deviceHeader);
154 var deviceName = document.createElement('div');
155 deviceName.className = 'device-name';
156 deviceHeader.appendChild(deviceName);
158 if (device.adbSerial) {
159 var deviceSerial = document.createElement('div');
160 deviceSerial.className = 'device-serial';
161 deviceSerial.textContent = '#' + device.adbSerial.toUpperCase();
162 deviceHeader.appendChild(deviceSerial);
165 var devicePorts = document.createElement('div');
166 devicePorts.className = 'device-ports';
167 deviceHeader.appendChild(devicePorts);
169 var browserList = document.createElement('div');
170 browserList.className = 'browsers';
171 deviceSection.appendChild(browserList);
173 var authenticating = document.createElement('div');
174 authenticating.className = 'device-auth';
175 deviceSection.appendChild(authenticating);
178 if (alreadyDisplayed(deviceSection, device))
181 deviceSection.querySelector('.device-name').textContent = device.adbModel;
182 deviceSection.querySelector('.device-auth').textContent =
183 device.adbConnected ? '' : 'Pending authentication: please accept ' +
184 'debugging session on the device.';
186 var devicePorts = deviceSection.querySelector('.device-ports');
187 devicePorts.textContent = '';
188 if (device.adbPortStatus) {
189 for (var port in device.adbPortStatus) {
190 var status = device.adbPortStatus[port];
191 var portIcon = document.createElement('div');
192 portIcon.className = 'port-icon';
194 portIcon.classList.add('connected');
195 else if (status == -1 || status == -2)
196 portIcon.classList.add('transient');
198 portIcon.classList.add('error');
199 devicePorts.appendChild(portIcon);
201 var portNumber = document.createElement('div');
202 portNumber.className = 'port-number';
203 portNumber.textContent = ':' + port;
205 portNumber.textContent += '(' + status + ')';
206 devicePorts.appendChild(portNumber);
210 var browserList = deviceSection.querySelector('.browsers');
212 device.browsers.map(function(b) { return b.adbGlobalId });
213 Array.prototype.forEach.call(
214 browserList.querySelectorAll('.browser'),
215 removeObsolete.bind(null, newBrowserIds));
217 for (var b = 0; b < device.browsers.length; b++) {
218 var browser = device.browsers[b];
220 var isChrome = browser.adbBrowserProduct &&
221 browser.adbBrowserProduct.match(/^Chrome/);
223 var majorChromeVersion = 0;
224 if (isChrome && browser.adbBrowserVersion) {
225 var match = browser.adbBrowserVersion.match(/^(\d+)/);
227 majorChromeVersion = parseInt(match[1]);
231 var browserSection = $(browser.adbGlobalId);
232 if (browserSection) {
233 pageList = browserSection.querySelector('.pages');
235 browserSection = document.createElement('div');
236 browserSection.id = browser.adbGlobalId;
237 browserSection.className = 'browser';
238 insertChildSortedById(browserList, browserSection);
240 var browserHeader = document.createElement('div');
241 browserHeader.className = 'browser-header';
243 var browserName = document.createElement('div');
244 browserName.className = 'browser-name';
245 browserHeader.appendChild(browserName);
246 if (browser.adbBrowserPackage && !isChrome)
247 browserName.textContent = browser.adbBrowserPackage;
249 browserName.textContent = browser.adbBrowserProduct;
250 if (browser.adbBrowserVersion)
251 browserName.textContent += ' (' + browser.adbBrowserVersion + ')';
252 browserSection.appendChild(browserHeader);
254 if (majorChromeVersion >= MIN_VERSION_NEW_TAB) {
255 var newPage = document.createElement('div');
256 newPage.className = 'open';
258 var newPageUrl = document.createElement('input');
259 newPageUrl.type = 'text';
260 newPageUrl.placeholder = 'Open tab with url';
261 newPage.appendChild(newPageUrl);
263 var openHandler = function(browserId, input) {
264 open(browserId, input.value || 'about:blank');
266 }.bind(null, browser.adbGlobalId, newPageUrl);
267 newPageUrl.addEventListener('keyup', function(handler, event) {
268 if (event.keyIdentifier == 'Enter' && event.target.value)
270 }.bind(null, openHandler), true);
272 var newPageButton = document.createElement('button');
273 newPageButton.textContent = 'Open';
274 newPage.appendChild(newPageButton);
275 newPageButton.addEventListener('click', openHandler, true);
277 browserHeader.appendChild(newPage);
280 pageList = document.createElement('div');
281 pageList.className = 'list pages';
282 browserSection.appendChild(pageList);
285 if (alreadyDisplayed(browserSection, browser))
288 pageList.textContent = '';
289 for (var p = 0; p < browser.pages.length; p++) {
290 var page = browser.pages[p];
291 // Attached targets have no unique id until Chrome 26. For such targets
292 // it is impossible to activate existing DevTools window.
293 page.hasNoUniqueId = page.attached &&
294 majorChromeVersion < MIN_VERSION_TARGET_ID;
295 var row = addTargetToList(page, pageList, ['name', 'url']);
296 if (page['description'])
297 addWebViewDetails(row, page);
299 addFavicon(row, page);
301 if (majorChromeVersion >= MIN_VERSION_TAB_ACTIVATE) {
302 addActionLink(row, 'focus tab', activate.bind(null, page), false);
304 addActionLink(row, 'reload', reload.bind(null, page), page.attached);
305 if (majorChromeVersion >= MIN_VERSION_TAB_CLOSE) {
307 row, 'close', close.bind(null, page), page.attached);
315 function addToPagesList(data) {
316 var row = addTargetToList(data, $('pages-list'), ['name', 'url']);
317 addFavicon(row, data);
320 function addToExtensionsList(data) {
321 var row = addTargetToList(data, $('extensions-list'), ['name', 'url']);
322 addFavicon(row, data);
325 function addToAppsList(data) {
326 var row = addTargetToList(data, $('apps-list'), ['name', 'url']);
327 addFavicon(row, data);
329 Array.prototype.forEach.call(data.guests, function(guest) {
330 var guestRow = addTargetToList(guest, row, ['name', 'url']);
331 guestRow.classList.add('guest');
332 addFavicon(guestRow, guest);
337 function addToWorkersList(data) {
339 addTargetToList(data, $('workers-list'), ['name', 'description', 'url']);
340 addActionLink(row, 'terminate', close.bind(null, data), data.attached);
343 function addToOthersList(data) {
344 addTargetToList(data, $('others-list'), ['url']);
347 function formatValue(data, property) {
348 var value = data[property];
350 if (property == 'name' && value == '') {
354 var text = value ? String(value) : '';
355 if (text.length > 100)
356 text = text.substring(0, 100) + '\u2026';
358 var span = document.createElement('div');
359 span.textContent = text;
360 span.className = property;
364 function addFavicon(row, data) {
365 var favicon = document.createElement('img');
366 if (data['faviconUrl'])
367 favicon.src = data['faviconUrl'];
368 row.insertBefore(favicon, row.firstChild);
371 function addWebViewDetails(row, data) {
374 webview = JSON.parse(data['description']);
378 addWebViewDescription(row, webview);
379 if (data.adbScreenWidth && data.adbScreenHeight)
381 row, webview, data.adbScreenWidth, data.adbScreenHeight);
384 function addWebViewDescription(row, webview) {
385 var viewStatus = { visibility: '', position: '', size: '' };
386 if (!webview.empty) {
387 if (webview.attached && !webview.visible)
388 viewStatus.visibility = 'hidden';
389 else if (!webview.attached)
390 viewStatus.visibility = 'detached';
391 viewStatus.size = 'size ' + webview.width + ' \u00d7 ' + webview.height;
393 viewStatus.visibility = 'empty';
395 if (webview.attached) {
396 viewStatus.position =
397 'at (' + webview.screenX + ', ' + webview.screenY + ')';
400 var subRow = document.createElement('div');
401 subRow.className = 'subrow webview';
402 if (webview.empty || !webview.attached || !webview.visible)
403 subRow.className += ' invisible-view';
404 if (viewStatus.visibility)
405 subRow.appendChild(formatValue(viewStatus, 'visibility'));
406 subRow.appendChild(formatValue(viewStatus, 'position'));
407 subRow.appendChild(formatValue(viewStatus, 'size'));
408 var mainSubrow = row.querySelector('.subrow.main');
409 if (mainSubrow.nextSibling)
410 mainSubrow.parentNode.insertBefore(subRow, mainSubrow.nextSibling);
412 mainSubrow.parentNode.appendChild(subRow);
415 function addWebViewThumbnail(row, webview, screenWidth, screenHeight) {
416 var maxScreenRectSize = 50;
418 var screenRectHeight;
420 var aspectRatio = screenWidth / screenHeight;
421 if (aspectRatio < 1) {
422 screenRectWidth = Math.round(maxScreenRectSize * aspectRatio);
423 screenRectHeight = maxScreenRectSize;
425 screenRectWidth = maxScreenRectSize;
426 screenRectHeight = Math.round(maxScreenRectSize / aspectRatio);
429 var thumbnail = document.createElement('div');
430 thumbnail.className = 'webview-thumbnail';
431 var thumbnailWidth = 3 * screenRectWidth;
432 var thumbnailHeight = 60;
433 thumbnail.style.width = thumbnailWidth + 'px';
434 thumbnail.style.height = thumbnailHeight + 'px';
436 var screenRect = document.createElement('div');
437 screenRect.className = 'screen-rect';
438 screenRect.style.left = screenRectWidth + 'px';
439 screenRect.style.top = (thumbnailHeight - screenRectHeight) / 2 + 'px';
440 screenRect.style.width = screenRectWidth + 'px';
441 screenRect.style.height = screenRectHeight + 'px';
442 thumbnail.appendChild(screenRect);
444 if (!webview.empty && webview.attached) {
445 var viewRect = document.createElement('div');
446 viewRect.className = 'view-rect';
447 if (!webview.visible)
448 viewRect.classList.add('hidden');
449 function percent(ratio) {
450 return ratio * 100 + '%';
452 viewRect.style.left = percent(webview.screenX / screenWidth);
453 viewRect.style.top = percent(webview.screenY / screenHeight);
454 viewRect.style.width = percent(webview.width / screenWidth);
455 viewRect.style.height = percent(webview.height / screenHeight);
456 screenRect.appendChild(viewRect);
459 row.insertBefore(thumbnail, row.firstChild);
462 function addTargetToList(data, list, properties) {
463 var row = document.createElement('div');
464 row.className = 'row';
466 var subrowBox = document.createElement('div');
467 subrowBox.className = 'subrow-box';
468 row.appendChild(subrowBox);
470 var subrow = document.createElement('div');
471 subrow.className = 'subrow main';
472 subrowBox.appendChild(subrow);
474 var description = null;
475 for (var j = 0; j < properties.length; j++)
476 subrow.appendChild(formatValue(data, properties[j]));
479 addWebViewDescription(description, subrowBox);
481 var actionBox = document.createElement('div');
482 actionBox.className = 'actions';
483 subrowBox.appendChild(actionBox);
485 addActionLink(row, 'inspect', inspect.bind(null, data),
486 data.hasNoUniqueId || data.adbAttachedForeign);
488 list.appendChild(row);
492 function addActionLink(row, text, handler, opt_disabled) {
493 var link = document.createElement('a');
495 link.classList.add('disabled');
497 link.classList.remove('disabled');
499 link.setAttribute('href', '#');
500 link.textContent = text;
501 link.addEventListener('click', handler, true);
502 row.querySelector('.actions').appendChild(link);
506 function initSettings() {
507 $('discover-usb-devices-enable').addEventListener('change',
508 enableDiscoverUsbDevices);
510 $('port-forwarding-enable').addEventListener('change', enablePortForwarding);
511 $('port-forwarding-config-open').addEventListener(
512 'click', openPortForwardingConfig);
513 $('port-forwarding-config-close').addEventListener(
514 'click', closePortForwardingConfig);
515 $('port-forwarding-config-done').addEventListener(
516 'click', commitPortForwardingConfig.bind(true));
519 function enableDiscoverUsbDevices(event) {
520 chrome.send('set-discover-usb-devices-enabled', [event.target.checked]);
523 function enablePortForwarding(event) {
524 chrome.send('set-port-forwarding-enabled', [event.target.checked]);
527 function handleKey(event) {
528 switch (event.keyCode) {
530 if (event.target.nodeName == 'INPUT') {
531 var line = event.target.parentNode;
532 if (!line.classList.contains('fresh') ||
533 line.classList.contains('empty')) {
534 commitPortForwardingConfig(true);
536 commitFreshLineIfValid(true /* select new line */);
537 commitPortForwardingConfig(false);
540 commitPortForwardingConfig(true);
545 commitPortForwardingConfig(true);
550 function setModal(dialog) {
551 dialog.deactivatedNodes = Array.prototype.filter.call(
552 document.querySelectorAll('*'),
554 return n != dialog && !dialog.contains(n) && n.tabIndex >= 0;
557 dialog.tabIndexes = dialog.deactivatedNodes.map(
558 function(n) { return n.getAttribute('tabindex'); });
560 dialog.deactivatedNodes.forEach(function(n) { n.tabIndex = -1; });
561 window.modal = dialog;
564 function unsetModal(dialog) {
565 for (var i = 0; i < dialog.deactivatedNodes.length; i++) {
566 var node = dialog.deactivatedNodes[i];
567 if (dialog.tabIndexes[i] === null)
568 node.removeAttribute('tabindex');
570 node.setAttribute('tabindex', tabIndexes[i]);
573 if (window.holdDevices) {
574 populateDeviceLists(window.holdDevices);
575 delete window.holdDevices;
578 delete dialog.deactivatedNodes;
579 delete dialog.tabIndexes;
583 function openPortForwardingConfig() {
584 loadPortForwardingConfig(window.portForwardingConfig);
586 $('port-forwarding-overlay').classList.add('open');
587 document.addEventListener('keyup', handleKey);
589 var freshPort = document.querySelector('.fresh .port');
593 $('port-forwarding-config-done').focus();
595 setModal($('port-forwarding-overlay'));
598 function closePortForwardingConfig() {
599 $('port-forwarding-overlay').classList.remove('open');
600 document.removeEventListener('keyup', handleKey);
601 unsetModal($('port-forwarding-overlay'));
604 function loadPortForwardingConfig(config) {
605 var list = $('port-forwarding-config-list');
606 list.textContent = '';
607 for (var port in config)
608 list.appendChild(createConfigLine(port, config[port]));
609 list.appendChild(createEmptyConfigLine());
612 function commitPortForwardingConfig(closeConfig) {
614 closePortForwardingConfig();
616 commitFreshLineIfValid();
617 var lines = document.querySelectorAll('.port-forwarding-pair');
619 for (var i = 0; i != lines.length; i++) {
621 var portInput = line.querySelector('.port');
622 var locationInput = line.querySelector('.location');
624 var port = portInput.classList.contains('invalid') ?
625 portInput.lastValidValue :
628 var location = locationInput.classList.contains('invalid') ?
629 locationInput.lastValidValue :
632 if (port && location)
633 config[port] = location;
635 chrome.send('set-port-forwarding-config', [config]);
638 function updateDiscoverUsbDevicesEnabled(enabled) {
639 var checkbox = $('discover-usb-devices-enable');
640 checkbox.checked = !!enabled;
641 checkbox.disabled = false;
644 function updatePortForwardingEnabled(enabled) {
645 var checkbox = $('port-forwarding-enable');
646 checkbox.checked = !!enabled;
647 checkbox.disabled = false;
650 function updatePortForwardingConfig(config) {
651 window.portForwardingConfig = config;
652 $('port-forwarding-config-open').disabled = !config;
655 function createConfigLine(port, location) {
656 var line = document.createElement('div');
657 line.className = 'port-forwarding-pair';
659 var portInput = createConfigField(port, 'port', 'Port', validatePort);
660 line.appendChild(portInput);
662 var locationInput = createConfigField(
663 location, 'location', 'IP address and port', validateLocation);
664 line.appendChild(locationInput);
665 locationInput.addEventListener('keydown', function(e) {
666 if (e.keyIdentifier == 'U+0009' && // Tab
667 !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey &&
668 line.classList.contains('fresh') &&
669 !line.classList.contains('empty')) {
670 // Tabbing forward on the fresh line, try create a new empty one.
671 commitFreshLineIfValid(true);
676 var lineDelete = document.createElement('div');
677 lineDelete.className = 'close-button';
678 lineDelete.addEventListener('click', function() {
679 var newSelection = line.nextElementSibling;
680 line.parentNode.removeChild(line);
681 selectLine(newSelection);
683 line.appendChild(lineDelete);
685 line.addEventListener('click', selectLine.bind(null, line));
686 line.addEventListener('focus', selectLine.bind(null, line));
688 checkEmptyLine(line);
693 function validatePort(input) {
694 var match = input.value.match(/^(\d+)$/);
697 var port = parseInt(match[1]);
698 if (port < 5000 || 10000 < port)
701 var inputs = document.querySelectorAll('input.port:not(.invalid)');
702 for (var i = 0; i != inputs.length; ++i) {
703 if (inputs[i] == input)
705 if (parseInt(inputs[i].value) == port)
711 function validateLocation(input) {
712 var match = input.value.match(/^([a-zA-Z0-9\.]+):(\d+)$/);
715 var port = parseInt(match[2]);
716 return port <= 10000;
719 function createEmptyConfigLine() {
720 var line = createConfigLine('', '');
721 line.classList.add('fresh');
725 function createConfigField(value, className, hint, validate) {
726 var input = document.createElement('input');
727 input.className = className;
729 input.placeholder = hint;
731 input.lastValidValue = value;
733 function checkInput() {
735 input.classList.remove('invalid');
737 input.classList.add('invalid');
738 if (input.parentNode)
739 checkEmptyLine(input.parentNode);
743 input.addEventListener('keyup', checkInput);
744 input.addEventListener('focus', function() {
745 selectLine(input.parentNode);
748 input.addEventListener('blur', function() {
750 input.lastValidValue = input.value;
756 function checkEmptyLine(line) {
757 var inputs = line.querySelectorAll('input');
759 for (var i = 0; i != inputs.length; i++) {
760 if (inputs[i].value != '')
764 line.classList.add('empty');
766 line.classList.remove('empty');
769 function selectLine(line) {
770 if (line.classList.contains('selected'))
773 line.classList.add('selected');
776 function unselectLine() {
777 var line = document.querySelector('.port-forwarding-pair.selected');
780 line.classList.remove('selected');
781 commitFreshLineIfValid();
784 function commitFreshLineIfValid(opt_selectNew) {
785 var line = document.querySelector('.port-forwarding-pair.fresh');
786 if (line.querySelector('.invalid'))
788 line.classList.remove('fresh');
789 var freshLine = createEmptyConfigLine();
790 line.parentNode.appendChild(freshLine);
792 freshLine.querySelector('.port').focus();
795 document.addEventListener('DOMContentLoaded', onload);