toolbox-host-manager.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. const Services = require("Services");
  2. const {Ci} = require("chrome");
  3. const {LocalizationHelper} = require("devtools/shared/l10n");
  4. const L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
  5. const DevToolsUtils = require("devtools/shared/DevToolsUtils");
  6. const {Task} = require("devtools/shared/task");
  7. loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
  8. loader.lazyRequireGetter(this, "Hosts", "devtools/client/framework/toolbox-hosts", true);
  9. /**
  10. * Implement a wrapper on the chrome side to setup a Toolbox within Firefox UI.
  11. *
  12. * This component handles iframe creation within Firefox, in which we are loading
  13. * the toolbox document. Then both the chrome and the toolbox document communicate
  14. * via "message" events.
  15. *
  16. * Messages sent by the toolbox to the chrome:
  17. * - switch-host:
  18. * Order to display the toolbox in another host (side, bottom, window, or the
  19. * previously used one)
  20. * - toggle-minimize-mode:
  21. * When using the bottom host, the toolbox can be miximized to only display
  22. * the tool titles
  23. * - maximize-host:
  24. * When using the bottom host in minimized mode, revert back to regular mode
  25. * in order to see tool titles and the tools
  26. * - raise-host:
  27. * Focus the tools
  28. * - set-host-title:
  29. * When using the window host, update the window title
  30. *
  31. * Messages sent by the chrome to the toolbox:
  32. * - host-minimized:
  33. * The bottom host is done minimizing (after animation end)
  34. * - host-maximized:
  35. * The bottom host is done switching back to regular mode (after animation
  36. * end)
  37. * - switched-host:
  38. * The `switch-host` command sent by the toolbox is done
  39. */
  40. const LAST_HOST = "devtools.toolbox.host";
  41. const PREVIOUS_HOST = "devtools.toolbox.previousHost";
  42. let ID_COUNTER = 1;
  43. function ToolboxHostManager(target, hostType, hostOptions) {
  44. this.target = target;
  45. this.frameId = ID_COUNTER++;
  46. if (!hostType) {
  47. hostType = Services.prefs.getCharPref(LAST_HOST);
  48. }
  49. this.onHostMinimized = this.onHostMinimized.bind(this);
  50. this.onHostMaximized = this.onHostMaximized.bind(this);
  51. this.host = this.createHost(hostType, hostOptions);
  52. this.hostType = hostType;
  53. }
  54. ToolboxHostManager.prototype = {
  55. create: Task.async(function* (toolId) {
  56. yield this.host.create();
  57. this.host.frame.setAttribute("aria-label", L10N.getStr("toolbox.label"));
  58. this.host.frame.ownerDocument.defaultView.addEventListener("message", this);
  59. // We have to listen on capture as no event fires on bubble
  60. this.host.frame.addEventListener("unload", this, true);
  61. let toolbox = new Toolbox(this.target, toolId, this.host.type, this.host.frame.contentWindow, this.frameId);
  62. // Prevent reloading the toolbox when loading the tools in a tab (e.g. from about:debugging)
  63. if (!this.host.frame.contentWindow.location.href.startsWith("about:devtools-toolbox")) {
  64. this.host.frame.setAttribute("src", "about:devtools-toolbox");
  65. }
  66. return toolbox;
  67. }),
  68. handleEvent(event) {
  69. switch(event.type) {
  70. case "message":
  71. this.onMessage(event);
  72. break;
  73. case "unload":
  74. // On unload, host iframe already lost its contentWindow attribute, so
  75. // we can only compare against locations. Here we filter two very
  76. // different cases: preliminary about:blank document as well as iframes
  77. // like tool iframes.
  78. if (!event.target.location.href.startsWith("about:devtools-toolbox")) {
  79. break;
  80. }
  81. // Don't destroy the host during unload event (esp., don't remove the
  82. // iframe from DOM!). Otherwise the unload event for the toolbox
  83. // document doesn't fire within the toolbox *document*! This is
  84. // the unload event that fires on the toolbox *iframe*.
  85. DevToolsUtils.executeSoon(() => {
  86. this.destroy();
  87. });
  88. break;
  89. }
  90. },
  91. onMessage(event) {
  92. if (!event.data) {
  93. return;
  94. }
  95. // Toolbox document is still chrome and disallow identifying message
  96. // origin via event.source as it is null. So use a custom id.
  97. if (event.data.frameId != this.frameId) {
  98. return;
  99. }
  100. switch (event.data.name) {
  101. case "switch-host":
  102. this.switchHost(event.data.hostType);
  103. break;
  104. case "maximize-host":
  105. this.host.maximize();
  106. break;
  107. case "raise-host":
  108. this.host.raise();
  109. break;
  110. case "toggle-minimize-mode":
  111. this.host.toggleMinimizeMode(event.data.toolbarHeight);
  112. break;
  113. case "set-host-title":
  114. this.host.setTitle(event.data.title);
  115. break;
  116. }
  117. },
  118. postMessage(data) {
  119. let window = this.host.frame.contentWindow;
  120. window.postMessage(data, "*");
  121. },
  122. destroy() {
  123. this.destroyHost();
  124. this.host = null;
  125. this.hostType = null;
  126. this.target = null;
  127. },
  128. /**
  129. * Create a host object based on the given host type.
  130. *
  131. * Warning: bottom and sidebar hosts require that the toolbox target provides
  132. * a reference to the attached tab. Not all Targets have a tab property -
  133. * make sure you correctly mix and match hosts and targets.
  134. *
  135. * @param {string} hostType
  136. * The host type of the new host object
  137. *
  138. * @return {Host} host
  139. * The created host object
  140. */
  141. createHost(hostType, options) {
  142. if (!Hosts[hostType]) {
  143. throw new Error("Unknown hostType: " + hostType);
  144. }
  145. let newHost = new Hosts[hostType](this.target.tab, options);
  146. // Update the label and icon when the state changes.
  147. newHost.on("minimized", this.onHostMinimized);
  148. newHost.on("maximized", this.onHostMaximized);
  149. return newHost;
  150. },
  151. onHostMinimized() {
  152. this.postMessage({
  153. name: "host-minimized"
  154. });
  155. },
  156. onHostMaximized() {
  157. this.postMessage({
  158. name: "host-maximized"
  159. });
  160. },
  161. switchHost: Task.async(function* (hostType) {
  162. if (hostType == "previous") {
  163. // Switch to the last used host for the toolbox UI.
  164. // This is determined by the devtools.toolbox.previousHost pref.
  165. hostType = Services.prefs.getCharPref(PREVIOUS_HOST);
  166. // Handle the case where the previous host happens to match the current
  167. // host. If so, switch to bottom if it's not already used, and side if not.
  168. if (hostType === this.hostType) {
  169. if (hostType === Toolbox.HostType.BOTTOM) {
  170. hostType = Toolbox.HostType.SIDE;
  171. } else {
  172. hostType = Toolbox.HostType.BOTTOM;
  173. }
  174. }
  175. }
  176. let iframe = this.host.frame;
  177. let newHost = this.createHost(hostType);
  178. let newIframe = yield newHost.create();
  179. // change toolbox document's parent to the new host
  180. newIframe.swapFrameLoaders(iframe);
  181. this.destroyHost();
  182. if (this.hostType != Toolbox.HostType.CUSTOM) {
  183. Services.prefs.setCharPref(PREVIOUS_HOST, this.hostType);
  184. }
  185. this.host = newHost;
  186. this.hostType = hostType;
  187. this.host.setTitle(this.host.frame.contentWindow.document.title);
  188. this.host.frame.ownerDocument.defaultView.addEventListener("message", this);
  189. this.host.frame.addEventListener("unload", this, true);
  190. if (hostType != Toolbox.HostType.CUSTOM) {
  191. Services.prefs.setCharPref(LAST_HOST, hostType);
  192. }
  193. // Tell the toolbox the host changed
  194. this.postMessage({
  195. name: "switched-host",
  196. hostType
  197. });
  198. }),
  199. /**
  200. * Destroy the current host, and remove event listeners from its frame.
  201. *
  202. * @return {promise} to be resolved when the host is destroyed.
  203. */
  204. destroyHost() {
  205. // When Firefox toplevel is closed, the frame may already be detached and
  206. // the top level document gone
  207. if (this.host.frame.ownerDocument.defaultView) {
  208. this.host.frame.ownerDocument.defaultView.removeEventListener("message", this);
  209. }
  210. this.host.frame.removeEventListener("unload", this, true);
  211. this.host.off("minimized", this.onHostMinimized);
  212. this.host.off("maximized", this.onHostMaximized);
  213. return this.host.destroy();
  214. }
  215. };
  216. exports.ToolboxHostManager = ToolboxHostManager;