browser/devtools/styleeditor/StyleEditorUI.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* vim:set ts=2 sw=2 sts=2 et: */
     2 /* This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 "use strict";
     8 this.EXPORTED_SYMBOLS = ["StyleEditorUI"];
    10 const Cc = Components.classes;
    11 const Ci = Components.interfaces;
    12 const Cu = Components.utils;
    14 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    15 Cu.import("resource://gre/modules/Services.jsm");
    16 Cu.import("resource://gre/modules/NetUtil.jsm");
    17 Cu.import("resource://gre/modules/osfile.jsm");
    18 Cu.import("resource://gre/modules/Task.jsm");
    19 Cu.import("resource://gre/modules/devtools/event-emitter.js");
    20 Cu.import("resource:///modules/devtools/gDevTools.jsm");
    21 Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
    22 Cu.import("resource:///modules/devtools/SplitView.jsm");
    23 Cu.import("resource:///modules/devtools/StyleSheetEditor.jsm");
    24 const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
    26 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
    27                                   "resource://gre/modules/PluralForm.jsm");
    29 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
    30 const { PrefObserver, PREF_ORIG_SOURCES } = require("devtools/styleeditor/utils");
    32 const LOAD_ERROR = "error-load";
    33 const STYLE_EDITOR_TEMPLATE = "stylesheet";
    35 /**
    36  * StyleEditorUI is controls and builds the UI of the Style Editor, including
    37  * maintaining a list of editors for each stylesheet on a debuggee.
    38  *
    39  * Emits events:
    40  *   'editor-added': A new editor was added to the UI
    41  *   'editor-selected': An editor was selected
    42  *   'error': An error occured
    43  *
    44  * @param {StyleEditorFront} debuggee
    45  *        Client-side front for interacting with the page's stylesheets
    46  * @param {Target} target
    47  *        Interface for the page we're debugging
    48  * @param {Document} panelDoc
    49  *        Document of the toolbox panel to populate UI in.
    50  */
    51 function StyleEditorUI(debuggee, target, panelDoc) {
    52   EventEmitter.decorate(this);
    54   this._debuggee = debuggee;
    55   this._target = target;
    56   this._panelDoc = panelDoc;
    57   this._window = this._panelDoc.defaultView;
    58   this._root = this._panelDoc.getElementById("style-editor-chrome");
    60   this.editors = [];
    61   this.selectedEditor = null;
    62   this.savedLocations = {};
    64   this._updateSourcesLabel = this._updateSourcesLabel.bind(this);
    65   this._onStyleSheetCreated = this._onStyleSheetCreated.bind(this);
    66   this._onNewDocument = this._onNewDocument.bind(this);
    67   this._clear = this._clear.bind(this);
    68   this._onError = this._onError.bind(this);
    70   this._prefObserver = new PrefObserver("devtools.styleeditor.");
    71   this._prefObserver.on(PREF_ORIG_SOURCES, this._onNewDocument);
    72 }
    74 StyleEditorUI.prototype = {
    75   /**
    76    * Get whether any of the editors have unsaved changes.
    77    *
    78    * @return boolean
    79    */
    80   get isDirty() {
    81     if (this._markedDirty === true) {
    82       return true;
    83     }
    84     return this.editors.some((editor) => {
    85       return editor.sourceEditor && !editor.sourceEditor.isClean();
    86     });
    87   },
    89   /*
    90    * Mark the style editor as having or not having unsaved changes.
    91    */
    92   set isDirty(value) {
    93     this._markedDirty = value;
    94   },
    96   /*
    97    * Index of selected stylesheet in document.styleSheets
    98    */
    99   get selectedStyleSheetIndex() {
   100     return this.selectedEditor ?
   101            this.selectedEditor.styleSheet.styleSheetIndex : -1;
   102   },
   104   /**
   105    * Initiates the style editor ui creation and the inspector front to get
   106    * reference to the walker.
   107    */
   108   initialize: function() {
   109     let toolbox = gDevTools.getToolbox(this._target);
   110     return toolbox.initInspector().then(() => {
   111       this._walker = toolbox.walker;
   112     }).then(() => {
   113       this.createUI();
   114       this._debuggee.getStyleSheets().then((styleSheets) => {
   115         this._resetStyleSheetList(styleSheets);
   117         this._target.on("will-navigate", this._clear);
   118         this._target.on("navigate", this._onNewDocument);
   119       });
   120     });
   121   },
   123   /**
   124    * Build the initial UI and wire buttons with event handlers.
   125    */
   126   createUI: function() {
   127     let viewRoot = this._root.parentNode.querySelector(".splitview-root");
   129     this._view = new SplitView(viewRoot);
   131     wire(this._view.rootElement, ".style-editor-newButton", function onNew() {
   132       this._debuggee.addStyleSheet(null).then(this._onStyleSheetCreated);
   133     }.bind(this));
   135     wire(this._view.rootElement, ".style-editor-importButton", function onImport() {
   136       this._importFromFile(this._mockImportFile || null, this._window);
   137     }.bind(this));
   139     this._contextMenu = this._panelDoc.getElementById("sidebar-context");
   140     this._contextMenu.addEventListener("popupshowing",
   141                                        this._updateSourcesLabel);
   143     this._sourcesItem = this._panelDoc.getElementById("context-origsources");
   144     this._sourcesItem.addEventListener("command",
   145                                        this._toggleOrigSources);
   146   },
   148   /**
   149    * Update text of context menu option to reflect whether we're showing
   150    * original sources (e.g. Sass files) or not.
   151    */
   152   _updateSourcesLabel: function() {
   153     let string = "showOriginalSources";
   154     if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
   155       string = "showCSSSources";
   156     }
   157     this._sourcesItem.setAttribute("label", _(string + ".label"));
   158     this._sourcesItem.setAttribute("accesskey", _(string + ".accesskey"));
   159   },
   161   /**
   162    * Refresh editors to reflect the stylesheets in the document.
   163    *
   164    * @param {string} event
   165    *        Event name
   166    * @param {StyleSheet} styleSheet
   167    *        StyleSheet object for new sheet
   168    */
   169   _onNewDocument: function() {
   170     this._debuggee.getStyleSheets().then((styleSheets) => {
   171       this._resetStyleSheetList(styleSheets);
   172     })
   173   },
   175   /**
   176    * Add editors for all the given stylesheets to the UI.
   177    *
   178    * @param  {array} styleSheets
   179    *         Array of StyleSheetFront
   180    */
   181   _resetStyleSheetList: function(styleSheets) {
   182     this._clear();
   184     for (let sheet of styleSheets) {
   185       this._addStyleSheet(sheet);
   186     }
   188     this._root.classList.remove("loading");
   190     this.emit("stylesheets-reset");
   191   },
   193   /**
   194    * Remove all editors and add loading indicator.
   195    */
   196   _clear: function() {
   197     // remember selected sheet and line number for next load
   198     if (this.selectedEditor && this.selectedEditor.sourceEditor) {
   199       let href = this.selectedEditor.styleSheet.href;
   200       let {line, ch} = this.selectedEditor.sourceEditor.getCursor();
   202       this._styleSheetToSelect = {
   203         href: href,
   204         line: line,
   205         col: ch
   206       };
   207     }
   209     // remember saved file locations
   210     for (let editor of this.editors) {
   211       if (editor.savedFile) {
   212         let identifier = this.getStyleSheetIdentifier(editor.styleSheet);
   213         this.savedLocations[identifier] = editor.savedFile;
   214       }
   215     }
   217     this._clearStyleSheetEditors();
   218     this._view.removeAll();
   220     this.selectedEditor = null;
   222     this._root.classList.add("loading");
   223   },
   225   /**
   226    * Add an editor for this stylesheet. Add editors for its original sources
   227    * instead (e.g. Sass sources), if applicable.
   228    *
   229    * @param  {StyleSheetFront} styleSheet
   230    *         Style sheet to add to style editor
   231    */
   232   _addStyleSheet: function(styleSheet) {
   233     let editor = this._addStyleSheetEditor(styleSheet);
   235     if (!Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
   236       return;
   237     }
   239     styleSheet.getOriginalSources().then((sources) => {
   240       if (sources && sources.length) {
   241         this._removeStyleSheetEditor(editor);
   242         sources.forEach((source) => {
   243           // set so the first sheet will be selected, even if it's a source
   244           source.styleSheetIndex = styleSheet.styleSheetIndex;
   245           source.relatedStyleSheet = styleSheet;
   247           this._addStyleSheetEditor(source);
   248         });
   249       }
   250     });
   251   },
   253   /**
   254    * Add a new editor to the UI for a source.
   255    *
   256    * @param {StyleSheet}  styleSheet
   257    *        Object representing stylesheet
   258    * @param {nsIfile}  file
   259    *         Optional file object that sheet was imported from
   260    * @param {Boolean} isNew
   261    *         Optional if stylesheet is a new sheet created by user
   262    */
   263   _addStyleSheetEditor: function(styleSheet, file, isNew) {
   264     // recall location of saved file for this sheet after page reload
   265     let identifier = this.getStyleSheetIdentifier(styleSheet);
   266     let savedFile = this.savedLocations[identifier];
   267     if (savedFile && !file) {
   268       file = savedFile;
   269     }
   271     let editor =
   272       new StyleSheetEditor(styleSheet, this._window, file, isNew, this._walker);
   274     editor.on("property-change", this._summaryChange.bind(this, editor));
   275     editor.on("linked-css-file", this._summaryChange.bind(this, editor));
   276     editor.on("linked-css-file-error", this._summaryChange.bind(this, editor));
   277     editor.on("error", this._onError);
   279     this.editors.push(editor);
   281     editor.fetchSource(this._sourceLoaded.bind(this, editor));
   282     return editor;
   283   },
   285   /**
   286    * Import a style sheet from file and asynchronously create a
   287    * new stylesheet on the debuggee for it.
   288    *
   289    * @param {mixed} file
   290    *        Optional nsIFile or filename string.
   291    *        If not set a file picker will be shown.
   292    * @param {nsIWindow} parentWindow
   293    *        Optional parent window for the file picker.
   294    */
   295   _importFromFile: function(file, parentWindow) {
   296     let onFileSelected = function(file) {
   297       if (!file) {
   298         // nothing selected
   299         return;
   300       }
   301       NetUtil.asyncFetch(file, (stream, status) => {
   302         if (!Components.isSuccessCode(status)) {
   303           this.emit("error", LOAD_ERROR);
   304           return;
   305         }
   306         let source = NetUtil.readInputStreamToString(stream, stream.available());
   307         stream.close();
   309         this._debuggee.addStyleSheet(source).then((styleSheet) => {
   310           this._onStyleSheetCreated(styleSheet, file);
   311         });
   312       });
   314     }.bind(this);
   316     showFilePicker(file, false, parentWindow, onFileSelected);
   317   },
   320   /**
   321    * When a new or imported stylesheet has been added to the document.
   322    * Add an editor for it.
   323    */
   324   _onStyleSheetCreated: function(styleSheet, file) {
   325     this._addStyleSheetEditor(styleSheet, file, true);
   326   },
   328   /**
   329    * Forward any error from a stylesheet.
   330    *
   331    * @param  {string} event
   332    *         Event name
   333    * @param  {string} errorCode
   334    *         Code represeting type of error
   335    * @param  {string} message
   336    *         The full error message
   337    */
   338   _onError: function(event, errorCode, message) {
   339     this.emit("error", errorCode, message);
   340   },
   342   /**
   343    *  Toggle the original sources pref.
   344    */
   345   _toggleOrigSources: function() {
   346     let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
   347     Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
   348   },
   350   /**
   351    * Remove a particular stylesheet editor from the UI
   352    *
   353    * @param {StyleSheetEditor}  editor
   354    *        The editor to remove.
   355    */
   356   _removeStyleSheetEditor: function(editor) {
   357     if (editor.summary) {
   358       this._view.removeItem(editor.summary);
   359     }
   360     else {
   361       let self = this;
   362       this.on("editor-added", function onAdd(event, added) {
   363         if (editor == added) {
   364           self.off("editor-added", onAdd);
   365           self._view.removeItem(editor.summary);
   366         }
   367       })
   368     }
   370     editor.destroy();
   371     this.editors.splice(this.editors.indexOf(editor), 1);
   372   },
   374   /**
   375    * Clear all the editors from the UI.
   376    */
   377   _clearStyleSheetEditors: function() {
   378     for (let editor of this.editors) {
   379       editor.destroy();
   380     }
   381     this.editors = [];
   382   },
   384   /**
   385    * Called when a StyleSheetEditor's source has been fetched. Create a
   386    * summary UI for the editor.
   387    *
   388    * @param  {StyleSheetEditor} editor
   389    *         Editor to create UI for.
   390    */
   391   _sourceLoaded: function(editor) {
   392     // add new sidebar item and editor to the UI
   393     this._view.appendTemplatedItem(STYLE_EDITOR_TEMPLATE, {
   394       data: {
   395         editor: editor
   396       },
   397       disableAnimations: this._alwaysDisableAnimations,
   398       ordinal: editor.styleSheet.styleSheetIndex,
   399       onCreate: function(summary, details, data) {
   400         let editor = data.editor;
   401         editor.summary = summary;
   403         wire(summary, ".stylesheet-enabled", function onToggleDisabled(event) {
   404           event.stopPropagation();
   405           event.target.blur();
   407           editor.toggleDisabled();
   408         });
   410         wire(summary, ".stylesheet-name", {
   411           events: {
   412             "keypress": function onStylesheetNameActivate(aEvent) {
   413               if (aEvent.keyCode == aEvent.DOM_VK_RETURN) {
   414                 this._view.activeSummary = summary;
   415               }
   416             }.bind(this)
   417           }
   418         });
   420         wire(summary, ".stylesheet-saveButton", function onSaveButton(event) {
   421           event.stopPropagation();
   422           event.target.blur();
   424           editor.saveToFile(editor.savedFile);
   425         });
   427         this._updateSummaryForEditor(editor, summary);
   429         summary.addEventListener("focus", function onSummaryFocus(event) {
   430           if (event.target == summary) {
   431             // autofocus the stylesheet name
   432             summary.querySelector(".stylesheet-name").focus();
   433           }
   434         }, false);
   436         Task.spawn(function* () {
   437           // autofocus if it's a new user-created stylesheet
   438           if (editor.isNew) {
   439             yield this._selectEditor(editor);
   440           }
   442           if (this._styleSheetToSelect
   443               && this._styleSheetToSelect.href == editor.styleSheet.href) {
   444             yield this.switchToSelectedSheet();
   445           }
   447           // If this is the first stylesheet and there is no pending request to
   448           // select a particular style sheet, select this sheet.
   449           if (!this.selectedEditor && !this._styleSheetBoundToSelect
   450               && editor.styleSheet.styleSheetIndex == 0) {
   451             yield this._selectEditor(editor);
   452           }
   454           this.emit("editor-added", editor);
   455         }.bind(this)).then(null, Cu.reportError);
   456       }.bind(this),
   458       onShow: function(summary, details, data) {
   459         let editor = data.editor;
   460         this.selectedEditor = editor;
   462         Task.spawn(function* () {
   463           if (!editor.sourceEditor) {
   464             // only initialize source editor when we switch to this view
   465             let inputElement = details.querySelector(".stylesheet-editor-input");
   466             yield editor.load(inputElement);
   467           }
   469           editor.onShow();
   471           this.emit("editor-selected", editor);
   472         }.bind(this)).then(null, Cu.reportError);
   473       }.bind(this)
   474     });
   475   },
   477   /**
   478    * Switch to the editor that has been marked to be selected.
   479    *
   480    * @return {Promise}
   481    *         Promise that will resolve when the editor is selected.
   482    */
   483   switchToSelectedSheet: function() {
   484     let sheet = this._styleSheetToSelect;
   486     for (let editor of this.editors) {
   487       if (editor.styleSheet.href == sheet.href) {
   488         // The _styleSheetBoundToSelect will always hold the latest pending
   489         // requested style sheet (with line and column) which is not yet
   490         // selected by the source editor. Only after we select that particular
   491         // editor and go the required line and column, it will become null.
   492         this._styleSheetBoundToSelect = this._styleSheetToSelect;
   493         this._styleSheetToSelect = null;
   494         return this._selectEditor(editor, sheet.line, sheet.col);
   495       }
   496     }
   498     return promise.resolve();
   499   },
   501   /**
   502    * Select an editor in the UI.
   503    *
   504    * @param  {StyleSheetEditor} editor
   505    *         Editor to switch to.
   506    * @param  {number} line
   507    *         Line number to jump to
   508    * @param  {number} col
   509    *         Column number to jump to
   510    * @return {Promise}
   511    *         Promise that will resolve when the editor is selected.
   512    */
   513   _selectEditor: function(editor, line, col) {
   514     line = line || 0;
   515     col = col || 0;
   517     let editorPromise = editor.getSourceEditor().then(() => {
   518       editor.sourceEditor.setCursor({line: line, ch: col});
   519       this._styleSheetBoundToSelect = null;
   520     });
   522     let summaryPromise = this.getEditorSummary(editor).then((summary) => {
   523       this._view.activeSummary = summary;
   524     });
   526     return promise.all([editorPromise, summaryPromise]);
   527   },
   529   getEditorSummary: function(editor) {
   530     if (editor.summary) {
   531       return promise.resolve(editor.summary);
   532     }
   534     let deferred = promise.defer();
   535     let self = this;
   537     this.on("editor-added", function onAdd(e, selected) {
   538       if (selected == editor) {
   539         self.off("editor-added", onAdd);
   540         deferred.resolve(editor.summary);
   541       }
   542     });
   544     return deferred.promise;
   545   },
   547   /**
   548    * Returns an identifier for the given style sheet.
   549    *
   550    * @param {StyleSheet} aStyleSheet
   551    *        The style sheet to be identified.
   552    */
   553   getStyleSheetIdentifier: function (aStyleSheet) {
   554     // Identify inline style sheets by their host page URI and index at the page.
   555     return aStyleSheet.href ? aStyleSheet.href :
   556             "inline-" + aStyleSheet.styleSheetIndex + "-at-" + aStyleSheet.nodeHref;
   557   },
   559   /**
   560    * selects a stylesheet and optionally moves the cursor to a selected line
   561    *
   562    * @param {string} [href]
   563    *        Href of stylesheet that should be selected. If a stylesheet is not passed
   564    *        and the editor is not initialized we focus the first stylesheet. If
   565    *        a stylesheet is not passed and the editor is initialized we ignore
   566    *        the call.
   567    * @param {Number} [line]
   568    *        Line to which the caret should be moved (zero-indexed).
   569    * @param {Number} [col]
   570    *        Column to which the caret should be moved (zero-indexed).
   571    */
   572   selectStyleSheet: function(href, line, col) {
   573     this._styleSheetToSelect = {
   574       href: href,
   575       line: line,
   576       col: col,
   577     };
   579     /* Switch to the editor for this sheet, if it exists yet.
   580        Otherwise each editor will be checked when it's created. */
   581     this.switchToSelectedSheet();
   582   },
   585   /**
   586    * Handler for an editor's 'property-changed' event.
   587    * Update the summary in the UI.
   588    *
   589    * @param  {StyleSheetEditor} editor
   590    *         Editor for which a property has changed
   591    */
   592   _summaryChange: function(editor) {
   593     this._updateSummaryForEditor(editor);
   594   },
   596   /**
   597    * Update split view summary of given StyleEditor instance.
   598    *
   599    * @param {StyleSheetEditor} editor
   600    * @param {DOMElement} summary
   601    *        Optional item's summary element to update. If none, item corresponding
   602    *        to passed editor is used.
   603    */
   604   _updateSummaryForEditor: function(editor, summary) {
   605     summary = summary || editor.summary;
   606     if (!summary) {
   607       return;
   608     }
   610     let ruleCount = editor.styleSheet.ruleCount;
   611     if (editor.styleSheet.relatedStyleSheet && editor.linkedCSSFile) {
   612       ruleCount = editor.styleSheet.relatedStyleSheet.ruleCount;
   613     }
   614     if (ruleCount === undefined) {
   615       ruleCount = "-";
   616     }
   618     var flags = [];
   619     if (editor.styleSheet.disabled) {
   620       flags.push("disabled");
   621     }
   622     if (editor.unsaved) {
   623       flags.push("unsaved");
   624     }
   625     if (editor.linkedCSSFileError) {
   626       flags.push("linked-file-error");
   627     }
   628     this._view.setItemClassName(summary, flags.join(" "));
   630     let label = summary.querySelector(".stylesheet-name > label");
   631     label.setAttribute("value", editor.friendlyName);
   632     if (editor.styleSheet.href) {
   633       label.setAttribute("tooltiptext", editor.styleSheet.href);
   634     }
   636     let linkedCSSFile = "";
   637     if (editor.linkedCSSFile) {
   638       linkedCSSFile = OS.Path.basename(editor.linkedCSSFile);
   639     }
   640     text(summary, ".stylesheet-linked-file", linkedCSSFile);
   641     text(summary, ".stylesheet-title", editor.styleSheet.title || "");
   642     text(summary, ".stylesheet-rule-count",
   643       PluralForm.get(ruleCount, _("ruleCount.label")).replace("#1", ruleCount));
   644   },
   646   destroy: function() {
   647     this._clearStyleSheetEditors();
   649     this._prefObserver.off(PREF_ORIG_SOURCES, this._onNewDocument);
   650     this._prefObserver.destroy();
   651   }
   652 }

mercurial