Bug 1029371 - Make style editor media sidebar interact with media sidebar. r=bgrins r=paul r=mgoodwin
authorTim Nguyen <ntim.bugs@gmail.com>
Wed, 18 Feb 2015 03:39:00 +0100
changeset 277864 075e39cc5f6e423d02aa4ba6613e06e94555f9d2
parent 277863 f28fd9bd845c35867d9089ea73be03d10cf68cd8
child 277865 81e422d038d3752790fb8b073faed8351eaf3e16
push id69628
push usercbook@mozilla.com
push dateWed, 30 Dec 2015 11:16:09 +0000
treeherdermozilla-inbound@b493cf33851f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins, paul, mgoodwin
bugs1029371
milestone46.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 1029371 - Make style editor media sidebar interact with media sidebar. r=bgrins r=paul r=mgoodwin
devtools/client/responsivedesign/responsivedesign.jsm
devtools/client/styleeditor/StyleEditorUI.jsm
devtools/client/styleeditor/test/browser.ini
devtools/client/styleeditor/test/browser_styleeditor_media_sidebar.js
devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_links.js
devtools/client/styleeditor/test/media-rules.css
devtools/client/themes/styleeditor.css
--- a/devtools/client/responsivedesign/responsivedesign.jsm
+++ b/devtools/client/responsivedesign/responsivedesign.jsm
@@ -47,16 +47,28 @@ this.ResponsiveUIManager = {
    *
    * @param aWindow the main window.
    * @param aTab the tab targeted.
    */
   toggle: function(aWindow, aTab) {
     if (this.isActiveForTab(aTab)) {
       ActiveTabs.get(aTab).close();
     } else {
+      this.runIfNeeded(aWindow, aTab);
+    }
+  },
+
+  /**
+   * Launches the responsive mode.
+   *
+   * @param aWindow the main window.
+   * @param aTab the tab targeted.
+   */
+  runIfNeeded: function(aWindow, aTab) {
+    if (!this.isActiveForTab(aTab)) {
       new ResponsiveUI(aWindow, aTab);
     }
   },
 
   /**
    * Returns true if responsive view is active for the provided tab.
    *
    * @param aTab the tab targeted.
@@ -78,25 +90,21 @@ this.ResponsiveUIManager = {
    * @param aWindow the browser window.
    * @param aTab the tab targeted.
    * @param aCommand the command name.
    * @param aArgs command arguments.
    */
   handleGcliCommand: function(aWindow, aTab, aCommand, aArgs) {
     switch (aCommand) {
       case "resize to":
-        if (!this.isActiveForTab(aTab)) {
-          new ResponsiveUI(aWindow, aTab);
-        }
+        this.runIfNeeded(aWindow, aTab);
         ActiveTabs.get(aTab).setSize(aArgs.width, aArgs.height);
         break;
       case "resize on":
-        if (!this.isActiveForTab(aTab)) {
-          new ResponsiveUI(aWindow, aTab);
-        }
+        this.runIfNeeded(aWindow, aTab);
         break;
       case "resize off":
         if (this.isActiveForTab(aTab)) {
           ActiveTabs.get(aTab).close();
         }
         break;
       case "resize toggle":
           this.toggle(aWindow, aTab);
@@ -846,46 +854,48 @@ ResponsiveUI.prototype = {
 
   /**
    * Change the size of the browser.
    *
    * @param aWidth width of the browser.
    * @param aHeight height of the browser.
    */
   setSize: function RUI_setSize(aWidth, aHeight) {
-    aWidth = Math.min(Math.max(aWidth, MIN_WIDTH), MAX_WIDTH);
-    aHeight = Math.min(Math.max(aHeight, MIN_HEIGHT), MAX_HEIGHT);
+    this.setWidth(aWidth);
+    this.setHeight(aHeight);
+  },
 
-    // We resize the containing stack.
-    let style = "max-width: %width;" +
-                "min-width: %width;" +
-                "max-height: %height;" +
-                "min-height: %height;";
+  setWidth: function RUI_setWidth(aWidth) {
+    aWidth = Math.min(Math.max(aWidth, MIN_WIDTH), MAX_WIDTH);
+    this.stack.style.maxWidth = this.stack.style.minWidth = aWidth + "px";
 
-    style = style.replace(/%width/g, aWidth + "px");
-    style = style.replace(/%height/g, aHeight + "px");
-
-    this.stack.setAttribute("style", style);
-
-    if (!this.ignoreY)
-      this.resizeBarV.setAttribute("top", Math.round(aHeight / 2));
     if (!this.ignoreX)
       this.resizeBarH.setAttribute("left", Math.round(aWidth / 2));
 
     let selectedPreset = this.menuitems.get(this.selectedItem);
 
-    // We update the custom menuitem if we are using it
     if (selectedPreset.custom) {
       selectedPreset.width = aWidth;
-      selectedPreset.height = aHeight;
-
       this.setMenuLabel(this.selectedItem, selectedPreset);
     }
   },
 
+  setHeight: function RUI_setHeight(aHeight) {
+    aHeight = Math.min(Math.max(aHeight, MIN_HEIGHT), MAX_HEIGHT);
+    this.stack.style.maxHeight = this.stack.style.minHeight = aHeight + "px";
+
+    if (!this.ignoreY)
+      this.resizeBarV.setAttribute("top", Math.round(aHeight / 2));
+
+    let selectedPreset = this.menuitems.get(this.selectedItem);
+    if (selectedPreset.custom) {
+      selectedPreset.height = aHeight;
+      this.setMenuLabel(this.selectedItem, selectedPreset);
+    }
+  },
   /**
    * Start the process of resizing the browser.
    *
    * @param aEvent
    */
   startResizing: function RUI_startResizing(aEvent) {
     let selectedPreset = this.menuitems.get(this.selectedItem);
 
--- a/devtools/client/styleeditor/StyleEditorUI.jsm
+++ b/devtools/client/styleeditor/StyleEditorUI.jsm
@@ -6,35 +6,33 @@
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["StyleEditorUI"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/NetUtil.jsm");
-Cu.import("resource://gre/modules/osfile.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource://devtools/shared/event-emitter.js");
-Cu.import("resource://devtools/client/framework/gDevTools.jsm");
+const {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
+const {OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
+const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
+const EventEmitter = require("devtools/shared/event-emitter");
+const {gDevTools} = require("resource://devtools/client/framework/gDevTools.jsm");
 Cu.import("resource://devtools/client/styleeditor/StyleEditorUtil.jsm");
-Cu.import("resource://devtools/client/shared/SplitView.jsm");
-Cu.import("resource://devtools/client/styleeditor/StyleSheetEditor.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
-                                  "resource://gre/modules/PluralForm.jsm");
-
-const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
-const { PrefObserver, PREF_ORIG_SOURCES } = require("devtools/client/styleeditor/utils");
+const {SplitView} = Cu.import("resource://devtools/client/shared/SplitView.jsm", {});
+const {StyleSheetEditor} = Cu.import("resource://devtools/client/styleeditor/StyleSheetEditor.jsm");
+loader.lazyImporter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
+const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils");
 const csscoverage = require("devtools/server/actors/csscoverage");
-const console = require("resource://gre/modules/Console.jsm").console;
+const {console} = require("resource://gre/modules/Console.jsm");
 const promise = require("promise");
+const {ResponsiveUIManager} =
+  Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm", {});
 
 const LOAD_ERROR = "error-load";
 const STYLE_EDITOR_TEMPLATE = "stylesheet";
 const SELECTOR_HIGHLIGHTER_TYPE = "SelectorHighlighter";
 const PREF_MEDIA_SIDEBAR = "devtools.styleeditor.showMediaSidebar";
 const PREF_SIDEBAR_WIDTH = "devtools.styleeditor.mediaSidebarWidth";
 const PREF_NAV_WIDTH = "devtools.styleeditor.navSidebarWidth";
 
@@ -874,20 +872,24 @@ StyleEditorUI.prototype = {
         inSource = true;
 
         let div = this._panelDoc.createElement("div");
         div.className = "media-rule-label";
         div.addEventListener("click", this._jumpToLocation.bind(this, location));
 
         let cond = this._panelDoc.createElement("div");
         cond.textContent = rule.conditionText;
-        cond.className = "media-rule-condition"
+        cond.className = "media-rule-condition";
         if (!rule.matches) {
           cond.classList.add("media-condition-unmatched");
         }
+        if (this._target.tab.tagName == "tab") {
+          cond.innerHTML = cond.textContent.replace(/(min\-|max\-)(width|height):\s\d+(px)/ig, "<a href='#' class='media-responsive-mode-toggle'>$&</a>");
+          cond.addEventListener("click", this._onMediaConditionClick.bind(this));
+        }
         div.appendChild(cond);
 
         let link = this._panelDoc.createElement("div");
         link.className = "media-rule-line theme-link";
         if (location.line != -1) {
           link.textContent = ":" + location.line;
         }
         div.appendChild(link);
@@ -897,16 +899,59 @@ StyleEditorUI.prototype = {
 
       sidebar.hidden = !showSidebar || !inSource;
 
       this.emit("media-list-changed", editor);
     }.bind(this)).then(null, Cu.reportError);
   },
 
   /**
+    * Called when a media condition is clicked
+    * If a responsive mode link is clicked, it will launch it.
+    *
+    * @param {object} e
+    *        Event object
+    */
+  _onMediaConditionClick: function(e) {
+    if (!e.target.matches(".media-responsive-mode-toggle")) {
+      return;
+    }
+    let conditionText = e.target.textContent;
+    let isWidthCond = conditionText.toLowerCase().indexOf("width") > -1;
+    let mediaVal = parseInt(/\d+/.exec(conditionText));
+
+    let options = isWidthCond ? {width: mediaVal} : {height: mediaVal};
+    this._launchResponsiveMode(options);
+    e.preventDefault();
+    e.stopPropagation();
+  },
+
+  /**
+   * Launches the responsive mode with a specific width or height
+   *
+   * @param  {object} options
+   *         Object with width or/and height properties.
+   */
+  _launchResponsiveMode: function(options = {}) {
+    let tab = this._target.tab;
+    let win = this._target.tab.ownerGlobal;
+
+    ResponsiveUIManager.runIfNeeded(win, tab);
+    if (options.width && options.height) {
+      ResponsiveUIManager.getResponsiveUIForTab(tab).setSize(options.width, options.height);
+    }
+    else if (options.width) {
+      ResponsiveUIManager.getResponsiveUIForTab(tab).setWidth(options.width);
+    }
+    else if (options.height) {
+      ResponsiveUIManager.getResponsiveUIForTab(tab).setHeight(options.height);
+    }
+  },
+
+  /**
    * Jump cursor to the editor for a stylesheet and line number for a rule.
    *
    * @param  {object} location
    *         Location object with 'line', 'column', and 'source' properties.
    */
   _jumpToLocation: function(location) {
     let source = location.styleSheet || location.source;
     this.selectStyleSheet(source, location.line - 1, location.column - 1);
--- a/devtools/client/styleeditor/test/browser.ini
+++ b/devtools/client/styleeditor/test/browser.ini
@@ -61,16 +61,17 @@ support-files =
 [browser_styleeditor_filesave.js]
 [browser_styleeditor_highlight-selector.js]
 [browser_styleeditor_import.js]
 [browser_styleeditor_import_rule.js]
 [browser_styleeditor_init.js]
 [browser_styleeditor_inline_friendly_names.js]
 [browser_styleeditor_loading.js]
 [browser_styleeditor_media_sidebar.js]
+[browser_styleeditor_media_sidebar_links.js]
 [browser_styleeditor_media_sidebar_sourcemaps.js]
 [browser_styleeditor_missing_stylesheet.js]
 [browser_styleeditor_navigate.js]
 [browser_styleeditor_new.js]
 [browser_styleeditor_nostyle.js]
 [browser_styleeditor_opentab.js]
 [browser_styleeditor_pretty.js]
 [browser_styleeditor_private_perwindowpb.js]
--- a/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar.js
@@ -4,18 +4,19 @@
 
 "use strict";
 
 // https rather than chrome to improve coverage
 const TESTCASE_URI = TEST_BASE_HTTPS + "media-rules.html";
 const MEDIA_PREF = "devtools.styleeditor.showMediaSidebar";
 
 const RESIZE = 300;
-const LABELS = ["not all", "all", "(max-width: 400px)", "(max-width: 600px)"];
-const LINE_NOS = [1, 7, 19, 25];
+const LABELS = ["not all", "all", "(max-width: 400px)",
+                "(min-height: 200px) and (max-height: 250px)", "(max-width: 600px)"];
+const LINE_NOS = [1, 7, 19, 25, 30];
 const NEW_RULE = "\n@media (max-width: 600px) { div { color: blue; } }";
 
 waitForExplicitFinish();
 
 add_task(function*() {
   let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
 
   is(ui.editors.length, 2, "correct number of editors");
@@ -54,21 +55,22 @@ function testPlainEditor(editor) {
   is(sidebar.hidden, true, "sidebar is hidden on editor without @media");
 }
 
 function testMediaEditor(editor) {
   let sidebar = editor.details.querySelector(".stylesheet-sidebar");
   is(sidebar.hidden, false, "sidebar is showing on editor with @media");
 
   let entries = [...sidebar.querySelectorAll(".media-rule-label")];
-  is(entries.length, 3, "three @media rules displayed in sidebar");
+  is(entries.length, 4, "four @media rules displayed in sidebar");
 
   testRule(entries[0], LABELS[0], false, LINE_NOS[0]);
   testRule(entries[1], LABELS[1], true, LINE_NOS[1]);
   testRule(entries[2], LABELS[2], false, LINE_NOS[2]);
+  testRule(entries[3], LABELS[3], false, LINE_NOS[3]);
 }
 
 function testMediaMatchChanged(editor) {
   let sidebar = editor.details.querySelector(".stylesheet-sidebar");
 
   let cond = sidebar.querySelectorAll(".media-rule-condition")[2];
   is(cond.textContent, "(max-width: 400px)",
      "third rule condition text is correct");
@@ -97,19 +99,19 @@ function* testMediaRuleAdded(UI, editor)
   text += NEW_RULE;
 
   let listChange = listenForMediaChange(UI);
   editor.sourceEditor.setText(text);
   yield listChange;
 
   let sidebar = editor.details.querySelector(".stylesheet-sidebar");
   let entries = [...sidebar.querySelectorAll(".media-rule-label")];
-  is(entries.length, 4, "four @media rules after changing text");
+  is(entries.length, 5, "five @media rules after changing text");
 
-  testRule(entries[3], LABELS[3], false, LINE_NOS[3]);
+  testRule(entries[4], LABELS[4], false, LINE_NOS[4]);
 }
 
 function testRule(rule, text, matches, lineno) {
   let cond = rule.querySelector(".media-rule-condition");
   is(cond.textContent, text, "media label is correct for " + text);
 
   let matched = !cond.classList.contains("media-condition-unmatched");
   ok(matches ? matched : !matched,
new file mode 100644
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_links.js
@@ -0,0 +1,82 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests responsive mode links for
+ * @media sidebar width and height related conditions */
+
+const {ResponsiveUIManager} =
+      Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm", {});
+const TESTCASE_URI = TEST_BASE_HTTPS + "media-rules.html";
+
+waitForExplicitFinish();
+
+add_task(function*() {
+  let {ui} = yield openStyleEditorForURL(TESTCASE_URI);
+
+  let mediaEditor = ui.editors[1];
+  yield openEditor(mediaEditor);
+
+  yield testLinkifiedConditions(mediaEditor, gBrowser.selectedTab, ui);
+});
+
+function testLinkifiedConditions(editor, tab, ui) {
+  let sidebar = editor.details.querySelector(".stylesheet-sidebar");
+  let conditions = sidebar.querySelectorAll(".media-rule-condition");
+  let responsiveModeToggleClass = ".media-responsive-mode-toggle";
+
+  info("Testing if media rules have the appropriate number of links");
+  ok(!conditions[0].querySelector(responsiveModeToggleClass),
+    "There should be no links in the first media rule.");
+  ok(!conditions[1].querySelector(responsiveModeToggleClass),
+    "There should be no links in the second media rule.")
+  ok(conditions[2].querySelector(responsiveModeToggleClass),
+     "There should be 1 responsive mode link in the media rule");
+  is(conditions[3].querySelectorAll(responsiveModeToggleClass).length, 2,
+       "There should be 2 resposnive mode links in the media rule");
+
+  info("Launching responsive mode");
+  conditions[2].querySelector(responsiveModeToggleClass).click();
+
+  info("Waiting for the @media list to update");
+  let onMediaChange = once("media-list-changed", ui);
+  yield once("on", ResponsiveUIManager);
+  yield onMediaChange;
+
+  ok(ResponsiveUIManager.isActiveForTab(tab),
+    "Responsive mode should be active.");
+  conditions = sidebar.querySelectorAll(".media-rule-condition");
+  ok(!conditions[2].classList.contains("media-condition-unmatched"),
+     "media rule should now be matched after responsive mode is active");
+
+  info("Closing responsive mode");
+  ResponsiveUIManager.toggle(window, tab);
+  onMediaChange = once("media-list-changed", ui);
+  yield once("off", ResponsiveUIManager);
+  yield onMediaChange;
+
+  ok(!ResponsiveUIManager.isActiveForTab(tab),
+     "Responsive mode should no longer be active.");
+  conditions = sidebar.querySelectorAll(".media-rule-condition");
+  ok(conditions[2].classList.contains("media-condition-unmatched"),
+       "media rule should now be unmatched after responsive mode is closed");
+}
+
+/* Helpers */
+function once(event, target) {
+  let deferred = promise.defer();
+  target.once(event, () => {
+    deferred.resolve();
+  });
+  return deferred.promise;
+}
+
+function openEditor(editor) {
+  getLinkFor(editor).click();
+
+  return editor.getSourceEditor();
+}
+
+function getLinkFor(editor) {
+  return editor.summary.querySelector(".stylesheet-name");
+}
--- a/devtools/client/styleeditor/test/media-rules.css
+++ b/devtools/client/styleeditor/test/media-rules.css
@@ -16,8 +16,14 @@ div {
   background-color: ghostwhite;
 }
 
 @media (max-width: 400px) {
   div {
     color: green;
   }
 }
+
+@media (min-height: 200px) and (max-height: 250px) {
+  div {
+    color: orange;
+  }
+}
\ No newline at end of file
--- a/devtools/client/themes/styleeditor.css
+++ b/devtools/client/themes/styleeditor.css
@@ -79,26 +79,27 @@
 }
 
 .media-rule-label {
   padding: 4px;
   cursor: pointer;
   border-bottom: 1px solid;
 }
 
+.media-responsive-mode-toggle {
+  color: var(--theme-highlight-blue);
+  text-decoration: underline;
+}
+
 .media-rule-line {
   -moz-padding-start: 4px;
 }
 
-.theme-light .media-condition-unmatched {
-  color: grey;
-}
-
-.theme-dark .media-condition-unmatched {
-  color: #606C75;
+.media-condition-unmatched {
+  opacity: 0.4;
 }
 
 .stylesheet-enabled {
   padding: 8px 0;
   margin: 0 8px;
   background-image: url(images/itemToggle.png);
   background-repeat: no-repeat;
   background-clip: content-box;