Bug 1518512 - (Part 1) Add basic context menu to Changes panel. r=gl
☠☠ backed out by a7918210d2f1 ☠ ☠
authorRazvan Caliman <rcaliman@mozilla.com>
Wed, 23 Jan 2019 14:00:10 +0000
changeset 515427 3c03e2282b4f180cbc6dfd2c37ac23d192050fe1
parent 515426 3b8aae988f23624d43c48123987476f736a2720f
child 515428 c8f0d19844f67cd9a2189f612e4f1a865d697b04
push id1953
push userffxbld-merge
push dateMon, 11 Mar 2019 12:10:20 +0000
treeherdermozilla-release@9c35dcbaa899 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgl
bugs1518512
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1518512 - (Part 1) Add basic context menu to Changes panel. r=gl Adds context menu with options to select all and copy text content from the Changes panel. Differential Revision: https://phabricator.services.mozilla.com/D17255
devtools/client/inspector/changes/ChangesContextMenu.js
devtools/client/inspector/changes/ChangesView.js
devtools/client/inspector/changes/components/ChangesApp.js
devtools/client/inspector/changes/moz.build
devtools/client/locales/en-US/changes.properties
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/ChangesContextMenu.js
@@ -0,0 +1,99 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+loader.lazyRequireGetter(this, "Menu", "devtools/client/framework/menu");
+loader.lazyRequireGetter(this, "MenuItem", "devtools/client/framework/menu-item");
+loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
+
+const { getStr } = require("./utils/l10n");
+
+/**
+ * Context menu for the Changes panel with options to select, copy and export CSS changes.
+ */
+class ChangesContextMenu {
+  /**
+   * @param {ChangesView} view
+   */
+  constructor(view) {
+    this.view = view;
+    this.inspector = this.view.inspector;
+    // Document object to which the Changes panel belongs to.
+    this.document = this.view.document;
+    // DOM element container for the Changes panel content.
+    this.panel = this.document.getElementById("sidebar-panel-changes");
+    // Window object to which the Changes panel belongs to.
+    this.window = this.document.defaultView;
+
+    this._onCopy = this._onCopy.bind(this);
+    this._onSelectAll = this._onSelectAll.bind(this);
+  }
+
+  show(event) {
+    this._openMenu({
+      target: event.explicitOriginalTarget,
+      screenX: event.screenX,
+      screenY: event.screenY,
+    });
+  }
+
+  _openMenu({ target, screenX = 0, screenY = 0 } = {}) {
+    this.window.focus();
+
+    const menu = new Menu();
+
+    // Copy option
+    const menuitemCopy = new MenuItem({
+      label: getStr("changes.contextmenu.copy"),
+      accesskey: getStr("changes.contextmenu.copy.accessKey"),
+      click: this._onCopy,
+      disabled: !this._hasTextSelected(),
+    });
+    menu.append(menuitemCopy);
+
+    // Select All option
+    const menuitemSelectAll = new MenuItem({
+      label: getStr("changes.contextmenu.selectAll"),
+      accesskey: getStr("changes.contextmenu.selectAll.accessKey"),
+      click: this._onSelectAll,
+    });
+    menu.append(menuitemSelectAll);
+
+    menu.popup(screenX, screenY, this.inspector.toolbox);
+    return menu;
+  }
+
+  _hasTextSelected() {
+    const selection = this.window.getSelection();
+    return selection.toString() && !selection.isCollapsed;
+  }
+
+  /**
+   * Select all text.
+   */
+  _onSelectAll() {
+    const selection = this.window.getSelection();
+    selection.selectAllChildren(this.panel);
+  }
+
+  /**
+   * Copy the selected text to clipboard.
+   */
+  _onCopy() {
+    const text = this.window.getSelection().toString();
+    clipboardHelper.copyString(text);
+  }
+
+  destroy() {
+    this.inspector = null;
+    this.panel = null;
+    this.view = null;
+    this.window = null;
+  }
+}
+
+module.exports = ChangesContextMenu;
--- a/devtools/client/inspector/changes/ChangesView.js
+++ b/devtools/client/inspector/changes/ChangesView.js
@@ -4,40 +4,52 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { createFactory, createElement } = require("devtools/client/shared/vendor/react");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 
+loader.lazyRequireGetter(this, "ChangesContextMenu", "devtools/client/inspector/changes/ChangesContextMenu");
+
 const ChangesApp = createFactory(require("./components/ChangesApp"));
 
 const {
   resetChanges,
   trackChange,
 } = require("./actions/changes");
 
 class ChangesView {
   constructor(inspector, window) {
     this.document = window.document;
     this.inspector = inspector;
     this.store = this.inspector.store;
-    this.toolbox = this.inspector.toolbox;
 
     this.onAddChange = this.onAddChange.bind(this);
     this.onClearChanges = this.onClearChanges.bind(this);
     this.onChangesFront = this.onChangesFront.bind(this);
+    this.onContextMenu = this.onContextMenu.bind(this);
     this.destroy = this.destroy.bind(this);
 
     this.init();
   }
 
+  get contextMenu() {
+    if (!this._contextMenu) {
+      this._contextMenu = new ChangesContextMenu(this);
+    }
+
+    return this._contextMenu;
+  }
+
   init() {
-    const changesApp = ChangesApp({});
+    const changesApp = ChangesApp({
+      onContextMenu: this.onContextMenu,
+    });
 
     // listen to the front for initialization, add listeners
     // when it is ready
     this._getChangesFront();
 
     // Expose the provider to let inspector.js use it in setupSidebar.
     this.provider = createElement(Provider, {
       id: "changesview",
@@ -83,27 +95,35 @@ class ChangesView {
     // Turn data into a suitable change to send to the store.
     this.store.dispatch(trackChange(change));
   }
 
   onClearChanges() {
     this.store.dispatch(resetChanges());
   }
 
+  onContextMenu(e) {
+    this.contextMenu.show(e);
+  }
+
   /**
    * Destruction function called when the inspector is destroyed.
    */
   async destroy() {
     this.store.dispatch(resetChanges());
 
     // ensure we finish waiting for the front before destroying.
     const changesFront = await this.changesFrontPromise;
     changesFront.off("add-change", this.onAddChange);
     changesFront.off("clear-changes", this.onClearChanges);
 
     this.document = null;
     this.inspector = null;
     this.store = null;
-    this.toolbox = null;
+
+    if (this._contextMenu) {
+      this._contextMenu.destroy();
+      this._contextMenu = null;
+    }
   }
 }
 
 module.exports = ChangesView;
--- a/devtools/client/inspector/changes/components/ChangesApp.js
+++ b/devtools/client/inspector/changes/components/ChangesApp.js
@@ -14,16 +14,18 @@ const { getChangesTree } = require("../s
 const { getSourceForDisplay } = require("../utils/changes-utils");
 const { getStr } = require("../utils/l10n");
 
 class ChangesApp extends PureComponent {
   static get propTypes() {
     return {
       // Nested CSS rule tree structure of CSS changes grouped by source (stylesheet)
       changesTree: PropTypes.object.isRequired,
+      // Event handler for "contextmenu" event
+      onContextMenu: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
   }
 
   renderDeclarations(remove = [], add = []) {
@@ -135,16 +137,17 @@ class ChangesApp extends PureComponent {
   }
 
   render() {
     const hasChanges = Object.keys(this.props.changesTree).length > 0;
     return dom.div(
       {
         className: "theme-sidebar inspector-tabpanel",
         id: "sidebar-panel-changes",
+        onContextMenu: this.props.onContextMenu,
       },
       !hasChanges && this.renderEmptyState(),
       hasChanges && this.renderDiff(this.props.changesTree)
     );
   }
 }
 
 const mapStateToProps = state => {
--- a/devtools/client/inspector/changes/moz.build
+++ b/devtools/client/inspector/changes/moz.build
@@ -8,12 +8,13 @@ DIRS += [
     'actions',
     'components',
     'reducers',
     'selectors',
     'utils',
 ]
 
 DevToolsModules(
+    'ChangesContextMenu.js',
     'ChangesView.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/locales/en-US/changes.properties
+++ b/devtools/client/locales/en-US/changes.properties
@@ -19,8 +19,24 @@ changes.inlineStyleSheetLabel=Inline %S
 
 # LOCALIZATION NOTE (changes.elementStyleLabel): This label appears in the Changes
 # panel above changes done to element styles.
 changes.elementStyleLabel=Element
 
 # LOCALIZATION NOTE (changes.iframeLabel): This label appears next to URLs of stylesheets
 # and element inline styles hosted by iframes. Lowercase intentional.
 changes.iframeLabel=iframe
+
+# LOCALIZATION NOTE (changes.contextmenu.copy): Label for "Copy" option in Changes panel
+# context menu
+changes.contextmenu.copy=Copy
+
+# LOCALIZATION NOTE (changes.contextmenu.copy.accessKey): Access key for "Copy"
+# option in the Changes panel.
+changes.contextmenu.copy.accessKey=C
+
+# LOCALIZATION NOTE (changes.contextmenu.selectAll): Label for "Select All" option in the
+# Changes panel context menu to select all text content.
+changes.contextmenu.selectAll=Select All
+
+# LOCALIZATION NOTE (changes.contextmenu.selectAll.accessKey): Access key for "Select All"
+# option in the Changes panel.
+changes.contextmenu.selectAll.accessKey=A