From 24f4d95564e1b85bfb96f0947a37c79d837b5dea Mon Sep 17 00:00:00 2001 From: Heather Arthur Date: Mon, 26 May 2014 13:52:24 -0400 Subject: [PATCH] Bug 1012806 - Add @media rules sidebar to style editor; r=jwalker --- browser/app/profile/firefox.js | 1 + .../devtools/styleeditor/StyleEditorUI.jsm | 144 ++++++++++++-- .../devtools/styleeditor/StyleSheetEditor.jsm | 39 ++++ browser/devtools/styleeditor/styleeditor.css | 19 ++ browser/devtools/styleeditor/styleeditor.xul | 13 +- browser/devtools/styleeditor/test/browser.ini | 3 + .../test/browser_styleeditor_media_sidebar.js | 102 ++++++++++ .../browser_styleeditor_selectstylesheet.js | 3 +- .../devtools/styleeditor/test/media-rules.css | 23 +++ .../styleeditor/test/media-rules.html | 13 ++ .../chrome/browser/devtools/styleeditor.dtd | 4 + .../browser/devtools/styleeditor.properties | 16 ++ .../themes/shared/devtools/styleeditor.css | 43 ++++ toolkit/devtools/server/actors/stylesheets.js | 186 ++++++++++++++++++ 14 files changed, 585 insertions(+), 24 deletions(-) create mode 100644 browser/devtools/styleeditor/test/browser_styleeditor_media_sidebar.js create mode 100644 browser/devtools/styleeditor/test/media-rules.css create mode 100644 browser/devtools/styleeditor/test/media-rules.html diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 0a44e71b368..39bb89bfa43 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1340,6 +1340,7 @@ pref("devtools.scratchpad.enableAutocompletion", true); pref("devtools.styleeditor.enabled", true); pref("devtools.styleeditor.source-maps-enabled", false); pref("devtools.styleeditor.autocompletion-enabled", true); +pref("devtools.styleeditor.showMediaSidebar", false); // Enable the Shader Editor. pref("devtools.shadereditor.enabled", false); diff --git a/browser/devtools/styleeditor/StyleEditorUI.jsm b/browser/devtools/styleeditor/StyleEditorUI.jsm index d9515c82d4c..6ce29135a71 100644 --- a/browser/devtools/styleeditor/StyleEditorUI.jsm +++ b/browser/devtools/styleeditor/StyleEditorUI.jsm @@ -33,6 +33,7 @@ const console = require("resource://gre/modules/devtools/Console.jsm").console; const LOAD_ERROR = "error-load"; const STYLE_EDITOR_TEMPLATE = "stylesheet"; +const PREF_MEDIA_SIDEBAR = "devtools.styleeditor.showMediaSidebar"; /** * StyleEditorUI is controls and builds the UI of the Style Editor, including @@ -63,14 +64,17 @@ function StyleEditorUI(debuggee, target, panelDoc) { this.selectedEditor = null; this.savedLocations = {}; - this._updateSourcesLabel = this._updateSourcesLabel.bind(this); + this._updateContextMenu = this._updateContextMenu.bind(this); this._onStyleSheetCreated = this._onStyleSheetCreated.bind(this); this._onNewDocument = this._onNewDocument.bind(this); + this._onMediaPrefChanged = this._onMediaPrefChanged.bind(this); + this._updateMediaList = this._updateMediaList.bind(this); this._clear = this._clear.bind(this); this._onError = this._onError.bind(this); this._prefObserver = new PrefObserver("devtools.styleeditor."); this._prefObserver.on(PREF_ORIG_SOURCES, this._onNewDocument); + this._prefObserver.on(PREF_MEDIA_SIDEBAR, this._onMediaPrefChanged); } StyleEditorUI.prototype = { @@ -140,24 +144,34 @@ StyleEditorUI.prototype = { this._contextMenu = this._panelDoc.getElementById("sidebar-context"); this._contextMenu.addEventListener("popupshowing", - this._updateSourcesLabel); + this._updateContextMenu); this._sourcesItem = this._panelDoc.getElementById("context-origsources"); this._sourcesItem.addEventListener("command", this._toggleOrigSources); + this._mediaItem = this._panelDoc.getElementById("context-show-media"); + this._mediaItem.addEventListener("command", + this._toggleMediaSidebar); }, /** - * Update text of context menu option to reflect whether we're showing - * original sources (e.g. Sass files) or not. + * Update text of context menu option to reflect current preference + * settings */ - _updateSourcesLabel: function() { - let string = "showOriginalSources"; + _updateContextMenu: function() { + let sourceString = "showOriginalSources"; if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { - string = "showCSSSources"; + sourceString = "showCSSSources"; } - this._sourcesItem.setAttribute("label", _(string + ".label")); - this._sourcesItem.setAttribute("accesskey", _(string + ".accesskey")); + this._sourcesItem.setAttribute("label", _(sourceString + ".label")); + this._sourcesItem.setAttribute("accesskey", _(sourceString + ".accesskey")); + + let mediaString = "showMediaSidebar" + if (Services.prefs.getBoolPref(PREF_MEDIA_SIDEBAR)) { + mediaString = "hideMediaSidebar"; + } + this._mediaItem.setAttribute("label", _(mediaString + ".label")); + this._mediaItem.setAttribute("accesskey", _(mediaString + ".accesskey")); }, /** @@ -202,7 +216,7 @@ StyleEditorUI.prototype = { let {line, ch} = this.selectedEditor.sourceEditor.getCursor(); this._styleSheetToSelect = { - href: href, + stylesheet: href, line: line, col: ch }; @@ -274,6 +288,7 @@ StyleEditorUI.prototype = { new StyleSheetEditor(styleSheet, this._window, file, isNew, this._walker); editor.on("property-change", this._summaryChange.bind(this, editor)); + editor.on("media-rules-changed", this._updateMediaList.bind(this, editor)); editor.on("linked-css-file", this._summaryChange.bind(this, editor)); editor.on("linked-css-file-error", this._summaryChange.bind(this, editor)); editor.on("error", this._onError); @@ -342,13 +357,28 @@ StyleEditorUI.prototype = { }, /** - * Toggle the original sources pref. + * Toggle the original sources pref. */ _toggleOrigSources: function() { let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled); }, + /** + * Toggle the pref for showing a @media rules sidebar in each editor. + */ + _toggleMediaSidebar: function() { + let isEnabled = Services.prefs.getBoolPref(PREF_MEDIA_SIDEBAR); + Services.prefs.setBoolPref(PREF_MEDIA_SIDEBAR, !isEnabled); + }, + + /** + * Toggle the @media sidebar in each editor depending on the setting. + */ + _onMediaPrefChanged: function() { + this.editors.forEach(this._updateMediaList); + }, + /** * Remove a particular stylesheet editor from the UI * @@ -401,6 +431,7 @@ StyleEditorUI.prototype = { onCreate: function(summary, details, data) { let editor = data.editor; editor.summary = summary; + editor.details = details; wire(summary, ".stylesheet-enabled", function onToggleDisabled(event) { event.stopPropagation(); @@ -442,7 +473,7 @@ StyleEditorUI.prototype = { } if (this._styleSheetToSelect - && this._styleSheetToSelect.href == editor.styleSheet.href) { + && this._styleSheetToSelect.stylesheet == editor.styleSheet.href) { yield this.switchToSelectedSheet(); } @@ -492,9 +523,11 @@ StyleEditorUI.prototype = { */ switchToSelectedSheet: function() { let sheet = this._styleSheetToSelect; + let isHref = sheet.stylesheet === null || typeof sheet.stylesheet == "string"; for (let editor of this.editors) { - if (editor.styleSheet.href == sheet.href) { + if ((isHref && editor.styleSheet.href == sheet.stylesheet) || + sheet.stylesheet == editor.styleSheet) { // The _styleSheetBoundToSelect will always hold the latest pending // requested style sheet (with line and column) which is not yet // selected by the source editor. Only after we select that particular @@ -554,6 +587,24 @@ StyleEditorUI.prototype = { return deferred.promise; }, + getEditorDetails: function(editor) { + if (editor.details) { + return promise.resolve(editor.details); + } + + let deferred = promise.defer(); + let self = this; + + this.on("editor-added", function onAdd(e, selected) { + if (selected == editor) { + self.off("editor-added", onAdd); + deferred.resolve(editor.details); + } + }); + + return deferred.promise; + }, + /** * Returns an identifier for the given style sheet. * @@ -569,19 +620,16 @@ StyleEditorUI.prototype = { /** * selects a stylesheet and optionally moves the cursor to a selected line * - * @param {string} [href] - * Href of stylesheet that should be selected. If a stylesheet is not passed - * and the editor is not initialized we focus the first stylesheet. If - * a stylesheet is not passed and the editor is initialized we ignore - * the call. + * @param {StyleSheetFront} [stylesheet] + * Stylesheet to select or href of stylesheet to select * @param {Number} [line] * Line to which the caret should be moved (zero-indexed). * @param {Number} [col] * Column to which the caret should be moved (zero-indexed). */ - selectStyleSheet: function(href, line, col) { + selectStyleSheet: function(stylesheet, line, col) { this._styleSheetToSelect = { - href: href, + stylesheet: stylesheet, line: line, col: col, }; @@ -653,10 +701,66 @@ StyleEditorUI.prototype = { PluralForm.get(ruleCount, _("ruleCount.label")).replace("#1", ruleCount)); }, + /** + * Update the @media rules sidebar for an editor. Hide if there are no rules + * Display a list of the @media rules in the editor's associated style sheet. + * Emits a 'media-list-changed' event after updating the UI. + * + * @param {StyleSheetEditor} editor + * Editor to update @media sidebar of + */ + _updateMediaList: function(editor) { + this.getEditorDetails(editor).then((details) => { + let list = details.querySelector(".stylesheet-media-list"); + + while (list.firstChild) { + list.removeChild(list.firstChild); + } + + let rules = editor.mediaRules; + let showSidebar = Services.prefs.getBoolPref(PREF_MEDIA_SIDEBAR); + let sidebar = details.querySelector(".stylesheet-sidebar"); + sidebar.hidden = !showSidebar || !rules.length; + + for (let rule of rules) { + let div = this._panelDoc.createElement("div"); + div.className = "media-rule-label"; + div.addEventListener("click", this._jumpToMediaRule.bind(this, rule)); + + let cond = this._panelDoc.createElement("div"); + cond.textContent = rule.conditionText; + cond.className = "media-rule-condition" + if (!rule.matches) { + cond.classList.add("media-condition-unmatched"); + } + div.appendChild(cond); + + let line = this._panelDoc.createElement("div"); + line.className = "media-rule-line theme-link"; + line.textContent = ":" + rule.line; + div.appendChild(line); + + list.appendChild(div); + } + this.emit("media-list-changed", editor); + }); + }, + + /** + * Jump cursor to the editor for a stylesheet and line number for a rule. + * + * @param {MediaRuleFront} rule + * Rule to jump to. + */ + _jumpToMediaRule: function(rule) { + this.selectStyleSheet(rule.parentStyleSheet, rule.line - 1, rule.column - 1); + }, + destroy: function() { this._clearStyleSheetEditors(); this._prefObserver.off(PREF_ORIG_SOURCES, this._onNewDocument); + this._prefObserver.off(PREF_MEDIA_SIDEBAR, this._onMediaPrefChanged); this._prefObserver.destroy(); } } diff --git a/browser/devtools/styleeditor/StyleSheetEditor.jsm b/browser/devtools/styleeditor/StyleSheetEditor.jsm index e37c6caed8b..a807e9649dc 100644 --- a/browser/devtools/styleeditor/StyleSheetEditor.jsm +++ b/browser/devtools/styleeditor/StyleSheetEditor.jsm @@ -91,9 +91,17 @@ function StyleSheetEditor(styleSheet, win, file, isNew, walker) { this._onPropertyChange = this._onPropertyChange.bind(this); this._onError = this._onError.bind(this); + this._onMediaRuleMatchesChange = this._onMediaRuleMatchesChange.bind(this); + this._onMediaRulesChanged = this._onMediaRulesChanged.bind(this) this.checkLinkedFileForChanges = this.checkLinkedFileForChanges.bind(this); this.markLinkedFileBroken = this.markLinkedFileBroken.bind(this); + this.mediaRules = []; + if (this.styleSheet.getMediaRules) { + this.styleSheet.getMediaRules().then(this._onMediaRulesChanged); + } + this.styleSheet.on("media-rules-changed", this._onMediaRulesChanged); + this._focusOnSourceEditorReady = false; let relatedSheet = this.styleSheet.relatedStyleSheet; @@ -275,6 +283,36 @@ StyleSheetEditor.prototype = { this.emit("property-change", property, value); }, + /** + * Handles changes to the list of @media rules in the stylesheet. + * Emits 'media-rules-changed' if the list has changed. + * + * @param {array} rules + * Array of MediaRuleFronts for new media rules of sheet. + */ + _onMediaRulesChanged: function(rules) { + if (!rules.length && !this.mediaRules.length) { + return; + } + for (let rule of this.mediaRules) { + rule.off("matches-change", this._onMediaRuleMatchesChange); + rule.destroy(); + } + this.mediaRules = rules; + + for (let rule of rules) { + rule.on("matches-change", this._onMediaRuleMatchesChange); + } + this.emit("media-rules-changed", rules); + }, + + /** + * Forward media-rules-changed event from stylesheet. + */ + _onMediaRuleMatchesChange: function() { + this.emit("media-rules-changed", this.mediaRules); + }, + /** * Forward error event from stylesheet. * @@ -590,6 +628,7 @@ StyleSheetEditor.prototype = { if (this.sourceEditor) { this.sourceEditor.destroy(); } + this.styleSheet.off("media-rules-changed", this._onMediaRulesChanged); this.styleSheet.off("property-change", this._onPropertyChange); this.styleSheet.off("error", this._onError); } diff --git a/browser/devtools/styleeditor/styleeditor.css b/browser/devtools/styleeditor/styleeditor.css index 4f3f9f0dd5b..f00b067c4e6 100644 --- a/browser/devtools/styleeditor/styleeditor.css +++ b/browser/devtools/styleeditor/styleeditor.css @@ -34,6 +34,25 @@ li.error > .stylesheet-info > .stylesheet-more > .stylesheet-error-message { display: -moz-box; } +.stylesheet-details-container { + -moz-box-flex: 1; +} + +.stylesheet-media-list { + overflow-x: hidden; + overflow-y: auto; + -moz-box-flex: 1; +} + +.media-rule-label { + display: flex; +} + +.media-rule-condition { + flex: 1; + overflow: hidden; +} + .splitview-nav > li { -moz-box-orient: horizontal; } diff --git a/browser/devtools/styleeditor/styleeditor.xul b/browser/devtools/styleeditor/styleeditor.xul index b63f5df461e..7a0a437b22f 100644 --- a/browser/devtools/styleeditor/styleeditor.xul +++ b/browser/devtools/styleeditor/styleeditor.xul @@ -61,6 +61,7 @@ + @@ -128,8 +129,16 @@ - + + + diff --git a/browser/devtools/styleeditor/test/browser.ini b/browser/devtools/styleeditor/test/browser.ini index 288a7ebfe24..51bdd54df35 100644 --- a/browser/devtools/styleeditor/test/browser.ini +++ b/browser/devtools/styleeditor/test/browser.ini @@ -14,6 +14,8 @@ support-files = longload.html media-small.css media.html + media-rules.html + media-rules.css minified.html nostyle.html pretty.css @@ -46,6 +48,7 @@ skip-if = os == "linux" || "mac" # bug 949355 [browser_styleeditor_init.js] [browser_styleeditor_inline_friendly_names.js] [browser_styleeditor_loading.js] +[browser_styleeditor_media_sidebar.js] [browser_styleeditor_new.js] [browser_styleeditor_nostyle.js] [browser_styleeditor_pretty.js] diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_media_sidebar.js b/browser/devtools/styleeditor/test/browser_styleeditor_media_sidebar.js new file mode 100644 index 00000000000..4ca60a27717 --- /dev/null +++ b/browser/devtools/styleeditor/test/browser_styleeditor_media_sidebar.js @@ -0,0 +1,102 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// https rather than chrome to improve coverage +const TESTCASE_URI = TEST_BASE_HTTPS + "media-rules.html"; +const PREF = "devtools.styleeditor.showMediaSidebar"; + +const RESIZE = 300; +const LABELS = ["not all", "all", "(max-width: 400px)"]; +const LINE_NOS = [2, 8, 20]; + +waitForExplicitFinish(); + +let test = asyncTest(function*() { + Services.prefs.setBoolPref(PREF, true); + + let {UI} = yield addTabAndOpenStyleEditors(2, null, TESTCASE_URI); + + is(UI.editors.length, 2, "correct number of editors"); + + // Test first plain css editor + let plainEditor = UI.editors[0]; + yield openEditor(plainEditor); + testPlainEditor(plainEditor); + + // Test editor with @media rules + let mediaEditor = UI.editors[1]; + yield openEditor(mediaEditor); + testMediaEditor(mediaEditor); + + // Test resizing and seeing @media matching state change + let originalWidth = window.outerWidth; + let originalHeight = window.outerHeight; + + let onMatchesChange = listenForMatchesChange(UI); + window.resizeTo(RESIZE, RESIZE); + yield onMatchesChange; + + testMediaMatchChanged(mediaEditor); + + window.resizeTo(originalWidth, originalHeight); + Services.prefs.clearUserPref(PREF); +}); + +function testPlainEditor(editor) { + let sidebar = editor.details.querySelector(".stylesheet-sidebar"); + 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"); + + 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]); +} + +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"); + ok(!cond.classList.contains("media-condition-unmatched"), + "media rule is now matched after resizing"); +} + +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, + "media rule is " + (matches ? "matched" : "unmatched")); + + let line = rule.querySelector(".media-rule-line"); + is(line.textContent, ":" + lineno, "correct line number shown"); +} + +/* Helpers */ + +function openEditor(editor) { + getLinkFor(editor).click(); + + return editor.getSourceEditor(); +} + +function listenForMatchesChange(UI) { + let deferred = promise.defer(); + UI.once("media-list-changed", () => { + deferred.resolve(); + }) + return deferred.promise; +} + +function getLinkFor(editor) { + return editor.summary.querySelector(".stylesheet-name"); +} diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_selectstylesheet.js b/browser/devtools/styleeditor/test/browser_styleeditor_selectstylesheet.js index 91bd2c82538..d613d4331b1 100644 --- a/browser/devtools/styleeditor/test/browser_styleeditor_selectstylesheet.js +++ b/browser/devtools/styleeditor/test/browser_styleeditor_selectstylesheet.js @@ -34,7 +34,7 @@ function runTests() editor.getSourceEditor().then(() => { is(gUI.selectedEditor, gUI.editors[1], "second editor is selected"); let {line, ch} = gUI.selectedEditor.sourceEditor.getCursor(); - + is(line, LINE_NO, "correct line selected"); is(ch, COL_NO, "correct column selected"); @@ -42,6 +42,5 @@ function runTests() finish(); }); }); - gUI.selectStyleSheet(gUI.editors[1].styleSheet.href, LINE_NO); } \ No newline at end of file diff --git a/browser/devtools/styleeditor/test/media-rules.css b/browser/devtools/styleeditor/test/media-rules.css new file mode 100644 index 00000000000..b4db3f21674 --- /dev/null +++ b/browser/devtools/styleeditor/test/media-rules.css @@ -0,0 +1,23 @@ +@media not all { + div { + color: blue; + } +} + +@media all { + div { + color: red; + } +} + +div { + width: 20px; + height: 20px; + background-color: ghostwhite; +} + +@media (max-width: 400px) { + div { + color: green; + } +} diff --git a/browser/devtools/styleeditor/test/media-rules.html b/browser/devtools/styleeditor/test/media-rules.html new file mode 100644 index 00000000000..edc45ccae08 --- /dev/null +++ b/browser/devtools/styleeditor/test/media-rules.html @@ -0,0 +1,13 @@ + + + + + + + + +
+ Testing style editor media sidebar +
+ + \ No newline at end of file diff --git a/browser/locales/en-US/chrome/browser/devtools/styleeditor.dtd b/browser/locales/en-US/chrome/browser/devtools/styleeditor.dtd index d9d6c7394f5..2fd81c8e84d 100644 --- a/browser/locales/en-US/chrome/browser/devtools/styleeditor.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/styleeditor.dtd @@ -24,6 +24,10 @@ + + +