toolbox.js 78 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. const MAX_ORDINAL = 99;
  6. const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsoleEnabled";
  7. const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight";
  8. const OS_HISTOGRAM = "DEVTOOLS_OS_ENUMERATED_PER_USER";
  9. const OS_IS_64_BITS = "DEVTOOLS_OS_IS_64_BITS_PER_USER";
  10. const HOST_HISTOGRAM = "DEVTOOLS_TOOLBOX_HOST";
  11. const SCREENSIZE_HISTOGRAM = "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER";
  12. const HTML_NS = "http://www.w3.org/1999/xhtml";
  13. const { SourceMapService } = require("./source-map-service");
  14. var {Ci, Cu} = require("chrome");
  15. var promise = require("promise");
  16. var defer = require("devtools/shared/defer");
  17. var Services = require("Services");
  18. var {Task} = require("devtools/shared/task");
  19. var {gDevTools} = require("devtools/client/framework/devtools");
  20. var EventEmitter = require("devtools/shared/event-emitter");
  21. var Telemetry = require("devtools/client/shared/telemetry");
  22. var { HUDService } = require("devtools/client/webconsole/hudservice");
  23. var viewSource = require("devtools/client/shared/view-source");
  24. var { attachThread, detachThread } = require("./attach-thread");
  25. var Menu = require("devtools/client/framework/menu");
  26. var MenuItem = require("devtools/client/framework/menu-item");
  27. var { DOMHelpers } = require("resource://devtools/client/shared/DOMHelpers.jsm");
  28. const { KeyCodes } = require("devtools/client/shared/keycodes");
  29. const { BrowserLoader } =
  30. Cu.import("resource://devtools/client/shared/browser-loader.js", {});
  31. const {LocalizationHelper} = require("devtools/shared/l10n");
  32. const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
  33. loader.lazyRequireGetter(this, "CommandUtils",
  34. "devtools/client/shared/developer-toolbar", true);
  35. loader.lazyRequireGetter(this, "getHighlighterUtils",
  36. "devtools/client/framework/toolbox-highlighter-utils", true);
  37. loader.lazyRequireGetter(this, "Selection",
  38. "devtools/client/framework/selection", true);
  39. loader.lazyRequireGetter(this, "InspectorFront",
  40. "devtools/shared/fronts/inspector", true);
  41. loader.lazyRequireGetter(this, "flags",
  42. "devtools/shared/flags");
  43. loader.lazyRequireGetter(this, "showDoorhanger",
  44. "devtools/client/shared/doorhanger", true);
  45. loader.lazyRequireGetter(this, "createPerformanceFront",
  46. "devtools/shared/fronts/performance", true);
  47. loader.lazyRequireGetter(this, "system",
  48. "devtools/shared/system");
  49. loader.lazyRequireGetter(this, "getPreferenceFront",
  50. "devtools/shared/fronts/preference", true);
  51. loader.lazyRequireGetter(this, "KeyShortcuts",
  52. "devtools/client/shared/key-shortcuts", true);
  53. loader.lazyRequireGetter(this, "ZoomKeys",
  54. "devtools/client/shared/zoom-keys");
  55. loader.lazyRequireGetter(this, "settleAll",
  56. "devtools/shared/ThreadSafeDevToolsUtils", true);
  57. loader.lazyRequireGetter(this, "ToolboxButtons",
  58. "devtools/client/definitions", true);
  59. loader.lazyGetter(this, "registerHarOverlay", () => {
  60. return require("devtools/client/netmonitor/har/toolbox-overlay").register;
  61. });
  62. /**
  63. * A "Toolbox" is the component that holds all the tools for one specific
  64. * target. Visually, it's a document that includes the tools tabs and all
  65. * the iframes where the tool panels will be living in.
  66. *
  67. * @param {object} target
  68. * The object the toolbox is debugging.
  69. * @param {string} selectedTool
  70. * Tool to select initially
  71. * @param {Toolbox.HostType} hostType
  72. * Type of host that will host the toolbox (e.g. sidebar, window)
  73. * @param {DOMWindow} contentWindow
  74. * The window object of the toolbox document
  75. * @param {string} frameId
  76. * A unique identifier to differentiate toolbox documents from the
  77. * chrome codebase when passing DOM messages
  78. */
  79. function Toolbox(target, selectedTool, hostType, contentWindow, frameId) {
  80. this._target = target;
  81. this._win = contentWindow;
  82. this.frameId = frameId;
  83. this._toolPanels = new Map();
  84. this._telemetry = new Telemetry();
  85. if (Services.prefs.getBoolPref("devtools.sourcemap.locations.enabled")) {
  86. this._sourceMapService = new SourceMapService(this._target);
  87. }
  88. this._initInspector = null;
  89. this._inspector = null;
  90. // Map of frames (id => frame-info) and currently selected frame id.
  91. this.frameMap = new Map();
  92. this.selectedFrameId = null;
  93. this._toolRegistered = this._toolRegistered.bind(this);
  94. this._toolUnregistered = this._toolUnregistered.bind(this);
  95. this._refreshHostTitle = this._refreshHostTitle.bind(this);
  96. this._toggleAutohide = this._toggleAutohide.bind(this);
  97. this.showFramesMenu = this.showFramesMenu.bind(this);
  98. this._updateFrames = this._updateFrames.bind(this);
  99. this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this);
  100. this.destroy = this.destroy.bind(this);
  101. this.highlighterUtils = getHighlighterUtils(this);
  102. this._highlighterReady = this._highlighterReady.bind(this);
  103. this._highlighterHidden = this._highlighterHidden.bind(this);
  104. this._prefChanged = this._prefChanged.bind(this);
  105. this._saveSplitConsoleHeight = this._saveSplitConsoleHeight.bind(this);
  106. this._onFocus = this._onFocus.bind(this);
  107. this._onBrowserMessage = this._onBrowserMessage.bind(this);
  108. this._showDevEditionPromo = this._showDevEditionPromo.bind(this);
  109. this._updateTextBoxMenuItems = this._updateTextBoxMenuItems.bind(this);
  110. this._onBottomHostMinimized = this._onBottomHostMinimized.bind(this);
  111. this._onBottomHostMaximized = this._onBottomHostMaximized.bind(this);
  112. this._onToolSelectWhileMinimized = this._onToolSelectWhileMinimized.bind(this);
  113. this._onPerformanceFrontEvent = this._onPerformanceFrontEvent.bind(this);
  114. this._onBottomHostWillChange = this._onBottomHostWillChange.bind(this);
  115. this._toggleMinimizeMode = this._toggleMinimizeMode.bind(this);
  116. this._onTabbarFocus = this._onTabbarFocus.bind(this);
  117. this._onTabbarArrowKeypress = this._onTabbarArrowKeypress.bind(this);
  118. this._onPickerClick = this._onPickerClick.bind(this);
  119. this._onPickerKeypress = this._onPickerKeypress.bind(this);
  120. this._onPickerStarted = this._onPickerStarted.bind(this);
  121. this._onPickerStopped = this._onPickerStopped.bind(this);
  122. this._target.on("close", this.destroy);
  123. if (!selectedTool) {
  124. selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
  125. }
  126. this._defaultToolId = selectedTool;
  127. this._hostType = hostType;
  128. EventEmitter.decorate(this);
  129. this._target.on("navigate", this._refreshHostTitle);
  130. this._target.on("frame-update", this._updateFrames);
  131. this.on("host-changed", this._refreshHostTitle);
  132. this.on("select", this._refreshHostTitle);
  133. this.on("ready", this._showDevEditionPromo);
  134. gDevTools.on("tool-registered", this._toolRegistered);
  135. gDevTools.on("tool-unregistered", this._toolUnregistered);
  136. this.on("picker-started", this._onPickerStarted);
  137. this.on("picker-stopped", this._onPickerStopped);
  138. }
  139. exports.Toolbox = Toolbox;
  140. /**
  141. * The toolbox can be 'hosted' either embedded in a browser window
  142. * or in a separate window.
  143. */
  144. Toolbox.HostType = {
  145. BOTTOM: "bottom",
  146. SIDE: "side",
  147. WINDOW: "window",
  148. CUSTOM: "custom"
  149. };
  150. Toolbox.prototype = {
  151. _URL: "about:devtools-toolbox",
  152. _prefs: {
  153. LAST_TOOL: "devtools.toolbox.selectedTool",
  154. SIDE_ENABLED: "devtools.toolbox.sideEnabled",
  155. },
  156. currentToolId: null,
  157. lastUsedToolId: null,
  158. /**
  159. * Returns a *copy* of the _toolPanels collection.
  160. *
  161. * @return {Map} panels
  162. * All the running panels in the toolbox
  163. */
  164. getToolPanels: function () {
  165. return new Map(this._toolPanels);
  166. },
  167. /**
  168. * Access the panel for a given tool
  169. */
  170. getPanel: function (id) {
  171. return this._toolPanels.get(id);
  172. },
  173. /**
  174. * Get the panel instance for a given tool once it is ready.
  175. * If the tool is already opened, the promise will resolve immediately,
  176. * otherwise it will wait until the tool has been opened before resolving.
  177. *
  178. * Note that this does not open the tool, use selectTool if you'd
  179. * like to select the tool right away.
  180. *
  181. * @param {String} id
  182. * The id of the panel, for example "jsdebugger".
  183. * @returns Promise
  184. * A promise that resolves once the panel is ready.
  185. */
  186. getPanelWhenReady: function (id) {
  187. let deferred = defer();
  188. let panel = this.getPanel(id);
  189. if (panel) {
  190. deferred.resolve(panel);
  191. } else {
  192. this.on(id + "-ready", (e, initializedPanel) => {
  193. deferred.resolve(initializedPanel);
  194. });
  195. }
  196. return deferred.promise;
  197. },
  198. /**
  199. * This is a shortcut for getPanel(currentToolId) because it is much more
  200. * likely that we're going to want to get the panel that we've just made
  201. * visible
  202. */
  203. getCurrentPanel: function () {
  204. return this._toolPanels.get(this.currentToolId);
  205. },
  206. /**
  207. * Get/alter the target of a Toolbox so we're debugging something different.
  208. * See Target.jsm for more details.
  209. * TODO: Do we allow |toolbox.target = null;| ?
  210. */
  211. get target() {
  212. return this._target;
  213. },
  214. get threadClient() {
  215. return this._threadClient;
  216. },
  217. /**
  218. * Get/alter the host of a Toolbox, i.e. is it in browser or in a separate
  219. * tab. See HostType for more details.
  220. */
  221. get hostType() {
  222. return this._hostType;
  223. },
  224. /**
  225. * Shortcut to the window containing the toolbox UI
  226. */
  227. get win() {
  228. return this._win;
  229. },
  230. /**
  231. * Shortcut to the document containing the toolbox UI
  232. */
  233. get doc() {
  234. return this.win.document;
  235. },
  236. /**
  237. * Get the toolbox highlighter front. Note that it may not always have been
  238. * initialized first. Use `initInspector()` if needed.
  239. * Consider using highlighterUtils instead, it exposes the highlighter API in
  240. * a useful way for the toolbox panels
  241. */
  242. get highlighter() {
  243. return this._highlighter;
  244. },
  245. /**
  246. * Get the toolbox's performance front. Note that it may not always have been
  247. * initialized first. Use `initPerformance()` if needed.
  248. */
  249. get performance() {
  250. return this._performance;
  251. },
  252. /**
  253. * Get the toolbox's inspector front. Note that it may not always have been
  254. * initialized first. Use `initInspector()` if needed.
  255. */
  256. get inspector() {
  257. return this._inspector;
  258. },
  259. /**
  260. * Get the toolbox's walker front. Note that it may not always have been
  261. * initialized first. Use `initInspector()` if needed.
  262. */
  263. get walker() {
  264. return this._walker;
  265. },
  266. /**
  267. * Get the toolbox's node selection. Note that it may not always have been
  268. * initialized first. Use `initInspector()` if needed.
  269. */
  270. get selection() {
  271. return this._selection;
  272. },
  273. /**
  274. * Get the toggled state of the split console
  275. */
  276. get splitConsole() {
  277. return this._splitConsole;
  278. },
  279. /**
  280. * Get the focused state of the split console
  281. */
  282. isSplitConsoleFocused: function () {
  283. if (!this._splitConsole) {
  284. return false;
  285. }
  286. let focusedWin = Services.focus.focusedWindow;
  287. return focusedWin && focusedWin ===
  288. this.doc.querySelector("#toolbox-panel-iframe-webconsole").contentWindow;
  289. },
  290. /**
  291. * Open the toolbox
  292. */
  293. open: function () {
  294. return Task.spawn(function* () {
  295. this.browserRequire = BrowserLoader({
  296. window: this.doc.defaultView,
  297. useOnlyShared: true
  298. }).require;
  299. if (this.win.location.href.startsWith(this._URL)) {
  300. // Update the URL so that onceDOMReady watch for the right url.
  301. this._URL = this.win.location.href;
  302. }
  303. let domReady = defer();
  304. let domHelper = new DOMHelpers(this.win);
  305. domHelper.onceDOMReady(() => {
  306. domReady.resolve();
  307. }, this._URL);
  308. // Optimization: fire up a few other things before waiting on
  309. // the iframe being ready (makes startup faster)
  310. // Load the toolbox-level actor fronts and utilities now
  311. yield this._target.makeRemote();
  312. // Attach the thread
  313. this._threadClient = yield attachThread(this);
  314. yield domReady.promise;
  315. this.isReady = true;
  316. let framesPromise = this._listFrames();
  317. this.closeButton = this.doc.getElementById("toolbox-close");
  318. this.closeButton.addEventListener("click", this.destroy, true);
  319. gDevTools.on("pref-changed", this._prefChanged);
  320. let framesMenu = this.doc.getElementById("command-button-frames");
  321. framesMenu.addEventListener("click", this.showFramesMenu, false);
  322. let noautohideMenu = this.doc.getElementById("command-button-noautohide");
  323. noautohideMenu.addEventListener("click", this._toggleAutohide, true);
  324. this.textBoxContextMenuPopup =
  325. this.doc.getElementById("toolbox-textbox-context-popup");
  326. this.textBoxContextMenuPopup.addEventListener("popupshowing",
  327. this._updateTextBoxMenuItems, true);
  328. this.shortcuts = new KeyShortcuts({
  329. window: this.doc.defaultView
  330. });
  331. this._buildDockButtons();
  332. this._buildOptions();
  333. this._buildTabs();
  334. this._applyCacheSettings();
  335. this._applyServiceWorkersTestingSettings();
  336. this._addKeysToWindow();
  337. this._addReloadKeys();
  338. this._addHostListeners();
  339. this._registerOverlays();
  340. if (!this._hostOptions || this._hostOptions.zoom === true) {
  341. ZoomKeys.register(this.win);
  342. }
  343. this.tabbar = this.doc.querySelector(".devtools-tabbar");
  344. this.tabbar.addEventListener("focus", this._onTabbarFocus, true);
  345. this.tabbar.addEventListener("click", this._onTabbarFocus, true);
  346. this.tabbar.addEventListener("keypress", this._onTabbarArrowKeypress);
  347. this.webconsolePanel = this.doc.querySelector("#toolbox-panel-webconsole");
  348. this.webconsolePanel.height = Services.prefs.getIntPref(SPLITCONSOLE_HEIGHT_PREF);
  349. this.webconsolePanel.addEventListener("resize", this._saveSplitConsoleHeight);
  350. let buttonsPromise = this._buildButtons();
  351. this._pingTelemetry();
  352. // The isTargetSupported check needs to happen after the target is
  353. // remoted, otherwise we could have done it in the toolbox constructor
  354. // (bug 1072764).
  355. let toolDef = gDevTools.getToolDefinition(this._defaultToolId);
  356. if (!toolDef || !toolDef.isTargetSupported(this._target)) {
  357. this._defaultToolId = "webconsole";
  358. }
  359. yield this.selectTool(this._defaultToolId);
  360. // Wait until the original tool is selected so that the split
  361. // console input will receive focus.
  362. let splitConsolePromise = promise.resolve();
  363. if (Services.prefs.getBoolPref(SPLITCONSOLE_ENABLED_PREF)) {
  364. splitConsolePromise = this.openSplitConsole();
  365. }
  366. yield promise.all([
  367. splitConsolePromise,
  368. buttonsPromise,
  369. framesPromise
  370. ]);
  371. // Lazily connect to the profiler here and don't wait for it to complete,
  372. // used to intercept console.profile calls before the performance tools are open.
  373. let performanceFrontConnection = this.initPerformance();
  374. // If in testing environment, wait for performance connection to finish,
  375. // so we don't have to explicitly wait for this in tests; ideally, all tests
  376. // will handle this on their own, but each have their own tear down function.
  377. if (flags.testing) {
  378. yield performanceFrontConnection;
  379. }
  380. this.emit("ready");
  381. }.bind(this)).then(null, console.error.bind(console));
  382. },
  383. /**
  384. * loading React modules when needed (to avoid performance penalties
  385. * during Firefox start up time).
  386. */
  387. get React() {
  388. return this.browserRequire("devtools/client/shared/vendor/react");
  389. },
  390. get ReactDOM() {
  391. return this.browserRequire("devtools/client/shared/vendor/react-dom");
  392. },
  393. get ReactRedux() {
  394. return this.browserRequire("devtools/client/shared/vendor/react-redux");
  395. },
  396. // Return HostType id for telemetry
  397. _getTelemetryHostId: function () {
  398. switch (this.hostType) {
  399. case Toolbox.HostType.BOTTOM: return 0;
  400. case Toolbox.HostType.SIDE: return 1;
  401. case Toolbox.HostType.WINDOW: return 2;
  402. case Toolbox.HostType.CUSTOM: return 3;
  403. default: return 9;
  404. }
  405. },
  406. _pingTelemetry: function () {
  407. this._telemetry.toolOpened("toolbox");
  408. this._telemetry.logOncePerBrowserVersion(OS_HISTOGRAM, system.getOSCPU());
  409. this._telemetry.logOncePerBrowserVersion(OS_IS_64_BITS,
  410. Services.appinfo.is64Bit ? 1 : 0);
  411. this._telemetry.logOncePerBrowserVersion(SCREENSIZE_HISTOGRAM,
  412. system.getScreenDimensions());
  413. this._telemetry.log(HOST_HISTOGRAM, this._getTelemetryHostId());
  414. },
  415. /**
  416. * Because our panels are lazy loaded this is a good place to watch for
  417. * "pref-changed" events.
  418. * @param {String} event
  419. * The event type, "pref-changed".
  420. * @param {Object} data
  421. * {
  422. * newValue: The new value
  423. * oldValue: The old value
  424. * pref: The name of the preference that has changed
  425. * }
  426. */
  427. _prefChanged: function (event, data) {
  428. switch (data.pref) {
  429. case "devtools.cache.disabled":
  430. this._applyCacheSettings();
  431. break;
  432. case "devtools.serviceWorkers.testing.enabled":
  433. this._applyServiceWorkersTestingSettings();
  434. break;
  435. }
  436. },
  437. _buildOptions: function () {
  438. let selectOptions = (name, event) => {
  439. // Flip back to the last used panel if we are already
  440. // on the options panel.
  441. if (this.currentToolId === "options" &&
  442. gDevTools.getToolDefinition(this.lastUsedToolId)) {
  443. this.selectTool(this.lastUsedToolId);
  444. } else {
  445. this.selectTool("options");
  446. }
  447. // Prevent the opening of bookmarks window on toolbox.options.key
  448. event.preventDefault();
  449. };
  450. this.shortcuts.on(L10N.getStr("toolbox.options.key"), selectOptions);
  451. this.shortcuts.on(L10N.getStr("toolbox.help.key"), selectOptions);
  452. },
  453. _splitConsoleOnKeypress: function (e) {
  454. if (e.keyCode === KeyCodes.DOM_VK_ESCAPE) {
  455. this.toggleSplitConsole();
  456. // If the debugger is paused, don't let the ESC key stop any pending
  457. // navigation.
  458. if (this._threadClient.state == "paused") {
  459. e.preventDefault();
  460. }
  461. }
  462. },
  463. /**
  464. * Add a shortcut key that should work when a split console
  465. * has focus to the toolbox.
  466. *
  467. * @param {String} key
  468. * The electron key shortcut.
  469. * @param {Function} handler
  470. * The callback that should be called when the provided key shortcut is pressed.
  471. * @param {String} whichTool
  472. * The tool the key belongs to. The corresponding handler will only be triggered
  473. * if this tool is active.
  474. */
  475. useKeyWithSplitConsole: function (key, handler, whichTool) {
  476. this.shortcuts.on(key, (name, event) => {
  477. if (this.currentToolId === whichTool && this.isSplitConsoleFocused()) {
  478. handler();
  479. event.preventDefault();
  480. }
  481. });
  482. },
  483. _addReloadKeys: function () {
  484. [
  485. ["reload", false],
  486. ["reload2", false],
  487. ["forceReload", true],
  488. ["forceReload2", true]
  489. ].forEach(([id, force]) => {
  490. let key = L10N.getStr("toolbox." + id + ".key");
  491. this.shortcuts.on(key, (name, event) => {
  492. this.reloadTarget(force);
  493. // Prevent Firefox shortcuts from reloading the page
  494. event.preventDefault();
  495. });
  496. });
  497. },
  498. _addHostListeners: function () {
  499. this.shortcuts.on(L10N.getStr("toolbox.nextTool.key"),
  500. (name, event) => {
  501. this.selectNextTool();
  502. event.preventDefault();
  503. });
  504. this.shortcuts.on(L10N.getStr("toolbox.previousTool.key"),
  505. (name, event) => {
  506. this.selectPreviousTool();
  507. event.preventDefault();
  508. });
  509. this.shortcuts.on(L10N.getStr("toolbox.minimize.key"),
  510. (name, event) => {
  511. this._toggleMinimizeMode();
  512. event.preventDefault();
  513. });
  514. this.shortcuts.on(L10N.getStr("toolbox.toggleHost.key"),
  515. (name, event) => {
  516. this.switchToPreviousHost();
  517. event.preventDefault();
  518. });
  519. this.doc.addEventListener("keypress", this._splitConsoleOnKeypress, false);
  520. this.doc.addEventListener("focus", this._onFocus, true);
  521. this.win.addEventListener("unload", this.destroy);
  522. this.win.addEventListener("message", this._onBrowserMessage, true);
  523. },
  524. _removeHostListeners: function () {
  525. // The host iframe's contentDocument may already be gone.
  526. if (this.doc) {
  527. this.doc.removeEventListener("keypress", this._splitConsoleOnKeypress, false);
  528. this.doc.removeEventListener("focus", this._onFocus, true);
  529. this.win.removeEventListener("unload", this.destroy);
  530. this.win.removeEventListener("message", this._onBrowserMessage, true);
  531. }
  532. },
  533. // Called whenever the chrome send a message
  534. _onBrowserMessage: function (event) {
  535. if (!event.data) {
  536. return;
  537. }
  538. switch (event.data.name) {
  539. case "switched-host":
  540. this._onSwitchedHost(event.data);
  541. break;
  542. case "host-minimized":
  543. if (this.hostType == Toolbox.HostType.BOTTOM) {
  544. this._onBottomHostMinimized();
  545. }
  546. break;
  547. case "host-maximized":
  548. if (this.hostType == Toolbox.HostType.BOTTOM) {
  549. this._onBottomHostMaximized();
  550. }
  551. break;
  552. }
  553. },
  554. _registerOverlays: function () {
  555. registerHarOverlay(this);
  556. },
  557. _saveSplitConsoleHeight: function () {
  558. Services.prefs.setIntPref(SPLITCONSOLE_HEIGHT_PREF,
  559. this.webconsolePanel.height);
  560. },
  561. /**
  562. * Make sure that the console is showing up properly based on all the
  563. * possible conditions.
  564. * 1) If the console tab is selected, then regardless of split state
  565. * it should take up the full height of the deck, and we should
  566. * hide the deck and splitter.
  567. * 2) If the console tab is not selected and it is split, then we should
  568. * show the splitter, deck, and console.
  569. * 3) If the console tab is not selected and it is *not* split,
  570. * then we should hide the console and splitter, and show the deck
  571. * at full height.
  572. */
  573. _refreshConsoleDisplay: function () {
  574. let deck = this.doc.getElementById("toolbox-deck");
  575. let webconsolePanel = this.webconsolePanel;
  576. let splitter = this.doc.getElementById("toolbox-console-splitter");
  577. let openedConsolePanel = this.currentToolId === "webconsole";
  578. if (openedConsolePanel) {
  579. deck.setAttribute("collapsed", "true");
  580. splitter.setAttribute("hidden", "true");
  581. webconsolePanel.removeAttribute("collapsed");
  582. } else {
  583. deck.removeAttribute("collapsed");
  584. if (this.splitConsole) {
  585. webconsolePanel.removeAttribute("collapsed");
  586. splitter.removeAttribute("hidden");
  587. } else {
  588. webconsolePanel.setAttribute("collapsed", "true");
  589. splitter.setAttribute("hidden", "true");
  590. }
  591. }
  592. },
  593. /**
  594. * Adds the keys and commands to the Toolbox Window in window mode.
  595. */
  596. _addKeysToWindow: function () {
  597. if (this.hostType != Toolbox.HostType.WINDOW) {
  598. return;
  599. }
  600. let doc = this.win.parent.document;
  601. for (let [id, toolDefinition] of gDevTools.getToolDefinitionMap()) {
  602. // Prevent multiple entries for the same tool.
  603. if (!toolDefinition.key || doc.getElementById("key_" + id)) {
  604. continue;
  605. }
  606. let toolId = id;
  607. let key = doc.createElement("key");
  608. key.id = "key_" + toolId;
  609. if (toolDefinition.key.startsWith("VK_")) {
  610. key.setAttribute("keycode", toolDefinition.key);
  611. } else {
  612. key.setAttribute("key", toolDefinition.key);
  613. }
  614. key.setAttribute("modifiers", toolDefinition.modifiers);
  615. // needed. See bug 371900
  616. key.setAttribute("oncommand", "void(0);");
  617. key.addEventListener("command", () => {
  618. this.selectTool(toolId).then(() => this.fireCustomKey(toolId));
  619. }, true);
  620. doc.getElementById("toolbox-keyset").appendChild(key);
  621. }
  622. // Add key for toggling the browser console from the detached window
  623. if (!doc.getElementById("key_browserconsole")) {
  624. let key = doc.createElement("key");
  625. key.id = "key_browserconsole";
  626. key.setAttribute("key", L10N.getStr("browserConsoleCmd.commandkey"));
  627. key.setAttribute("modifiers", "accel,shift");
  628. // needed. See bug 371900
  629. key.setAttribute("oncommand", "void(0)");
  630. key.addEventListener("command", () => {
  631. HUDService.toggleBrowserConsole();
  632. }, true);
  633. doc.getElementById("toolbox-keyset").appendChild(key);
  634. }
  635. },
  636. /**
  637. * Handle any custom key events. Returns true if there was a custom key
  638. * binding run.
  639. * @param {string} toolId Which tool to run the command on (skip if not
  640. * current)
  641. */
  642. fireCustomKey: function (toolId) {
  643. let toolDefinition = gDevTools.getToolDefinition(toolId);
  644. if (toolDefinition.onkey &&
  645. ((this.currentToolId === toolId) ||
  646. (toolId == "webconsole" && this.splitConsole))) {
  647. toolDefinition.onkey(this.getCurrentPanel(), this);
  648. }
  649. },
  650. /**
  651. * Build the notification box as soon as needed.
  652. */
  653. get notificationBox() {
  654. if (!this._notificationBox) {
  655. let { NotificationBox, PriorityLevels } =
  656. this.browserRequire(
  657. "devtools/client/shared/components/notification-box");
  658. NotificationBox = this.React.createFactory(NotificationBox);
  659. // Render NotificationBox and assign priority levels to it.
  660. let box = this.doc.getElementById("toolbox-notificationbox");
  661. this._notificationBox = Object.assign(
  662. this.ReactDOM.render(NotificationBox({}), box),
  663. PriorityLevels);
  664. }
  665. return this._notificationBox;
  666. },
  667. /**
  668. * Build the buttons for changing hosts. Called every time
  669. * the host changes.
  670. */
  671. _buildDockButtons: function () {
  672. let dockBox = this.doc.getElementById("toolbox-dock-buttons");
  673. while (dockBox.firstChild) {
  674. dockBox.removeChild(dockBox.firstChild);
  675. }
  676. if (!this._target.isLocalTab) {
  677. return;
  678. }
  679. // Bottom-type host can be minimized, add a button for this.
  680. if (this.hostType == Toolbox.HostType.BOTTOM) {
  681. let minimizeBtn = this.doc.createElementNS(HTML_NS, "button");
  682. minimizeBtn.id = "toolbox-dock-bottom-minimize";
  683. minimizeBtn.className = "devtools-button";
  684. /* Bug 1177463 - The minimize button is currently hidden until we agree on
  685. the UI for it, and until bug 1173849 is fixed too. */
  686. minimizeBtn.setAttribute("hidden", "true");
  687. minimizeBtn.addEventListener("click", this._toggleMinimizeMode);
  688. dockBox.appendChild(minimizeBtn);
  689. // Show the button in its maximized state.
  690. this._onBottomHostMaximized();
  691. // Maximize again when a tool gets selected.
  692. this.on("before-select", this._onToolSelectWhileMinimized);
  693. // Maximize and stop listening before the host type changes.
  694. this.once("host-will-change", this._onBottomHostWillChange);
  695. }
  696. if (this.hostType == Toolbox.HostType.WINDOW) {
  697. this.closeButton.setAttribute("hidden", "true");
  698. } else {
  699. this.closeButton.removeAttribute("hidden");
  700. }
  701. let sideEnabled = Services.prefs.getBoolPref(this._prefs.SIDE_ENABLED);
  702. for (let type in Toolbox.HostType) {
  703. let position = Toolbox.HostType[type];
  704. if (position == this.hostType ||
  705. position == Toolbox.HostType.CUSTOM ||
  706. (!sideEnabled && position == Toolbox.HostType.SIDE)) {
  707. continue;
  708. }
  709. let button = this.doc.createElementNS(HTML_NS, "button");
  710. button.id = "toolbox-dock-" + position;
  711. button.className = "toolbox-dock-button devtools-button";
  712. button.setAttribute("title", L10N.getStr("toolboxDockButtons." +
  713. position + ".tooltip"));
  714. button.addEventListener("click", this.switchHost.bind(this, position));
  715. dockBox.appendChild(button);
  716. }
  717. },
  718. _getMinimizeButtonShortcutTooltip: function () {
  719. let str = L10N.getStr("toolbox.minimize.key");
  720. let key = KeyShortcuts.parseElectronKey(this.win, str);
  721. return "(" + KeyShortcuts.stringify(key) + ")";
  722. },
  723. _onBottomHostMinimized: function () {
  724. let btn = this.doc.querySelector("#toolbox-dock-bottom-minimize");
  725. btn.className = "minimized";
  726. btn.setAttribute("title",
  727. L10N.getStr("toolboxDockButtons.bottom.maximize") + " " +
  728. this._getMinimizeButtonShortcutTooltip());
  729. },
  730. _onBottomHostMaximized: function () {
  731. let btn = this.doc.querySelector("#toolbox-dock-bottom-minimize");
  732. btn.className = "maximized";
  733. btn.setAttribute("title",
  734. L10N.getStr("toolboxDockButtons.bottom.minimize") + " " +
  735. this._getMinimizeButtonShortcutTooltip());
  736. },
  737. _onToolSelectWhileMinimized: function () {
  738. this.postMessage({
  739. name: "maximize-host"
  740. });
  741. },
  742. postMessage: function (msg) {
  743. // We sometime try to send messages in middle of destroy(), where the
  744. // toolbox iframe may already be detached and no longer have a parent.
  745. if (this.win.parent) {
  746. // Toolbox document is still chrome and disallow identifying message
  747. // origin via event.source as it is null. So use a custom id.
  748. msg.frameId = this.frameId;
  749. this.win.parent.postMessage(msg, "*");
  750. }
  751. },
  752. _onBottomHostWillChange: function () {
  753. this.postMessage({
  754. name: "maximize-host"
  755. });
  756. this.off("before-select", this._onToolSelectWhileMinimized);
  757. },
  758. _toggleMinimizeMode: function () {
  759. if (this.hostType !== Toolbox.HostType.BOTTOM) {
  760. return;
  761. }
  762. // Calculate the height to which the host should be minimized so the
  763. // tabbar is still visible.
  764. let toolbarHeight = this.tabbar.getBoxQuads({box: "content"})[0].bounds
  765. .height;
  766. this.postMessage({
  767. name: "toggle-minimize-mode",
  768. toolbarHeight
  769. });
  770. },
  771. /**
  772. * Add tabs to the toolbox UI for registered tools
  773. */
  774. _buildTabs: function () {
  775. for (let definition of gDevTools.getToolDefinitionArray()) {
  776. this._buildTabForTool(definition);
  777. }
  778. },
  779. /**
  780. * Get all dev tools tab bar focusable elements. These are visible elements
  781. * such as buttons or elements with tabindex.
  782. */
  783. get tabbarFocusableElms() {
  784. return [...this.tabbar.querySelectorAll(
  785. "[tabindex]:not([hidden]), button:not([hidden])")];
  786. },
  787. /**
  788. * Reset tabindex attributes across all focusable elements inside the tabbar.
  789. * Only have one element with tabindex=0 at a time to make sure that tabbing
  790. * results in navigating away from the tabbar container.
  791. * @param {FocusEvent} event
  792. */
  793. _onTabbarFocus: function (event) {
  794. this.tabbarFocusableElms.forEach(elm =>
  795. elm.setAttribute("tabindex", event.target === elm ? "0" : "-1"));
  796. },
  797. /**
  798. * On left/right arrow press, attempt to move the focus inside the tabbar to
  799. * the previous/next focusable element.
  800. * @param {KeyboardEvent} event
  801. */
  802. _onTabbarArrowKeypress: function (event) {
  803. let { key, target, ctrlKey, shiftKey, altKey, metaKey } = event;
  804. // If any of the modifier keys are pressed do not attempt navigation as it
  805. // might conflict with global shortcuts (Bug 1327972).
  806. if (ctrlKey || shiftKey || altKey || metaKey) {
  807. return;
  808. }
  809. let focusableElms = this.tabbarFocusableElms;
  810. let curIndex = focusableElms.indexOf(target);
  811. if (curIndex === -1) {
  812. console.warn(target + " is not found among Developer Tools tab bar " +
  813. "focusable elements. It needs to either be a button or have " +
  814. "tabindex. If it is intended to be hidden, 'hidden' attribute must " +
  815. "be used.");
  816. return;
  817. }
  818. let newTarget;
  819. if (key === "ArrowLeft") {
  820. // Do nothing if already at the beginning.
  821. if (curIndex === 0) {
  822. return;
  823. }
  824. newTarget = focusableElms[curIndex - 1];
  825. } else if (key === "ArrowRight") {
  826. // Do nothing if already at the end.
  827. if (curIndex === focusableElms.length - 1) {
  828. return;
  829. }
  830. newTarget = focusableElms[curIndex + 1];
  831. } else {
  832. return;
  833. }
  834. focusableElms.forEach(elm =>
  835. elm.setAttribute("tabindex", newTarget === elm ? "0" : "-1"));
  836. newTarget.focus();
  837. event.preventDefault();
  838. event.stopPropagation();
  839. },
  840. /**
  841. * Add buttons to the UI as specified in the devtools.toolbox.toolbarSpec pref
  842. */
  843. _buildButtons: function () {
  844. if (this.target.getTrait("highlightable")) {
  845. this._buildPickerButton();
  846. }
  847. this.setToolboxButtonsVisibility();
  848. // Old servers don't have a GCLI Actor, so just return
  849. if (!this.target.hasActor("gcli")) {
  850. return promise.resolve();
  851. }
  852. // Disable gcli in browser toolbox until there is usages of it
  853. if (this.target.chrome) {
  854. return promise.resolve();
  855. }
  856. const options = {
  857. environment: CommandUtils.createEnvironment(this, "_target")
  858. };
  859. return CommandUtils.createRequisition(this.target, options).then(requisition => {
  860. this._requisition = requisition;
  861. const spec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec");
  862. return CommandUtils.createButtons(spec, this.target, this.doc, requisition)
  863. .then(buttons => {
  864. let container = this.doc.getElementById("toolbox-buttons");
  865. buttons.forEach(button => {
  866. if (button) {
  867. container.appendChild(button);
  868. }
  869. });
  870. this.setToolboxButtonsVisibility();
  871. });
  872. });
  873. },
  874. /**
  875. * Adding the element picker button is done here unlike the other buttons
  876. * since we want it to work for remote targets too
  877. */
  878. _buildPickerButton: function () {
  879. this._pickerButton = this.doc.createElementNS(HTML_NS, "button");
  880. this._pickerButton.id = "command-button-pick";
  881. this._pickerButton.className =
  882. "command-button command-button-invertable devtools-button";
  883. this._pickerButton.setAttribute("title", L10N.getStr("pickButton.tooltip"));
  884. let container = this.doc.querySelector("#toolbox-picker-container");
  885. container.appendChild(this._pickerButton);
  886. this._pickerButton.addEventListener("click", this._onPickerClick, false);
  887. },
  888. /**
  889. * Toggle the picker, but also decide whether or not the highlighter should
  890. * focus the window. This is only desirable when the toolbox is mounted to the
  891. * window. When devtools is free floating, then the target window should not
  892. * pop in front of the viewer when the picker is clicked.
  893. */
  894. _onPickerClick: function () {
  895. let focus = this.hostType === Toolbox.HostType.BOTTOM ||
  896. this.hostType === Toolbox.HostType.SIDE;
  897. this.highlighterUtils.togglePicker(focus);
  898. },
  899. /**
  900. * If the picker is activated, then allow the Escape key to deactivate the
  901. * functionality instead of the default behavior of toggling the console.
  902. */
  903. _onPickerKeypress: function (event) {
  904. if (event.keyCode === KeyCodes.DOM_VK_ESCAPE) {
  905. this.highlighterUtils.cancelPicker();
  906. // Stop the console from toggling.
  907. event.stopImmediatePropagation();
  908. }
  909. },
  910. _onPickerStarted: function () {
  911. this.doc.addEventListener("keypress", this._onPickerKeypress, true);
  912. },
  913. _onPickerStopped: function () {
  914. this.doc.removeEventListener("keypress", this._onPickerKeypress, true);
  915. },
  916. /**
  917. * Apply the current cache setting from devtools.cache.disabled to this
  918. * toolbox's tab.
  919. */
  920. _applyCacheSettings: function () {
  921. let pref = "devtools.cache.disabled";
  922. let cacheDisabled = Services.prefs.getBoolPref(pref);
  923. if (this.target.activeTab) {
  924. this.target.activeTab.reconfigure({"cacheDisabled": cacheDisabled});
  925. }
  926. },
  927. /**
  928. * Apply the current service workers testing setting from
  929. * devtools.serviceWorkers.testing.enabled to this toolbox's tab.
  930. */
  931. _applyServiceWorkersTestingSettings: function () {
  932. let pref = "devtools.serviceWorkers.testing.enabled";
  933. let serviceWorkersTestingEnabled =
  934. Services.prefs.getBoolPref(pref) || false;
  935. if (this.target.activeTab) {
  936. this.target.activeTab.reconfigure({
  937. "serviceWorkersTestingEnabled": serviceWorkersTestingEnabled
  938. });
  939. }
  940. },
  941. /**
  942. * Setter for the checked state of the picker button in the toolbar
  943. * @param {Boolean} isChecked
  944. */
  945. set pickerButtonChecked(isChecked) {
  946. if (isChecked) {
  947. this._pickerButton.setAttribute("checked", "true");
  948. } else {
  949. this._pickerButton.removeAttribute("checked");
  950. }
  951. },
  952. /**
  953. * Return all toolbox buttons (command buttons, plus any others that were
  954. * added manually).
  955. */
  956. get toolboxButtons() {
  957. return ToolboxButtons.map(options => {
  958. let button = this.doc.getElementById(options.id);
  959. // Some buttons may not exist inside of Browser Toolbox
  960. if (!button) {
  961. return false;
  962. }
  963. return {
  964. id: options.id,
  965. button: button,
  966. label: button.getAttribute("title"),
  967. visibilityswitch: "devtools." + options.id + ".enabled",
  968. isTargetSupported: options.isTargetSupported
  969. ? options.isTargetSupported
  970. : target => target.isLocalTab,
  971. };
  972. }).filter(button=>button);
  973. },
  974. /**
  975. * Ensure the visibility of each toolbox button matches the
  976. * preference value. Simply hide buttons that are preffed off.
  977. */
  978. setToolboxButtonsVisibility: function () {
  979. this.toolboxButtons.forEach(buttonSpec => {
  980. let { visibilityswitch, button, isTargetSupported } = buttonSpec;
  981. let on = Services.prefs.getBoolPref(visibilityswitch, true);
  982. on = on && isTargetSupported(this.target);
  983. if (button) {
  984. if (on) {
  985. button.removeAttribute("hidden");
  986. } else {
  987. button.setAttribute("hidden", "true");
  988. }
  989. }
  990. });
  991. this._updateNoautohideButton();
  992. },
  993. /**
  994. * Build a tab for one tool definition and add to the toolbox
  995. *
  996. * @param {string} toolDefinition
  997. * Tool definition of the tool to build a tab for.
  998. */
  999. _buildTabForTool: function (toolDefinition) {
  1000. if (!toolDefinition.isTargetSupported(this._target)) {
  1001. return;
  1002. }
  1003. let tabs = this.doc.getElementById("toolbox-tabs");
  1004. let deck = this.doc.getElementById("toolbox-deck");
  1005. let id = toolDefinition.id;
  1006. if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) {
  1007. toolDefinition.ordinal = MAX_ORDINAL;
  1008. }
  1009. let radio = this.doc.createElement("radio");
  1010. // The radio element is not being used in the conventional way, thus
  1011. // the devtools-tab class replaces the radio XBL binding with its base
  1012. // binding (the control-item binding).
  1013. radio.className = "devtools-tab";
  1014. radio.id = "toolbox-tab-" + id;
  1015. radio.setAttribute("toolid", id);
  1016. radio.setAttribute("tabindex", "0");
  1017. radio.setAttribute("ordinal", toolDefinition.ordinal);
  1018. radio.setAttribute("tooltiptext", toolDefinition.tooltip);
  1019. if (toolDefinition.invertIconForLightTheme) {
  1020. radio.setAttribute("icon-invertable", "light-theme");
  1021. } else if (toolDefinition.invertIconForDarkTheme) {
  1022. radio.setAttribute("icon-invertable", "dark-theme");
  1023. }
  1024. radio.addEventListener("command", this.selectTool.bind(this, id));
  1025. // spacer lets us center the image and label, while allowing cropping
  1026. let spacer = this.doc.createElement("spacer");
  1027. spacer.setAttribute("flex", "1");
  1028. radio.appendChild(spacer);
  1029. if (toolDefinition.icon) {
  1030. let image = this.doc.createElement("image");
  1031. image.className = "default-icon";
  1032. image.setAttribute("src",
  1033. toolDefinition.icon || toolDefinition.highlightedicon);
  1034. radio.appendChild(image);
  1035. // Adding the highlighted icon image
  1036. image = this.doc.createElement("image");
  1037. image.className = "highlighted-icon";
  1038. image.setAttribute("src",
  1039. toolDefinition.highlightedicon || toolDefinition.icon);
  1040. radio.appendChild(image);
  1041. }
  1042. if (toolDefinition.label && !toolDefinition.iconOnly) {
  1043. let label = this.doc.createElement("label");
  1044. label.setAttribute("value", toolDefinition.label);
  1045. label.setAttribute("crop", "end");
  1046. label.setAttribute("flex", "1");
  1047. radio.appendChild(label);
  1048. }
  1049. if (!toolDefinition.bgTheme) {
  1050. toolDefinition.bgTheme = "theme-toolbar";
  1051. }
  1052. let vbox = this.doc.createElement("vbox");
  1053. vbox.className = "toolbox-panel " + toolDefinition.bgTheme;
  1054. // There is already a container for the webconsole frame.
  1055. if (!this.doc.getElementById("toolbox-panel-" + id)) {
  1056. vbox.id = "toolbox-panel-" + id;
  1057. }
  1058. if (id === "options") {
  1059. // Options panel is special. It doesn't belong in the same container as
  1060. // the other tabs.
  1061. radio.setAttribute("role", "button");
  1062. let optionTabContainer = this.doc.getElementById("toolbox-option-container");
  1063. optionTabContainer.appendChild(radio);
  1064. deck.appendChild(vbox);
  1065. } else {
  1066. radio.setAttribute("role", "tab");
  1067. // If there is no tab yet, or the ordinal to be added is the largest one.
  1068. if (tabs.childNodes.length == 0 ||
  1069. tabs.lastChild.getAttribute("ordinal") <= toolDefinition.ordinal) {
  1070. tabs.appendChild(radio);
  1071. deck.appendChild(vbox);
  1072. } else {
  1073. // else, iterate over all the tabs to get the correct location.
  1074. Array.some(tabs.childNodes, (node, i) => {
  1075. if (+node.getAttribute("ordinal") > toolDefinition.ordinal) {
  1076. tabs.insertBefore(radio, node);
  1077. deck.insertBefore(vbox, deck.childNodes[i]);
  1078. return true;
  1079. }
  1080. return false;
  1081. });
  1082. }
  1083. }
  1084. this._addKeysToWindow();
  1085. },
  1086. /**
  1087. * Ensure the tool with the given id is loaded.
  1088. *
  1089. * @param {string} id
  1090. * The id of the tool to load.
  1091. */
  1092. loadTool: function (id) {
  1093. if (id === "inspector" && !this._inspector) {
  1094. return this.initInspector().then(() => {
  1095. return this.loadTool(id);
  1096. });
  1097. }
  1098. let deferred = defer();
  1099. let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
  1100. if (iframe) {
  1101. let panel = this._toolPanels.get(id);
  1102. if (panel) {
  1103. deferred.resolve(panel);
  1104. } else {
  1105. this.once(id + "-ready", initializedPanel => {
  1106. deferred.resolve(initializedPanel);
  1107. });
  1108. }
  1109. return deferred.promise;
  1110. }
  1111. let definition = gDevTools.getToolDefinition(id);
  1112. if (!definition) {
  1113. deferred.reject(new Error("no such tool id " + id));
  1114. return deferred.promise;
  1115. }
  1116. iframe = this.doc.createElement("iframe");
  1117. iframe.className = "toolbox-panel-iframe";
  1118. iframe.id = "toolbox-panel-iframe-" + id;
  1119. iframe.setAttribute("flex", 1);
  1120. iframe.setAttribute("forceOwnRefreshDriver", "");
  1121. iframe.tooltip = "aHTMLTooltip";
  1122. iframe.style.visibility = "hidden";
  1123. gDevTools.emit(id + "-init", this, iframe);
  1124. this.emit(id + "-init", iframe);
  1125. // If no parent yet, append the frame into default location.
  1126. if (!iframe.parentNode) {
  1127. let vbox = this.doc.getElementById("toolbox-panel-" + id);
  1128. vbox.appendChild(iframe);
  1129. }
  1130. let onLoad = () => {
  1131. // Prevent flicker while loading by waiting to make visible until now.
  1132. iframe.style.visibility = "visible";
  1133. // Try to set the dir attribute as early as possible.
  1134. this.setIframeDocumentDir(iframe);
  1135. // The build method should return a panel instance, so events can
  1136. // be fired with the panel as an argument. However, in order to keep
  1137. // backward compatibility with existing extensions do a check
  1138. // for a promise return value.
  1139. let built = definition.build(iframe.contentWindow, this);
  1140. if (!(typeof built.then == "function")) {
  1141. let panel = built;
  1142. iframe.panel = panel;
  1143. // The panel instance is expected to fire (and listen to) various
  1144. // framework events, so make sure it's properly decorated with
  1145. // appropriate API (on, off, once, emit).
  1146. // In this case we decorate panel instances directly returned by
  1147. // the tool definition 'build' method.
  1148. if (typeof panel.emit == "undefined") {
  1149. EventEmitter.decorate(panel);
  1150. }
  1151. gDevTools.emit(id + "-build", this, panel);
  1152. this.emit(id + "-build", panel);
  1153. // The panel can implement an 'open' method for asynchronous
  1154. // initialization sequence.
  1155. if (typeof panel.open == "function") {
  1156. built = panel.open();
  1157. } else {
  1158. let buildDeferred = defer();
  1159. buildDeferred.resolve(panel);
  1160. built = buildDeferred.promise;
  1161. }
  1162. }
  1163. // Wait till the panel is fully ready and fire 'ready' events.
  1164. promise.resolve(built).then((panel) => {
  1165. this._toolPanels.set(id, panel);
  1166. // Make sure to decorate panel object with event API also in case
  1167. // where the tool definition 'build' method returns only a promise
  1168. // and the actual panel instance is available as soon as the
  1169. // promise is resolved.
  1170. if (typeof panel.emit == "undefined") {
  1171. EventEmitter.decorate(panel);
  1172. }
  1173. gDevTools.emit(id + "-ready", this, panel);
  1174. this.emit(id + "-ready", panel);
  1175. deferred.resolve(panel);
  1176. }, console.error);
  1177. };
  1178. iframe.setAttribute("src", definition.url);
  1179. if (definition.panelLabel) {
  1180. iframe.setAttribute("aria-label", definition.panelLabel);
  1181. }
  1182. // Depending on the host, iframe.contentWindow is not always
  1183. // defined at this moment. If it is not defined, we use an
  1184. // event listener on the iframe DOM node. If it's defined,
  1185. // we use the chromeEventHandler. We can't use a listener
  1186. // on the DOM node every time because this won't work
  1187. // if the (xul chrome) iframe is loaded in a content docshell.
  1188. if (iframe.contentWindow) {
  1189. let domHelper = new DOMHelpers(iframe.contentWindow);
  1190. domHelper.onceDOMReady(onLoad);
  1191. } else {
  1192. let callback = () => {
  1193. iframe.removeEventListener("DOMContentLoaded", callback);
  1194. onLoad();
  1195. };
  1196. iframe.addEventListener("DOMContentLoaded", callback);
  1197. }
  1198. return deferred.promise;
  1199. },
  1200. /**
  1201. * Set the dir attribute on the content document element of the provided iframe.
  1202. *
  1203. * @param {IFrameElement} iframe
  1204. */
  1205. setIframeDocumentDir: function (iframe) {
  1206. let docEl = iframe.contentWindow && iframe.contentWindow.document.documentElement;
  1207. if (!docEl || docEl.namespaceURI !== HTML_NS) {
  1208. // Bail out if the content window or document is not ready or if the document is not
  1209. // HTML.
  1210. return;
  1211. }
  1212. if (docEl.hasAttribute("dir")) {
  1213. // Set the dir attribute value only if dir is already present on the document.
  1214. let top = this.win.top;
  1215. let topDocEl = top.document.documentElement;
  1216. let isRtl = top.getComputedStyle(topDocEl).direction === "rtl";
  1217. docEl.setAttribute("dir", isRtl ? "rtl" : "ltr");
  1218. }
  1219. },
  1220. /**
  1221. * Mark all in collection as unselected; and id as selected
  1222. * @param {string} collection
  1223. * DOM collection of items
  1224. * @param {string} id
  1225. * The Id of the item within the collection to select
  1226. */
  1227. selectSingleNode: function (collection, id) {
  1228. [...collection].forEach(node => {
  1229. if (node.id === id) {
  1230. node.setAttribute("selected", "true");
  1231. node.setAttribute("aria-selected", "true");
  1232. } else {
  1233. node.removeAttribute("selected");
  1234. node.removeAttribute("aria-selected");
  1235. }
  1236. });
  1237. },
  1238. /**
  1239. * Switch to the tool with the given id
  1240. *
  1241. * @param {string} id
  1242. * The id of the tool to switch to
  1243. */
  1244. selectTool: function (id) {
  1245. this.emit("before-select", id);
  1246. let tabs = this.doc.querySelectorAll(".devtools-tab");
  1247. this.selectSingleNode(tabs, "toolbox-tab-" + id);
  1248. // If options is selected, the separator between it and the
  1249. // command buttons should be hidden.
  1250. let sep = this.doc.getElementById("toolbox-controls-separator");
  1251. if (id === "options") {
  1252. sep.setAttribute("invisible", "true");
  1253. } else {
  1254. sep.removeAttribute("invisible");
  1255. }
  1256. if (this.currentToolId == id) {
  1257. let panel = this._toolPanels.get(id);
  1258. if (panel) {
  1259. // We have a panel instance, so the tool is already fully loaded.
  1260. // re-focus tool to get key events again
  1261. this.focusTool(id);
  1262. // Return the existing panel in order to have a consistent return value.
  1263. return promise.resolve(panel);
  1264. }
  1265. // Otherwise, if there is no panel instance, it is still loading,
  1266. // so we are racing another call to selectTool with the same id.
  1267. return this.once("select").then(() => promise.resolve(this._toolPanels.get(id)));
  1268. }
  1269. if (!this.isReady) {
  1270. throw new Error("Can't select tool, wait for toolbox 'ready' event");
  1271. }
  1272. let tab = this.doc.getElementById("toolbox-tab-" + id);
  1273. if (tab) {
  1274. if (this.currentToolId) {
  1275. this._telemetry.toolClosed(this.currentToolId);
  1276. }
  1277. this._telemetry.toolOpened(id);
  1278. } else {
  1279. throw new Error("No tool found");
  1280. }
  1281. let tabstrip = this.doc.getElementById("toolbox-tabs");
  1282. // select the right tab, making 0th index the default tab if right tab not
  1283. // found.
  1284. tabstrip.selectedItem = tab || tabstrip.childNodes[0];
  1285. // and select the right iframe
  1286. let toolboxPanels = this.doc.querySelectorAll(".toolbox-panel");
  1287. this.selectSingleNode(toolboxPanels, "toolbox-panel-" + id);
  1288. this.lastUsedToolId = this.currentToolId;
  1289. this.currentToolId = id;
  1290. this._refreshConsoleDisplay();
  1291. if (id != "options") {
  1292. Services.prefs.setCharPref(this._prefs.LAST_TOOL, id);
  1293. }
  1294. return this.loadTool(id).then(panel => {
  1295. // focus the tool's frame to start receiving key events
  1296. this.focusTool(id);
  1297. this.emit("select", id);
  1298. this.emit(id + "-selected", panel);
  1299. return panel;
  1300. });
  1301. },
  1302. /**
  1303. * Focus a tool's panel by id
  1304. * @param {string} id
  1305. * The id of tool to focus
  1306. */
  1307. focusTool: function (id, state = true) {
  1308. let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
  1309. if (state) {
  1310. iframe.focus();
  1311. } else {
  1312. iframe.blur();
  1313. }
  1314. },
  1315. /**
  1316. * Focus split console's input line
  1317. */
  1318. focusConsoleInput: function () {
  1319. let consolePanel = this.getPanel("webconsole");
  1320. if (consolePanel) {
  1321. consolePanel.focusInput();
  1322. }
  1323. },
  1324. /**
  1325. * If the console is split and we are focusing an element outside
  1326. * of the console, then store the newly focused element, so that
  1327. * it can be restored once the split console closes.
  1328. */
  1329. _onFocus: function ({originalTarget}) {
  1330. // Ignore any non element nodes, or any elements contained
  1331. // within the webconsole frame.
  1332. let webconsoleURL = gDevTools.getToolDefinition("webconsole").url;
  1333. if (originalTarget.nodeType !== 1 ||
  1334. originalTarget.baseURI === webconsoleURL) {
  1335. return;
  1336. }
  1337. this._lastFocusedElement = originalTarget;
  1338. },
  1339. /**
  1340. * Opens the split console.
  1341. *
  1342. * @returns {Promise} a promise that resolves once the tool has been
  1343. * loaded and focused.
  1344. */
  1345. openSplitConsole: function () {
  1346. this._splitConsole = true;
  1347. Services.prefs.setBoolPref(SPLITCONSOLE_ENABLED_PREF, true);
  1348. this._refreshConsoleDisplay();
  1349. this.emit("split-console");
  1350. return this.loadTool("webconsole").then(() => {
  1351. this.focusConsoleInput();
  1352. });
  1353. },
  1354. /**
  1355. * Closes the split console.
  1356. *
  1357. * @returns {Promise} a promise that resolves once the tool has been
  1358. * closed.
  1359. */
  1360. closeSplitConsole: function () {
  1361. this._splitConsole = false;
  1362. Services.prefs.setBoolPref(SPLITCONSOLE_ENABLED_PREF, false);
  1363. this._refreshConsoleDisplay();
  1364. this.emit("split-console");
  1365. if (this._lastFocusedElement) {
  1366. this._lastFocusedElement.focus();
  1367. }
  1368. return promise.resolve();
  1369. },
  1370. /**
  1371. * Toggles the split state of the webconsole. If the webconsole panel
  1372. * is already selected then this command is ignored.
  1373. *
  1374. * @returns {Promise} a promise that resolves once the tool has been
  1375. * opened or closed.
  1376. */
  1377. toggleSplitConsole: function () {
  1378. if (this.currentToolId !== "webconsole") {
  1379. return this.splitConsole ?
  1380. this.closeSplitConsole() :
  1381. this.openSplitConsole();
  1382. }
  1383. return promise.resolve();
  1384. },
  1385. /**
  1386. * Tells the target tab to reload.
  1387. */
  1388. reloadTarget: function (force) {
  1389. this.target.activeTab.reload({ force: force });
  1390. },
  1391. /**
  1392. * Loads the tool next to the currently selected tool.
  1393. */
  1394. selectNextTool: function () {
  1395. let tools = this.doc.querySelectorAll(".devtools-tab");
  1396. let selected = this.doc.querySelector(".devtools-tab[selected]");
  1397. let nextIndex = [...tools].indexOf(selected) + 1;
  1398. let next = tools[nextIndex] || tools[0];
  1399. let tool = next.getAttribute("toolid");
  1400. return this.selectTool(tool);
  1401. },
  1402. /**
  1403. * Loads the tool just left to the currently selected tool.
  1404. */
  1405. selectPreviousTool: function () {
  1406. let tools = this.doc.querySelectorAll(".devtools-tab");
  1407. let selected = this.doc.querySelector(".devtools-tab[selected]");
  1408. let prevIndex = [...tools].indexOf(selected) - 1;
  1409. let prev = tools[prevIndex] || tools[tools.length - 1];
  1410. let tool = prev.getAttribute("toolid");
  1411. return this.selectTool(tool);
  1412. },
  1413. /**
  1414. * Highlights the tool's tab if it is not the currently selected tool.
  1415. *
  1416. * @param {string} id
  1417. * The id of the tool to highlight
  1418. */
  1419. highlightTool: function (id) {
  1420. let tab = this.doc.getElementById("toolbox-tab-" + id);
  1421. tab && tab.setAttribute("highlighted", "true");
  1422. },
  1423. /**
  1424. * De-highlights the tool's tab.
  1425. *
  1426. * @param {string} id
  1427. * The id of the tool to unhighlight
  1428. */
  1429. unhighlightTool: function (id) {
  1430. let tab = this.doc.getElementById("toolbox-tab-" + id);
  1431. tab && tab.removeAttribute("highlighted");
  1432. },
  1433. /**
  1434. * Raise the toolbox host.
  1435. */
  1436. raise: function () {
  1437. this.postMessage({
  1438. name: "raise-host"
  1439. });
  1440. },
  1441. /**
  1442. * Refresh the host's title.
  1443. */
  1444. _refreshHostTitle: function () {
  1445. let title;
  1446. if (this.target.name && this.target.name != this.target.url) {
  1447. title = L10N.getFormatStr("toolbox.titleTemplate2", this.target.name,
  1448. this.target.url);
  1449. } else {
  1450. title = L10N.getFormatStr("toolbox.titleTemplate1", this.target.url);
  1451. }
  1452. this.postMessage({
  1453. name: "set-host-title",
  1454. title
  1455. });
  1456. },
  1457. // Returns an instance of the preference actor
  1458. get _preferenceFront() {
  1459. return this.target.root.then(rootForm => {
  1460. return getPreferenceFront(this.target.client, rootForm);
  1461. });
  1462. },
  1463. _toggleAutohide: Task.async(function* () {
  1464. let prefName = "ui.popup.disable_autohide";
  1465. let front = yield this._preferenceFront;
  1466. let current = yield front.getBoolPref(prefName);
  1467. yield front.setBoolPref(prefName, !current);
  1468. this._updateNoautohideButton();
  1469. }),
  1470. _updateNoautohideButton: Task.async(function* () {
  1471. let menu = this.doc.getElementById("command-button-noautohide");
  1472. if (menu.getAttribute("hidden") === "true") {
  1473. return;
  1474. }
  1475. if (!this.target.root) {
  1476. return;
  1477. }
  1478. let prefName = "ui.popup.disable_autohide";
  1479. let front = yield this._preferenceFront;
  1480. let current = yield front.getBoolPref(prefName);
  1481. if (current) {
  1482. menu.setAttribute("checked", "true");
  1483. } else {
  1484. menu.removeAttribute("checked");
  1485. }
  1486. }),
  1487. _listFrames: function (event) {
  1488. if (!this._target.activeTab || !this._target.activeTab.traits.frames) {
  1489. // We are not targetting a regular TabActor
  1490. // it can be either an addon or browser toolbox actor
  1491. return promise.resolve();
  1492. }
  1493. let packet = {
  1494. to: this._target.form.actor,
  1495. type: "listFrames"
  1496. };
  1497. return this._target.client.request(packet, resp => {
  1498. this._updateFrames(null, { frames: resp.frames });
  1499. });
  1500. },
  1501. /**
  1502. * Show a drop down menu that allows the user to switch frames.
  1503. */
  1504. showFramesMenu: function (event) {
  1505. let menu = new Menu();
  1506. let target = event.target;
  1507. // Generate list of menu items from the list of frames.
  1508. this.frameMap.forEach(frame => {
  1509. // A frame is checked if it's the selected one.
  1510. let checked = frame.id == this.selectedFrameId;
  1511. // Create menu item.
  1512. menu.append(new MenuItem({
  1513. label: frame.url,
  1514. type: "radio",
  1515. checked,
  1516. click: () => {
  1517. this.onSelectFrame(frame.id);
  1518. }
  1519. }));
  1520. });
  1521. menu.once("open").then(() => {
  1522. target.setAttribute("open", "true");
  1523. });
  1524. menu.once("close").then(() => {
  1525. target.removeAttribute("open");
  1526. });
  1527. // Show a drop down menu with frames.
  1528. // XXX Missing menu API for specifying target (anchor)
  1529. // and relative position to it. See also:
  1530. // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/openPopup
  1531. // https://bugzilla.mozilla.org/show_bug.cgi?id=1274551
  1532. let rect = target.getBoundingClientRect();
  1533. let screenX = target.ownerDocument.defaultView.mozInnerScreenX;
  1534. let screenY = target.ownerDocument.defaultView.mozInnerScreenY;
  1535. menu.popup(rect.left + screenX, rect.bottom + screenY, this);
  1536. return menu;
  1537. },
  1538. /**
  1539. * Select a frame by sending 'switchToFrame' packet to the backend.
  1540. */
  1541. onSelectFrame: function (frameId) {
  1542. // Send packet to the backend to select specified frame and
  1543. // wait for 'frameUpdate' event packet to update the UI.
  1544. let packet = {
  1545. to: this._target.form.actor,
  1546. type: "switchToFrame",
  1547. windowId: frameId
  1548. };
  1549. this._target.client.request(packet);
  1550. },
  1551. /**
  1552. * A handler for 'frameUpdate' packets received from the backend.
  1553. * Following properties might be set on the packet:
  1554. *
  1555. * destroyAll {Boolean}: All frames have been destroyed.
  1556. * selected {Number}: A frame has been selected
  1557. * frames {Array}: list of frames. Every frame can have:
  1558. * id {Number}: frame ID
  1559. * url {String}: frame URL
  1560. * title {String}: frame title
  1561. * destroy {Boolean}: Set to true if destroyed
  1562. * parentID {Number}: ID of the parent frame (not set
  1563. * for top level window)
  1564. */
  1565. _updateFrames: function (event, data) {
  1566. if (!Services.prefs.getBoolPref("devtools.command-button-frames.enabled")) {
  1567. return;
  1568. }
  1569. // We may receive this event before the toolbox is ready.
  1570. if (!this.isReady) {
  1571. return;
  1572. }
  1573. // Store (synchronize) data about all existing frames on the backend
  1574. if (data.destroyAll) {
  1575. this.frameMap.clear();
  1576. this.selectedFrameId = null;
  1577. } else if (data.selected) {
  1578. this.selectedFrameId = data.selected;
  1579. } else if (data.frames) {
  1580. data.frames.forEach(frame => {
  1581. if (frame.destroy) {
  1582. this.frameMap.delete(frame.id);
  1583. // Reset the currently selected frame if it's destroyed.
  1584. if (this.selectedFrameId == frame.id) {
  1585. this.selectedFrameId = null;
  1586. }
  1587. } else {
  1588. this.frameMap.set(frame.id, frame);
  1589. }
  1590. });
  1591. }
  1592. // If there is no selected frame select the first top level
  1593. // frame by default. Note that there might be more top level
  1594. // frames in case of the BrowserToolbox.
  1595. if (!this.selectedFrameId) {
  1596. let frames = [...this.frameMap.values()];
  1597. let topFrames = frames.filter(frame => !frame.parentID);
  1598. this.selectedFrameId = topFrames.length ? topFrames[0].id : null;
  1599. }
  1600. // Check out whether top frame is currently selected.
  1601. // Note that only child frame has parentID.
  1602. let frame = this.frameMap.get(this.selectedFrameId);
  1603. let topFrameSelected = frame ? !frame.parentID : false;
  1604. let button = this.doc.getElementById("command-button-frames");
  1605. button.removeAttribute("checked");
  1606. // If non-top level frame is selected the toolbar button is
  1607. // marked as 'checked' indicating that a child frame is active.
  1608. if (!topFrameSelected && this.selectedFrameId) {
  1609. button.setAttribute("checked", "true");
  1610. }
  1611. },
  1612. /**
  1613. * Switch to the last used host for the toolbox UI.
  1614. */
  1615. switchToPreviousHost: function () {
  1616. return this.switchHost("previous");
  1617. },
  1618. /**
  1619. * Switch to a new host for the toolbox UI. E.g. bottom, sidebar, window,
  1620. * and focus the window when done.
  1621. *
  1622. * @param {string} hostType
  1623. * The host type of the new host object
  1624. */
  1625. switchHost: function (hostType) {
  1626. if (hostType == this.hostType || !this._target.isLocalTab) {
  1627. return null;
  1628. }
  1629. this.emit("host-will-change", hostType);
  1630. // ToolboxHostManager is going to call swapFrameLoaders which mess up with
  1631. // focus. We have to blur before calling it in order to be able to restore
  1632. // the focus after, in _onSwitchedHost.
  1633. this.focusTool(this.currentToolId, false);
  1634. // Host code on the chrome side will send back a message once the host
  1635. // switched
  1636. this.postMessage({
  1637. name: "switch-host",
  1638. hostType
  1639. });
  1640. return this.once("host-changed");
  1641. },
  1642. _onSwitchedHost: function ({ hostType }) {
  1643. this._hostType = hostType;
  1644. this._buildDockButtons();
  1645. this._addKeysToWindow();
  1646. // We blurred the tools at start of switchHost, but also when clicking on
  1647. // host switching button. We now have to restore the focus.
  1648. this.focusTool(this.currentToolId, true);
  1649. this.emit("host-changed");
  1650. this._telemetry.log(HOST_HISTOGRAM, this._getTelemetryHostId());
  1651. },
  1652. /**
  1653. * Return if the tool is available as a tab (i.e. if it's checked
  1654. * in the options panel). This is different from Toolbox.getPanel -
  1655. * a tool could be registered but not yet opened in which case
  1656. * isToolRegistered would return true but getPanel would return false.
  1657. */
  1658. isToolRegistered: function (toolId) {
  1659. return gDevTools.getToolDefinitionMap().has(toolId);
  1660. },
  1661. /**
  1662. * Handler for the tool-registered event.
  1663. * @param {string} event
  1664. * Name of the event ("tool-registered")
  1665. * @param {string} toolId
  1666. * Id of the tool that was registered
  1667. */
  1668. _toolRegistered: function (event, toolId) {
  1669. let tool = gDevTools.getToolDefinition(toolId);
  1670. this._buildTabForTool(tool);
  1671. // Emit the event so tools can listen to it from the toolbox level
  1672. // instead of gDevTools
  1673. this.emit("tool-registered", toolId);
  1674. },
  1675. /**
  1676. * Handler for the tool-unregistered event.
  1677. * @param {string} event
  1678. * Name of the event ("tool-unregistered")
  1679. * @param {string|object} toolId
  1680. * Definition or id of the tool that was unregistered. Passing the
  1681. * tool id should be avoided as it is a temporary measure.
  1682. */
  1683. _toolUnregistered: function (event, toolId) {
  1684. if (typeof toolId != "string") {
  1685. toolId = toolId.id;
  1686. }
  1687. if (this._toolPanels.has(toolId)) {
  1688. let instance = this._toolPanels.get(toolId);
  1689. instance.destroy();
  1690. this._toolPanels.delete(toolId);
  1691. }
  1692. let radio = this.doc.getElementById("toolbox-tab-" + toolId);
  1693. let panel = this.doc.getElementById("toolbox-panel-" + toolId);
  1694. if (radio) {
  1695. if (this.currentToolId == toolId) {
  1696. let nextToolName = null;
  1697. if (radio.nextSibling) {
  1698. nextToolName = radio.nextSibling.getAttribute("toolid");
  1699. }
  1700. if (radio.previousSibling) {
  1701. nextToolName = radio.previousSibling.getAttribute("toolid");
  1702. }
  1703. if (nextToolName) {
  1704. this.selectTool(nextToolName);
  1705. }
  1706. }
  1707. radio.parentNode.removeChild(radio);
  1708. }
  1709. if (panel) {
  1710. panel.parentNode.removeChild(panel);
  1711. }
  1712. if (this.hostType == Toolbox.HostType.WINDOW) {
  1713. let doc = this.win.parent.document;
  1714. let key = doc.getElementById("key_" + toolId);
  1715. if (key) {
  1716. key.parentNode.removeChild(key);
  1717. }
  1718. }
  1719. // Emit the event so tools can listen to it from the toolbox level
  1720. // instead of gDevTools
  1721. this.emit("tool-unregistered", toolId);
  1722. },
  1723. /**
  1724. * Initialize the inspector/walker/selection/highlighter fronts.
  1725. * Returns a promise that resolves when the fronts are initialized
  1726. */
  1727. initInspector: function () {
  1728. if (!this._initInspector) {
  1729. this._initInspector = Task.spawn(function* () {
  1730. this._inspector = InspectorFront(this._target.client, this._target.form);
  1731. let pref = "devtools.inspector.showAllAnonymousContent";
  1732. let showAllAnonymousContent = Services.prefs.getBoolPref(pref);
  1733. this._walker = yield this._inspector.getWalker({ showAllAnonymousContent });
  1734. this._selection = new Selection(this._walker);
  1735. if (this.highlighterUtils.isRemoteHighlightable()) {
  1736. this.walker.on("highlighter-ready", this._highlighterReady);
  1737. this.walker.on("highlighter-hide", this._highlighterHidden);
  1738. let autohide = !flags.testing;
  1739. this._highlighter = yield this._inspector.getHighlighter(autohide);
  1740. }
  1741. }.bind(this));
  1742. }
  1743. return this._initInspector;
  1744. },
  1745. /**
  1746. * Destroy the inspector/walker/selection fronts
  1747. * Returns a promise that resolves when the fronts are destroyed
  1748. */
  1749. destroyInspector: function () {
  1750. if (this._destroyingInspector) {
  1751. return this._destroyingInspector;
  1752. }
  1753. this._destroyingInspector = Task.spawn(function* () {
  1754. if (!this._inspector) {
  1755. return;
  1756. }
  1757. // Releasing the walker (if it has been created)
  1758. // This can fail, but in any case, we want to continue destroying the
  1759. // inspector/highlighter/selection
  1760. // FF42+: Inspector actor starts managing Walker actor and auto destroy it.
  1761. if (this._walker && !this.walker.traits.autoReleased) {
  1762. try {
  1763. yield this._walker.release();
  1764. } catch (e) {
  1765. // Do nothing;
  1766. }
  1767. }
  1768. yield this.highlighterUtils.stopPicker();
  1769. yield this._inspector.destroy();
  1770. if (this._highlighter) {
  1771. // Note that if the toolbox is closed, this will work fine, but will fail
  1772. // in case the browser is closed and will trigger a noSuchActor message.
  1773. // We ignore the promise that |_hideBoxModel| returns, since we should still
  1774. // proceed with the rest of destruction if it fails.
  1775. // FF42+ now does the cleanup from the actor.
  1776. if (!this.highlighter.traits.autoHideOnDestroy) {
  1777. this.highlighterUtils.unhighlight();
  1778. }
  1779. yield this._highlighter.destroy();
  1780. }
  1781. if (this._selection) {
  1782. this._selection.destroy();
  1783. }
  1784. if (this.walker) {
  1785. this.walker.off("highlighter-ready", this._highlighterReady);
  1786. this.walker.off("highlighter-hide", this._highlighterHidden);
  1787. }
  1788. this._inspector = null;
  1789. this._highlighter = null;
  1790. this._selection = null;
  1791. this._walker = null;
  1792. }.bind(this));
  1793. return this._destroyingInspector;
  1794. },
  1795. /**
  1796. * Get the toolbox's notification component
  1797. *
  1798. * @return The notification box component.
  1799. */
  1800. getNotificationBox: function () {
  1801. return this.notificationBox;
  1802. },
  1803. /**
  1804. * Remove all UI elements, detach from target and clear up
  1805. */
  1806. destroy: function () {
  1807. // If several things call destroy then we give them all the same
  1808. // destruction promise so we're sure to destroy only once
  1809. if (this._destroyer) {
  1810. return this._destroyer;
  1811. }
  1812. let deferred = defer();
  1813. this._destroyer = deferred.promise;
  1814. this.emit("destroy");
  1815. this._target.off("navigate", this._refreshHostTitle);
  1816. this._target.off("frame-update", this._updateFrames);
  1817. this.off("select", this._refreshHostTitle);
  1818. this.off("host-changed", this._refreshHostTitle);
  1819. this.off("ready", this._showDevEditionPromo);
  1820. gDevTools.off("tool-registered", this._toolRegistered);
  1821. gDevTools.off("tool-unregistered", this._toolUnregistered);
  1822. gDevTools.off("pref-changed", this._prefChanged);
  1823. this._lastFocusedElement = null;
  1824. if (this._sourceMapService) {
  1825. this._sourceMapService.destroy();
  1826. this._sourceMapService = null;
  1827. }
  1828. if (this.webconsolePanel) {
  1829. this._saveSplitConsoleHeight();
  1830. this.webconsolePanel.removeEventListener("resize",
  1831. this._saveSplitConsoleHeight);
  1832. this.webconsolePanel = null;
  1833. }
  1834. if (this.closeButton) {
  1835. this.closeButton.removeEventListener("click", this.destroy, true);
  1836. this.closeButton = null;
  1837. }
  1838. if (this.textBoxContextMenuPopup) {
  1839. this.textBoxContextMenuPopup.removeEventListener("popupshowing",
  1840. this._updateTextBoxMenuItems, true);
  1841. this.textBoxContextMenuPopup = null;
  1842. }
  1843. if (this.tabbar) {
  1844. this.tabbar.removeEventListener("focus", this._onTabbarFocus, true);
  1845. this.tabbar.removeEventListener("click", this._onTabbarFocus, true);
  1846. this.tabbar.removeEventListener("keypress", this._onTabbarArrowKeypress);
  1847. this.tabbar = null;
  1848. }
  1849. let outstanding = [];
  1850. for (let [id, panel] of this._toolPanels) {
  1851. try {
  1852. gDevTools.emit(id + "-destroy", this, panel);
  1853. this.emit(id + "-destroy", panel);
  1854. outstanding.push(panel.destroy());
  1855. } catch (e) {
  1856. // We don't want to stop here if any panel fail to close.
  1857. console.error("Panel " + id + ":", e);
  1858. }
  1859. }
  1860. this.browserRequire = null;
  1861. // Now that we are closing the toolbox we can re-enable the cache settings
  1862. // and disable the service workers testing settings for the current tab.
  1863. // FF41+ automatically cleans up state in actor on disconnect.
  1864. if (this.target.activeTab && !this.target.activeTab.traits.noTabReconfigureOnClose) {
  1865. this.target.activeTab.reconfigure({
  1866. "cacheDisabled": false,
  1867. "serviceWorkersTestingEnabled": false
  1868. });
  1869. }
  1870. // Destroying the walker and inspector fronts
  1871. outstanding.push(this.destroyInspector().then(() => {
  1872. // Removing buttons
  1873. if (this._pickerButton) {
  1874. this._pickerButton.removeEventListener("click", this._togglePicker, false);
  1875. this._pickerButton = null;
  1876. }
  1877. }));
  1878. // Destroy the profiler connection
  1879. outstanding.push(this.destroyPerformance());
  1880. // Detach the thread
  1881. detachThread(this._threadClient);
  1882. this._threadClient = null;
  1883. // We need to grab a reference to win before this._host is destroyed.
  1884. let win = this.win;
  1885. if (this._requisition) {
  1886. CommandUtils.destroyRequisition(this._requisition, this.target);
  1887. }
  1888. this._telemetry.toolClosed("toolbox");
  1889. this._telemetry.destroy();
  1890. // Finish all outstanding tasks (which means finish destroying panels and
  1891. // then destroying the host, successfully or not) before destroying the
  1892. // target.
  1893. deferred.resolve(settleAll(outstanding)
  1894. .catch(console.error)
  1895. .then(() => {
  1896. this._removeHostListeners();
  1897. // `location` may already be null if the toolbox document is already
  1898. // in process of destruction. Otherwise if it is still around, ensure
  1899. // releasing toolbox document and triggering cleanup thanks to unload
  1900. // event. We do that precisely here, before nullifying the target as
  1901. // various cleanup code depends on the target attribute to be still
  1902. // defined.
  1903. if (win.location) {
  1904. win.location.replace("about:blank");
  1905. }
  1906. // Targets need to be notified that the toolbox is being torn down.
  1907. // This is done after other destruction tasks since it may tear down
  1908. // fronts and the debugger transport which earlier destroy methods may
  1909. // require to complete.
  1910. if (!this._target) {
  1911. return null;
  1912. }
  1913. let target = this._target;
  1914. this._target = null;
  1915. this.highlighterUtils.release();
  1916. target.off("close", this.destroy);
  1917. return target.destroy();
  1918. }, console.error).then(() => {
  1919. this.emit("destroyed");
  1920. // Free _host after the call to destroyed in order to let a chance
  1921. // to destroyed listeners to still query toolbox attributes
  1922. this._host = null;
  1923. this._win = null;
  1924. this._toolPanels.clear();
  1925. // Force GC to prevent long GC pauses when running tests and to free up
  1926. // memory in general when the toolbox is closed.
  1927. if (flags.testing) {
  1928. win.QueryInterface(Ci.nsIInterfaceRequestor)
  1929. .getInterface(Ci.nsIDOMWindowUtils)
  1930. .garbageCollect();
  1931. }
  1932. }).then(null, console.error));
  1933. let leakCheckObserver = ({wrappedJSObject: barrier}) => {
  1934. // Make the leak detector wait until this toolbox is properly destroyed.
  1935. barrier.client.addBlocker("DevTools: Wait until toolbox is destroyed",
  1936. this._destroyer);
  1937. };
  1938. let topic = "shutdown-leaks-before-check";
  1939. Services.obs.addObserver(leakCheckObserver, topic, false);
  1940. this._destroyer.then(() => {
  1941. Services.obs.removeObserver(leakCheckObserver, topic);
  1942. });
  1943. return this._destroyer;
  1944. },
  1945. _highlighterReady: function () {
  1946. this.emit("highlighter-ready");
  1947. },
  1948. _highlighterHidden: function () {
  1949. this.emit("highlighter-hide");
  1950. },
  1951. /**
  1952. * For displaying the promotional Doorhanger on first opening of
  1953. * the developer tools, promoting the Developer Edition.
  1954. */
  1955. _showDevEditionPromo: function () {
  1956. // Do not display in browser toolbox
  1957. if (this.target.chrome) {
  1958. return;
  1959. }
  1960. showDoorhanger({ window: this.win, type: "deveditionpromo" });
  1961. },
  1962. /**
  1963. * Enable / disable necessary textbox menu items using globalOverlay.js.
  1964. */
  1965. _updateTextBoxMenuItems: function () {
  1966. let window = this.win;
  1967. ["cmd_undo", "cmd_delete", "cmd_cut",
  1968. "cmd_copy", "cmd_paste", "cmd_selectAll"].forEach(window.goUpdateCommand);
  1969. },
  1970. /**
  1971. * Open the textbox context menu at given coordinates.
  1972. * Panels in the toolbox can call this on contextmenu events with event.screenX/Y
  1973. * instead of having to implement their own copy/paste/selectAll menu.
  1974. * @param {Number} x
  1975. * @param {Number} y
  1976. */
  1977. openTextBoxContextMenu: function (x, y) {
  1978. this.textBoxContextMenuPopup.openPopupAtScreen(x, y, true);
  1979. },
  1980. /**
  1981. * Connects to the SPS profiler when the developer tools are open. This is
  1982. * necessary because of the WebConsole's `profile` and `profileEnd` methods.
  1983. */
  1984. initPerformance: Task.async(function* () {
  1985. // If target does not have profiler actor (addons), do not
  1986. // even register the shared performance connection.
  1987. if (!this.target.hasActor("profiler")) {
  1988. return promise.resolve();
  1989. }
  1990. if (this._performanceFrontConnection) {
  1991. return this._performanceFrontConnection.promise;
  1992. }
  1993. this._performanceFrontConnection = defer();
  1994. this._performance = createPerformanceFront(this._target);
  1995. yield this.performance.connect();
  1996. // Emit an event when connected, but don't wait on startup for this.
  1997. this.emit("profiler-connected");
  1998. this.performance.on("*", this._onPerformanceFrontEvent);
  1999. this._performanceFrontConnection.resolve(this.performance);
  2000. return this._performanceFrontConnection.promise;
  2001. }),
  2002. /**
  2003. * Disconnects the underlying Performance actor. If the connection
  2004. * has not finished initializing, as opening a toolbox does not wait,
  2005. * the performance connection destroy method will wait for it on its own.
  2006. */
  2007. destroyPerformance: Task.async(function* () {
  2008. if (!this.performance) {
  2009. return;
  2010. }
  2011. // If still connecting to performance actor, allow the
  2012. // actor to resolve its connection before attempting to destroy.
  2013. if (this._performanceFrontConnection) {
  2014. yield this._performanceFrontConnection.promise;
  2015. }
  2016. this.performance.off("*", this._onPerformanceFrontEvent);
  2017. yield this.performance.destroy();
  2018. this._performance = null;
  2019. }),
  2020. /**
  2021. * Called when any event comes from the PerformanceFront. If the performance tool is
  2022. * already loaded when the first event comes in, immediately unbind this handler, as
  2023. * this is only used to queue up observed recordings before the performance tool can
  2024. * handle them, which will only occur when `console.profile()` recordings are started
  2025. * before the tool loads.
  2026. */
  2027. _onPerformanceFrontEvent: Task.async(function* (eventName, recording) {
  2028. if (this.getPanel("performance")) {
  2029. this.performance.off("*", this._onPerformanceFrontEvent);
  2030. return;
  2031. }
  2032. this._performanceQueuedRecordings = this._performanceQueuedRecordings || [];
  2033. let recordings = this._performanceQueuedRecordings;
  2034. // Before any console recordings, we'll get a `console-profile-start` event
  2035. // warning us that a recording will come later (via `recording-started`), so
  2036. // start to boot up the tool and populate the tool with any other recordings
  2037. // observed during that time.
  2038. if (eventName === "console-profile-start" && !this._performanceToolOpenedViaConsole) {
  2039. this._performanceToolOpenedViaConsole = this.loadTool("performance");
  2040. let panel = yield this._performanceToolOpenedViaConsole;
  2041. yield panel.open();
  2042. panel.panelWin.PerformanceController.populateWithRecordings(recordings);
  2043. this.performance.off("*", this._onPerformanceFrontEvent);
  2044. }
  2045. // Otherwise, if it's a recording-started event, we've already started loading
  2046. // the tool, so just store this recording in our array to be later populated
  2047. // once the tool loads.
  2048. if (eventName === "recording-started") {
  2049. recordings.push(recording);
  2050. }
  2051. }),
  2052. /**
  2053. * Returns gViewSourceUtils for viewing source.
  2054. */
  2055. get gViewSourceUtils() {
  2056. return this.win.gViewSourceUtils;
  2057. },
  2058. /**
  2059. * Opens source in style editor. Falls back to plain "view-source:".
  2060. * @see devtools/client/shared/source-utils.js
  2061. */
  2062. viewSourceInStyleEditor: function (sourceURL, sourceLine) {
  2063. return viewSource.viewSourceInStyleEditor(this, sourceURL, sourceLine);
  2064. },
  2065. /**
  2066. * Opens source in debugger. Falls back to plain "view-source:".
  2067. * @see devtools/client/shared/source-utils.js
  2068. */
  2069. viewSourceInDebugger: function (sourceURL, sourceLine) {
  2070. return viewSource.viewSourceInDebugger(this, sourceURL, sourceLine);
  2071. },
  2072. /**
  2073. * Opens source in scratchpad. Falls back to plain "view-source:".
  2074. * TODO The `sourceURL` for scratchpad instances are like `Scratchpad/1`.
  2075. * If instances are scoped one-per-browser-window, then we should be able
  2076. * to infer the URL from this toolbox, or use the built in scratchpad IN
  2077. * the toolbox.
  2078. *
  2079. * @see devtools/client/shared/source-utils.js
  2080. */
  2081. viewSourceInScratchpad: function (sourceURL, sourceLine) {
  2082. return viewSource.viewSourceInScratchpad(sourceURL, sourceLine);
  2083. },
  2084. /**
  2085. * Opens source in plain "view-source:".
  2086. * @see devtools/client/shared/source-utils.js
  2087. */
  2088. viewSource: function (sourceURL, sourceLine) {
  2089. return viewSource.viewSource(this, sourceURL, sourceLine);
  2090. },
  2091. };