Wed, 31 Dec 2014 06:09:35 +0100
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 }