Bug 1443846 - Add swatch to mark rule as selected and toggle font inspector panel.
To use, toggle pref to true: devtools.inspector.fonteditor.enabled
- Introduces the ability for tools to mark one or more rules as "selected". Store rule references for tools to know here to map changes.
- On mouse hover over any rule, show the font editor swatch.
- On click on the font editor swatch, toggle the rule as "selected" and toggle the font inspector panel.
MozReview-Commit-ID: 3eTzEOXkApl
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -35,16 +35,17 @@ const {debounce} = require("devtools/sha
const EventEmitter = require("devtools/shared/event-emitter");
const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
const clipboardHelper = require("devtools/shared/platform/clipboard");
const AutocompletePopup = require("devtools/client/shared/autocomplete-popup");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit";
+const PREF_FONT_EDITOR = "devtools.inspector.fonteditor.enabled";
const FILTER_CHANGED_TIMEOUT = 150;
// This is used to parse user input when filtering.
const FILTER_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*;?$/;
// This is used to parse the filter search value to see if the filter
// should be strict or not
const FILTER_STRICT_RE = /\s*`(.*?)`\s*$/;
const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
@@ -95,35 +96,41 @@ const INSET_POINT_TYPES = ["top", "right
* @param {Object} store
* The CSS rule view can use this object to store metadata
* that might outlast the rule view, particularly the current
* set of disabled properties.
* @param {PageStyleFront} pageStyle
* The PageStyleFront for communicating with the remote server.
*/
function CssRuleView(inspector, document, store, pageStyle) {
+ EventEmitter.decorate(this);
+
this.inspector = inspector;
this.highlighters = inspector.highlighters;
this.styleDocument = document;
this.styleWindow = this.styleDocument.defaultView;
this.store = store || {};
+ // References to rules marked by various editors where they intend to write changes.
+ // @see selectRule(), unselectRule()
+ this.selectedRules = {};
this.pageStyle = pageStyle;
// Allow tests to override debouncing behavior, as this can cause intermittents.
this.debounce = debounce;
this.cssProperties = getCssProperties(inspector.toolbox);
this._outputParser = new OutputParser(document, this.cssProperties);
this._onAddRule = this._onAddRule.bind(this);
this._onContextMenu = this._onContextMenu.bind(this);
this._onCopy = this._onCopy.bind(this);
this._onFilterStyles = this._onFilterStyles.bind(this);
this._onClearSearch = this._onClearSearch.bind(this);
+ this._onRuleSelected = this._onRuleSelected.bind(this);
this._onTogglePseudoClassPanel = this._onTogglePseudoClassPanel.bind(this);
this._onTogglePseudoClass = this._onTogglePseudoClass.bind(this);
this._onToggleClassPanel = this._onToggleClassPanel.bind(this);
let doc = this.styleDocument;
this.element = doc.getElementById("ruleview-container-focusable");
this.addRuleButton = doc.getElementById("ruleview-add-rule-button");
this.searchField = doc.getElementById("ruleview-searchbox");
@@ -149,27 +156,29 @@ function CssRuleView(inspector, document
this.addRuleButton.addEventListener("click", this._onAddRule);
this.searchField.addEventListener("input", this._onFilterStyles);
this.searchClearButton.addEventListener("click", this._onClearSearch);
this.pseudoClassToggle.addEventListener("click", this._onTogglePseudoClassPanel);
this.classToggle.addEventListener("click", this._onToggleClassPanel);
this.hoverCheckbox.addEventListener("click", this._onTogglePseudoClass);
this.activeCheckbox.addEventListener("click", this._onTogglePseudoClass);
this.focusCheckbox.addEventListener("click", this._onTogglePseudoClass);
+ this.on("ruleview-rule-selected", this._onRuleSelected);
this._handlePrefChange = this._handlePrefChange.bind(this);
this._handleUAStylePrefChange = this._handleUAStylePrefChange.bind(this);
this._handleDefaultColorUnitPrefChange =
this._handleDefaultColorUnitPrefChange.bind(this);
this._prefObserver = new PrefObserver("devtools.");
this._prefObserver.on(PREF_UA_STYLES, this._handleUAStylePrefChange);
this._prefObserver.on(PREF_DEFAULT_COLOR_UNIT, this._handleDefaultColorUnitPrefChange);
this.showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES);
+ this.showFontEditor = Services.prefs.getBoolPref(PREF_FONT_EDITOR);
// The popup will be attached to the toolbox document.
this.popup = new AutocompletePopup(inspector._toolbox.doc, {
autoSelect: true,
theme: "auto"
});
this._showEmpty();
@@ -177,18 +186,16 @@ function CssRuleView(inspector, document
this._contextmenu = new StyleInspectorMenu(this, { isRuleView: true });
// Add the tooltips and highlighters to the view
this.tooltips = new TooltipsOverlay(this);
this.highlighters.addToView(this);
this.classListPreviewer = new ClassListPreviewer(this.inspector, this.classPanel);
-
- EventEmitter.decorate(this);
}
CssRuleView.prototype = {
// The element that we're inspecting.
_viewedElement: null,
// Used for cancelling timeouts in the style filter.
_filterChangedTimeout: null,
@@ -715,29 +722,31 @@ CssRuleView.prototype = {
if (this._contextmenu) {
this._contextmenu.destroy();
this._contextmenu = null;
}
this.tooltips.destroy();
this.highlighters.removeFromView(this);
this.classListPreviewer.destroy();
+ this.unselectAllRules();
// Remove bound listeners
this.shortcuts.destroy();
this.element.removeEventListener("copy", this._onCopy);
this.element.removeEventListener("contextmenu", this._onContextMenu);
this.addRuleButton.removeEventListener("click", this._onAddRule);
this.searchField.removeEventListener("input", this._onFilterStyles);
this.searchClearButton.removeEventListener("click", this._onClearSearch);
this.pseudoClassToggle.removeEventListener("click", this._onTogglePseudoClassPanel);
this.classToggle.removeEventListener("click", this._onToggleClassPanel);
this.hoverCheckbox.removeEventListener("click", this._onTogglePseudoClass);
this.activeCheckbox.removeEventListener("click", this._onTogglePseudoClass);
this.focusCheckbox.removeEventListener("click", this._onTogglePseudoClass);
+ this.off("ruleview-rule-selected", this._onRuleSelected);
this.searchField = null;
this.searchClearButton = null;
this.pseudoClassPanel = null;
this.pseudoClassToggle = null;
this.classPanel = null;
this.classToggle = null;
this.hoverCheckbox = null;
@@ -794,16 +803,17 @@ CssRuleView.prototype = {
this.popup.hidePopup();
}
this.clear(false);
this._viewedElement = element;
this.clearPseudoClassPanel();
this.refreshAddRuleButtonState();
+ this.unselectAllRules();
if (!this._viewedElement) {
this._stopSelectingElement();
this._clearRules();
this._showEmpty();
this.refreshPseudoClassPanel();
return promise.resolve(undefined);
}
@@ -1189,16 +1199,123 @@ CssRuleView.prototype = {
isHighlighted = true;
}
}
return isHighlighted;
},
/**
+ * Mark a rule as selected for the given editor id.
+ *
+ * Editing tools can mark one or more rules as selected for themselves so they have
+ * a reference of where to make changes, like add / remove properties.
+ * Each editor has an identifier string (aka editorId) which is used as a key in a map
+ * that holds references to Rule objects.
+ *
+ * Many editors may operate at the same time (ex: Font Editor and Shape Path Editor) so
+ * there are multiple possible selected rules at any given time. A rule can be selected
+ * by different editors at the same time, with each editor operating independently on it.
+ *
+ * @param {Rule} rule
+ * Rule object for which to hold a reference.
+ * @param {String} editorId
+ * Key to use for collecting references to selected rules.
+ * @param {Boolean} [unselectOthers=true]
+ * Optional. Default: `true`. If true, unselect all other rules that were
+ * selected for the given editor. Ensures only one rule at a time is selected for
+ * a particular editor. Set to `false` if an editor may operate on multiple rules
+ * at a time.
+ */
+ selectRule(rule, editorId, unselectOthers = true) {
+ this.selectedRules[editorId] = this.getSelectedRules(editorId);
+
+ if (!this.selectedRules[editorId].includes(rule)) {
+ this.selectedRules[editorId].push(rule);
+ }
+
+ // Mark other rules for this editorId as unselected.
+ if (unselectOthers) {
+ this.selectedRules[editorId]
+ .filter(item => item !== rule)
+ .map(item => this.unselectRule(item, editorId));
+ }
+
+ this.emit("ruleview-rule-selected", {editorId, rule});
+ },
+
+ /**
+ * Unmark a rule as selected for the given editor id.
+ *
+ * @param {Rule} rule
+ * Rule object for which to remove the reference.
+ * @param {String} editorId
+ * Key for which to mark the given rule as selected.
+ */
+ unselectRule(rule, editorId) {
+ if (!Array.isArray(this.selectedRules[editorId])) {
+ return;
+ }
+
+ let index = this.selectedRules[editorId].findIndex(item => item === rule);
+ if (index === -1) {
+ return;
+ }
+
+ this.selectedRules[editorId].splice(index, 1);
+ this.emit("ruleview-rule-unselected", {editorId, rule});
+ },
+
+ /**
+ * Unmark all selected rules for all editors. If an editor id is provided, unmark all
+ * selected rules just for that editor leaving others untouched.
+ *
+ * @param {String} [editorId]
+ * Optional editor id for which to restrict unselect operation.
+ */
+ unselectAllRules(editorId) {
+ let keys = Object.keys(this.selectedRules);
+ keys = editorId ? keys.filter(key => (key === editorId)) : keys;
+ for (let key of keys) {
+ this.selectedRules[key].map(item => this.unselectRule(item, key));
+ }
+ },
+
+ /**
+ * Return an array of selected rules for the given editor id.
+ * If no rules match, return an empty arrary;
+ *
+ * @param {String} editorId
+ * Editor id for which to return selected rules.
+ * @return {Array}
+ */
+ getSelectedRules(editorId) {
+ return Array.isArray(this.selectedRules[editorId]) ?
+ this.selectedRules[editorId] : [];
+ },
+
+ /**
+ * Called when a rule from the Rule view was marked as selected for an editor.
+ * Handle the event and show panels relevant for the given editor id.
+ *
+ * @param {Object} eventData
+ * Data payload for the event. Contains:
+ * - {String} editorId - id of the editor for which the rule was selected
+ * - {Rule} rule - reference to rule that was selected
+ */
+ _onRuleSelected(eventData) {
+ const { editorId } = eventData;
+ switch (editorId) {
+ case "fonteditor":
+ this.inspector.sidebar.show("fontinspector");
+ break;
+ }
+ },
+
+ /**
* Highlights the rule selector that matches the filter search value and
* returns a boolean indicating whether or not the selector was highlighted.
*
* @param {Rule} rule
* The Rule object.
* @return {Boolean} true if the rule selector was highlighted,
* false otherwise.
*/
--- a/devtools/client/inspector/rules/views/rule-editor.js
+++ b/devtools/client/inspector/rules/views/rule-editor.js
@@ -60,37 +60,45 @@ function RuleEditor(ruleView, rule) {
this.sourceMapURLService = this.toolbox.sourceMapURLService;
this.rule = rule;
this.isEditable = !rule.isSystem;
// Flag that blocks updates of the selector and properties when it is
// being edited
this.isEditing = false;
+ this._onFontSwatchClick = this._onFontSwatchClick.bind(this);
this._onNewProperty = this._onNewProperty.bind(this);
this._newPropertyDestroy = this._newPropertyDestroy.bind(this);
this._onSelectorDone = this._onSelectorDone.bind(this);
this._locationChanged = this._locationChanged.bind(this);
this.updateSourceLink = this.updateSourceLink.bind(this);
this._onToolChanged = this._onToolChanged.bind(this);
this._updateLocation = this._updateLocation.bind(this);
this._onSourceClick = this._onSourceClick.bind(this);
+ this._onRuleUnselected = this._onRuleUnselected.bind(this);
this.rule.domRule.on("location-changed", this._locationChanged);
this.toolbox.on("tool-registered", this._onToolChanged);
this.toolbox.on("tool-unregistered", this._onToolChanged);
+ this.ruleView.on("ruleview-rule-unselected", this._onRuleUnselected);
this._create();
}
RuleEditor.prototype = {
destroy: function() {
this.rule.domRule.off("location-changed");
this.toolbox.off("tool-registered", this._onToolChanged);
this.toolbox.off("tool-unregistered", this._onToolChanged);
+ this.ruleView.off("ruleview-rule-unselected", this._onRuleUnselected);
+
+ if (this.fontSwatch) {
+ this.fontSwatch.removeEventListener("click", this._onFontSwatchClick);
+ }
let url = null;
if (this.rule.sheet) {
url = this.rule.sheet.href || this.rule.sheet.nodeHref;
}
if (url && !this.rule.isSystem && this.rule.domRule.type !== ELEMENT_STYLE) {
// Only get the original source link if the rule isn't a system
// rule and if it isn't an inline rule.
@@ -230,16 +238,64 @@ RuleEditor.prototype = {
this.doc.defaultView.focus();
});
// Create a property editor when the close brace is clicked.
editableItem({ element: this.closeBrace }, () => {
this.newProperty();
});
}
+
+ // Create the font editor toggle icon visible on hover.
+ if (this.ruleView.showFontEditor) {
+ this.fontSwatch = createChild(this.element, "div", {
+ class: "ruleview-fontswatch"
+ });
+
+ // TODO: replace with tool icon and use this as visually hidden a11y text.
+ this.fontSwatch.textContent = "Aa";
+ this.fontSwatch.addEventListener("click", this._onFontSwatchClick);
+ }
+ },
+
+ /**
+ * Handler for clicks on font swatch icon.
+ * Toggles the selected state of the the current rule for the font editor.
+ *
+ * @param {MouseEvent} e
+ * Mouse click event.
+ */
+ _onFontSwatchClick: function(e) {
+ const editorId = "fonteditor";
+ const isActive = e.target.classList.toggle("active");
+
+ if (isActive) {
+ this.ruleView.selectRule(this.rule, editorId);
+ } else {
+ this.ruleView.unselectRule(this.rule, editorId);
+ }
+ },
+
+ /**
+ * Called when a rule was released from being selected for an editor.
+ * A rule may be released by: toggling a swatch icon, an action from an editor
+ * (ex: close), selecting a different node in the markup view, etc.
+ *
+ * @param {Object} eventData
+ * Data payload for the event. Contains:
+ * - {String} editorId - id of the editor for which the rule was released
+ * - {Rule} rule - reference to rule that was released
+ */
+ _onRuleUnselected: function(eventData) {
+ const { rule, editorId } = eventData;
+
+ // If no longer selected for the font editor, toggle the swatch icon.
+ if (editorId === "fonteditor" && rule == this.rule) {
+ this.fontSwatch.classList.remove("active");
+ }
},
/**
* Called when a tool is registered or unregistered.
*/
_onToolChanged: function() {
// When the source editor is registered, update the source links
// to be clickable; and if it is unregistered, update the links to
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -69,16 +69,18 @@ pref("devtools.inspector.shapesHighlight
// Enable the Changes View
pref("devtools.changesview.enabled", false);
// Enable the Events View
pref("devtools.eventsview.enabled", false);
// Enable the Flexbox Inspector panel
pref("devtools.flexboxinspector.enabled", false);
// Enable the new Animation Inspector
pref("devtools.new-animationinspector.enabled", false);
+// Enable the Variable Fonts editor
+pref("devtools.inspector.fonteditor.enabled", false);
// Grid highlighter preferences
pref("devtools.gridinspector.gridOutlineMaxColumns", 50);
pref("devtools.gridinspector.gridOutlineMaxRows", 50);
pref("devtools.gridinspector.showGridAreas", false);
pref("devtools.gridinspector.showGridLineNumbers", false);
pref("devtools.gridinspector.showInfiniteLines", false);
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -512,16 +512,39 @@
background-size: 1em;
}
.ruleview-angleswatch {
background: url("chrome://devtools/skin/images/angle-swatch.svg");
background-size: 1em;
}
+.ruleview-rule:not(:hover) .ruleview-fontswatch:not(.active) {
+ visibility: hidden;
+}
+
+.ruleview-fontswatch {
+ background-color: var(--grey-40);
+ background-size: 1em;
+ color: white;
+ cursor: pointer;
+
+ font-size: .8em;
+ position: absolute;
+ right: 2em;
+ bottom: .5em;
+ padding: .1em .2em;
+
+ -moz-user-select: none;
+}
+
+.ruleview-fontswatch.active {
+ background-color: var(--blue-50);
+}
+
.ruleview-shapeswatch {
background: url("chrome://devtools/skin/images/tool-shadereditor.svg");
-moz-context-properties: fill;
fill: var(--rule-shape-toggle-color);
border-radius: 0;
background-size: 1em;
box-shadow: none;
}