--- a/browser/devtools/highlighter/Makefile.in
+++ b/browser/devtools/highlighter/Makefile.in
@@ -44,15 +44,16 @@ VPATH = @srcdir@
include $(DEPTH)/config/autoconf.mk
EXTRA_JS_MODULES = \
inspector.jsm \
domplate.jsm \
InsideOutBox.jsm \
TreePanel.jsm \
+ highlighter.jsm \
$(NULL)
ifdef ENABLE_TESTS
DIRS += test
endif
include $(topsrcdir)/config/rules.mk
--- a/browser/devtools/highlighter/TreePanel.jsm
+++ b/browser/devtools/highlighter/TreePanel.jsm
@@ -356,17 +356,17 @@ TreePanel.prototype = {
if (node) {
if (hitTwisty) {
this.ioBox.toggleObject(node);
} else {
if (this.IUI.inspecting) {
this.IUI.stopInspecting(true);
} else {
this.IUI.select(node, true, false);
- this.IUI.highlighter.highlightNode(node);
+ this.IUI.highlighter.highlight(node);
}
}
}
},
/**
* Handle double-click events in the html tree panel.
* (double-clicking an attribute value allows it to be edited)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/highlighter/highlighter.jsm
@@ -0,0 +1,878 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Mozilla Highlighter Module.
+ *
+ * The Initial Developer of the Original Code is
+ * The Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Rob Campbell <rcampbell@mozilla.com> (original author)
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ * Julian Viereck <jviereck@mozilla.com>
+ * Paul Rouget <paul@mozilla.com>
+ * Kyle Simpson <ksimpson@mozilla.com>
+ * Johan Charlez <johan.charlez@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const Cu = Components.utils;
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+
+var EXPORTED_SYMBOLS = ["Highlighter"];
+
+const INSPECTOR_INVISIBLE_ELEMENTS = {
+ "head": true,
+ "base": true,
+ "basefont": true,
+ "isindex": true,
+ "link": true,
+ "meta": true,
+ "script": true,
+ "style": true,
+ "title": true,
+};
+
+/**
+ * A highlighter mechanism.
+ *
+ * The highlighter is built dynamically into the browser element.
+ * The caller is in charge of destroying the highlighter (ie, the highlighter
+ * won't be destroyed if a new tab is selected for example).
+ *
+ * API:
+ *
+ * // Constructor and destructor.
+ * // @param aWindow - browser.xul window.
+ * Highlighter(aWindow);
+ * void destroy();
+ *
+ * // Highlight a node.
+ * // @param aNode - node to highlight
+ * // @param aScroll - scroll to ensure the node is visible
+ * void highlight(aNode, aScroll);
+ *
+ * // Get the selected node.
+ * DOMNode getNode();
+ *
+ * // Lock and unlock the select node.
+ * void lock();
+ * void unlock();
+ *
+ * // Show and hide the highlighter
+ * void show();
+ * void hide();
+ * boolean isHidden();
+ *
+ * // Redraw the highlighter if the visible portion of the node has changed.
+ * void invalidateSize(aScroll);
+ *
+ * // Is a node highlightable.
+ * boolean isNodeHighlightable(aNode);
+ *
+ * // Add/Remove lsiteners
+ * // @param aEvent - event name
+ * // @param aListener - function callback
+ * void addListener(aEvent, aListener);
+ * void removeListener(aEvent, aListener);
+ *
+ * Events:
+ *
+ * "closed" - Highlighter is closing
+ * "nodeselected" - A new node has been selected
+ * "highlighting" - Highlighter is highlighting
+ * "locked" - The selected node has been locked
+ * "unlocked" - The selected ndoe has been unlocked
+ *
+ * Structure:
+ *
+ * <stack id="highlighter-container">
+ * <vbox id="highlighter-veil-container">...</vbox>
+ * <box id="highlighter-controls>...</vbox>
+ * </stack>
+ *
+ */
+
+
+/**
+ * Constructor.
+ *
+ * @param object aWindow
+ */
+function Highlighter(aWindow)
+{
+ this.chromeWin = aWindow;
+ this.tabbrowser = aWindow.gBrowser;
+ this.chromeDoc = aWindow.document;
+ this.browser = aWindow.gBrowser.selectedBrowser;
+ this.events = {};
+
+ this._init();
+}
+
+Highlighter.prototype = {
+ _init: function Highlighter__init()
+ {
+ let stack = this.browser.parentNode;
+ this.win = this.browser.contentWindow;
+ this._highlighting = false;
+
+ this.highlighterContainer = this.chromeDoc.createElement("stack");
+ this.highlighterContainer.id = "highlighter-container";
+
+ this.veilContainer = this.chromeDoc.createElement("vbox");
+ this.veilContainer.id = "highlighter-veil-container";
+
+ // The controlsBox will host the different interactive
+ // elements of the highlighter (buttons, toolbars, ...).
+ let controlsBox = this.chromeDoc.createElement("box");
+ controlsBox.id = "highlighter-controls";
+ this.highlighterContainer.appendChild(this.veilContainer);
+ this.highlighterContainer.appendChild(controlsBox);
+
+ stack.appendChild(this.highlighterContainer);
+
+ // The veil will make the whole page darker except
+ // for the region of the selected box.
+ this.buildVeil(this.veilContainer);
+
+ this.buildInfobar(controlsBox);
+
+ this.transitionDisabler = null;
+
+ this.computeZoomFactor();
+ this.unlock();
+ this.hide();
+ },
+
+ /**
+ * Destroy the nodes. Remove listeners.
+ */
+ destroy: function Highlighter_destroy()
+ {
+ this.detachKeysListeners();
+ this.detachMouseListeners();
+ this.detachPageListeners();
+
+ this.chromeWin.clearTimeout(this.transitionDisabler);
+ this.boundCloseEventHandler = null;
+ this._contentRect = null;
+ this._highlightRect = null;
+ this._highlighting = false;
+ this.veilTopBox = null;
+ this.veilLeftBox = null;
+ this.veilMiddleBox = null;
+ this.veilTransparentBox = null;
+ this.veilContainer = null;
+ this.node = null;
+ this.nodeInfo = null;
+ this.highlighterContainer.parentNode.removeChild(this.highlighterContainer);
+ this.highlighterContainer = null;
+ this.win = null
+ this.browser = null;
+ this.chromeDoc = null;
+ this.chromeWin = null;
+ this.tabbrowser = null;
+
+ this.emitEvent("closed");
+ this.removeAllListeners();
+ },
+
+ /**
+ * Show the veil, and select a node.
+ * If no node is specified, the previous selected node is highlighted if any.
+ * If no node was selected, the root element is selected.
+ *
+ * @param aNode [optional] - The node to be selected.
+ * @param aScroll [optional] boolean
+ * Should we scroll to ensure that the selected node is visible.
+ */
+ highlight: function Highlighter_highlight(aNode, aScroll)
+ {
+ if (this.hidden)
+ this.show();
+
+ let oldNode = this.node;
+
+ if (!aNode) {
+ if (!this.node)
+ this.node = this.win.document.documentElement;
+ } else {
+ this.node = aNode;
+ }
+
+ if (oldNode !== this.node) {
+ this.updateInfobar();
+ }
+
+ this.invalidateSize(!!aScroll);
+
+ if (oldNode !== this.node) {
+ this.emitEvent("nodeselected");
+ }
+ },
+
+ /**
+ * Update the highlighter size and position.
+ */
+ invalidateSize: function Highlighter_invalidateSize(aScroll)
+ {
+ let rect = null;
+
+ if (this.node && this.isNodeHighlightable(this.node)) {
+
+ if (aScroll &&
+ this.node.scrollIntoView) { // XUL elements don't have such method
+ this.node.scrollIntoView();
+ }
+ let clientRect = this.node.getBoundingClientRect();
+ rect = LayoutHelpers.getDirtyRect(this.node);
+ }
+
+ this.highlightRectangle(rect);
+
+ this.moveInfobar();
+
+ if (this._highlighting) {
+ this.emitEvent("highlighting");
+ }
+ },
+
+ /**
+ * Returns the selected node.
+ *
+ * @returns node
+ */
+ getNode: function() {
+ return this.node;
+ },
+
+ /**
+ * Show the highlighter if it has been hidden.
+ */
+ show: function() {
+ if (!this.hidden) return;
+ this.veilContainer.removeAttribute("hidden");
+ this.nodeInfo.container.removeAttribute("hidden");
+ this.attachKeysListeners();
+ this.attachPageListeners();
+ this.invalidateSize();
+ this.hidden = false;
+ },
+
+ /**
+ * Hide the highlighter, the veil and the infobar.
+ */
+ hide: function() {
+ if (this.hidden) return;
+ this.veilContainer.setAttribute("hidden", "true");
+ this.nodeInfo.container.setAttribute("hidden", "true");
+ this.detachKeysListeners();
+ this.detachPageListeners();
+ this.hidden = true;
+ },
+
+ /**
+ * Is the highlighter visible?
+ *
+ * @return boolean
+ */
+ isHidden: function() {
+ return this.hidden;
+ },
+
+ /**
+ * Lock a node. Stops the inspection.
+ */
+ lock: function() {
+ if (this.locked === true) return;
+ this.veilContainer.setAttribute("locked", "true");
+ this.nodeInfo.container.setAttribute("locked", "true");
+ this.detachMouseListeners();
+ this.locked = true;
+ this.emitEvent("locked");
+ },
+
+ /**
+ * Start inspecting.
+ * Unlock the current node (if any), and select any node being hovered.
+ */
+ unlock: function() {
+ if (this.locked === false) return;
+ this.veilContainer.removeAttribute("locked");
+ this.nodeInfo.container.removeAttribute("locked");
+ this.attachMouseListeners();
+ this.locked = false;
+ this.emitEvent("unlocked");
+ },
+
+ /**
+ * Is the specified node highlightable?
+ *
+ * @param nsIDOMNode aNode
+ * the DOM element in question
+ * @returns boolean
+ * True if the node is highlightable or false otherwise.
+ */
+ isNodeHighlightable: function Highlighter_isNodeHighlightable(aNode)
+ {
+ if (aNode.nodeType != aNode.ELEMENT_NODE) {
+ return false;
+ }
+ let nodeName = aNode.nodeName.toLowerCase();
+ return !INSPECTOR_INVISIBLE_ELEMENTS[nodeName];
+ },
+ /**
+ * Build the veil:
+ *
+ * <vbox id="highlighter-veil-container">
+ * <box id="highlighter-veil-topbox" class="highlighter-veil"/>
+ * <hbox id="highlighter-veil-middlebox">
+ * <box id="highlighter-veil-leftbox" class="highlighter-veil"/>
+ * <box id="highlighter-veil-transparentbox"/>
+ * <box id="highlighter-veil-rightbox" class="highlighter-veil"/>
+ * </hbox>
+ * <box id="highlighter-veil-bottombox" class="highlighter-veil"/>
+ * </vbox>
+ *
+ * @param nsIDOMElement aParent
+ * The container of the veil boxes.
+ */
+
+ buildVeil: function Highlighter_buildVeil(aParent)
+ {
+ // We will need to resize these boxes to surround a node.
+ // See highlightRectangle().
+
+ this.veilTopBox = this.chromeDoc.createElement("box");
+ this.veilTopBox.id = "highlighter-veil-topbox";
+ this.veilTopBox.className = "highlighter-veil";
+
+ this.veilMiddleBox = this.chromeDoc.createElement("hbox");
+ this.veilMiddleBox.id = "highlighter-veil-middlebox";
+
+ this.veilLeftBox = this.chromeDoc.createElement("box");
+ this.veilLeftBox.id = "highlighter-veil-leftbox";
+ this.veilLeftBox.className = "highlighter-veil";
+
+ this.veilTransparentBox = this.chromeDoc.createElement("box");
+ this.veilTransparentBox.id = "highlighter-veil-transparentbox";
+
+ // We don't need any references to veilRightBox and veilBottomBox.
+ // These boxes are automatically resized (flex=1)
+
+ let veilRightBox = this.chromeDoc.createElement("box");
+ veilRightBox.id = "highlighter-veil-rightbox";
+ veilRightBox.className = "highlighter-veil";
+
+ let veilBottomBox = this.chromeDoc.createElement("box");
+ veilBottomBox.id = "highlighter-veil-bottombox";
+ veilBottomBox.className = "highlighter-veil";
+
+ this.veilMiddleBox.appendChild(this.veilLeftBox);
+ this.veilMiddleBox.appendChild(this.veilTransparentBox);
+ this.veilMiddleBox.appendChild(veilRightBox);
+
+ aParent.appendChild(this.veilTopBox);
+ aParent.appendChild(this.veilMiddleBox);
+ aParent.appendChild(veilBottomBox);
+ },
+
+ /**
+ * Build the node Infobar.
+ *
+ * <box id="highlighter-nodeinfobar-container">
+ * <box id="Highlighter-nodeinfobar-arrow-top"/>
+ * <vbox id="highlighter-nodeinfobar">
+ * <label id="highlighter-nodeinfobar-tagname"/>
+ * <label id="highlighter-nodeinfobar-id"/>
+ * <vbox id="highlighter-nodeinfobar-classes"/>
+ * </vbox>
+ * <box id="Highlighter-nodeinfobar-arrow-bottom"/>
+ * </box>
+ *
+ * @param nsIDOMElement aParent
+ * The container of the infobar.
+ */
+ buildInfobar: function Highlighter_buildInfobar(aParent)
+ {
+ let container = this.chromeDoc.createElement("box");
+ container.id = "highlighter-nodeinfobar-container";
+ container.setAttribute("position", "top");
+ container.setAttribute("disabled", "true");
+
+ let nodeInfobar = this.chromeDoc.createElement("hbox");
+ nodeInfobar.id = "highlighter-nodeinfobar";
+
+ let arrowBoxTop = this.chromeDoc.createElement("box");
+ arrowBoxTop.className = "highlighter-nodeinfobar-arrow";
+ arrowBoxTop.id = "highlighter-nodeinfobar-arrow-top";
+
+ let arrowBoxBottom = this.chromeDoc.createElement("box");
+ arrowBoxBottom.className = "highlighter-nodeinfobar-arrow";
+ arrowBoxBottom.id = "highlighter-nodeinfobar-arrow-bottom";
+
+ let tagNameLabel = this.chromeDoc.createElement("label");
+ tagNameLabel.id = "highlighter-nodeinfobar-tagname";
+ tagNameLabel.className = "plain";
+
+ let idLabel = this.chromeDoc.createElement("label");
+ idLabel.id = "highlighter-nodeinfobar-id";
+ idLabel.className = "plain";
+
+ let classesBox = this.chromeDoc.createElement("hbox");
+ classesBox.id = "highlighter-nodeinfobar-classes";
+
+ nodeInfobar.appendChild(tagNameLabel);
+ nodeInfobar.appendChild(idLabel);
+ nodeInfobar.appendChild(classesBox);
+ container.appendChild(arrowBoxTop);
+ container.appendChild(nodeInfobar);
+ container.appendChild(arrowBoxBottom);
+
+ aParent.appendChild(container);
+
+ let barHeight = container.getBoundingClientRect().height;
+
+ this.nodeInfo = {
+ tagNameLabel: tagNameLabel,
+ idLabel: idLabel,
+ classesBox: classesBox,
+ container: container,
+ barHeight: barHeight,
+ };
+ },
+
+ /**
+ * Highlight a rectangular region.
+ *
+ * @param object aRect
+ * The rectangle region to highlight.
+ * @returns boolean
+ * True if the rectangle was highlighted, false otherwise.
+ */
+ highlightRectangle: function Highlighter_highlightRectangle(aRect)
+ {
+ if (!aRect) {
+ this.unhighlight();
+ return;
+ }
+
+ let oldRect = this._contentRect;
+
+ if (oldRect && aRect.top == oldRect.top && aRect.left == oldRect.left &&
+ aRect.width == oldRect.width && aRect.height == oldRect.height) {
+ return; // same rectangle
+ }
+
+ let aRectScaled = LayoutHelpers.getZoomedRect(this.win, aRect);
+
+ if (aRectScaled.left >= 0 && aRectScaled.top >= 0 &&
+ aRectScaled.width > 0 && aRectScaled.height > 0) {
+
+ this.veilTransparentBox.style.visibility = "visible";
+
+ // The bottom div and the right div are flexibles (flex=1).
+ // We don't need to resize them.
+ this.veilTopBox.style.height = aRectScaled.top + "px";
+ this.veilLeftBox.style.width = aRectScaled.left + "px";
+ this.veilMiddleBox.style.height = aRectScaled.height + "px";
+ this.veilTransparentBox.style.width = aRectScaled.width + "px";
+
+ this._highlighting = true;
+ } else {
+ this.unhighlight();
+ }
+
+ this._contentRect = aRect; // save orig (non-scaled) rect
+ this._highlightRect = aRectScaled; // and save the scaled rect.
+
+ return;
+ },
+
+ /**
+ * Clear the highlighter surface.
+ */
+ unhighlight: function Highlighter_unhighlight()
+ {
+ this._highlighting = false;
+ this.veilMiddleBox.style.height = 0;
+ this.veilTransparentBox.style.width = 0;
+ this.veilTransparentBox.style.visibility = "hidden";
+ },
+
+ /**
+ * Update node information (tagName#id.class)
+ */
+ updateInfobar: function Highlighter_updateInfobar()
+ {
+ // Tag name
+ this.nodeInfo.tagNameLabel.textContent = this.node.tagName;
+
+ // ID
+ this.nodeInfo.idLabel.textContent = this.node.id ? "#" + this.node.id : "";
+
+ // Classes
+ let classes = this.nodeInfo.classesBox;
+ while (classes.hasChildNodes()) {
+ classes.removeChild(classes.firstChild);
+ }
+
+ if (this.node.className) {
+ let fragment = this.chromeDoc.createDocumentFragment();
+ for (let i = 0; i < this.node.classList.length; i++) {
+ let classLabel = this.chromeDoc.createElement("label");
+ classLabel.className = "highlighter-nodeinfobar-class plain";
+ classLabel.textContent = "." + this.node.classList[i];
+ fragment.appendChild(classLabel);
+ }
+ classes.appendChild(fragment);
+ }
+ },
+
+ /**
+ * Move the Infobar to the right place in the highlighter.
+ */
+ moveInfobar: function Highlighter_moveInfobar()
+ {
+ if (this._highlightRect) {
+ let winHeight = this.win.innerHeight * this.zoom;
+ let winWidth = this.win.innerWidth * this.zoom;
+
+ let rect = {top: this._highlightRect.top,
+ left: this._highlightRect.left,
+ width: this._highlightRect.width,
+ height: this._highlightRect.height};
+
+ rect.top = Math.max(rect.top, 0);
+ rect.left = Math.max(rect.left, 0);
+ rect.width = Math.max(rect.width, 0);
+ rect.height = Math.max(rect.height, 0);
+
+ rect.top = Math.min(rect.top, winHeight);
+ rect.left = Math.min(rect.left, winWidth);
+
+ this.nodeInfo.container.removeAttribute("disabled");
+ // Can the bar be above the node?
+ if (rect.top < this.nodeInfo.barHeight) {
+ // No. Can we move the toolbar under the node?
+ if (rect.top + rect.height +
+ this.nodeInfo.barHeight > winHeight) {
+ // No. Let's move it inside.
+ this.nodeInfo.container.style.top = rect.top + "px";
+ this.nodeInfo.container.setAttribute("position", "overlap");
+ } else {
+ // Yes. Let's move it under the node.
+ this.nodeInfo.container.style.top = rect.top + rect.height + "px";
+ this.nodeInfo.container.setAttribute("position", "bottom");
+ }
+ } else {
+ // Yes. Let's move it on top of the node.
+ this.nodeInfo.container.style.top =
+ rect.top - this.nodeInfo.barHeight + "px";
+ this.nodeInfo.container.setAttribute("position", "top");
+ }
+
+ let barWidth = this.nodeInfo.container.getBoundingClientRect().width;
+ let left = rect.left + rect.width / 2 - barWidth / 2;
+
+ // Make sure the whole infobar is visible
+ if (left < 0) {
+ left = 0;
+ this.nodeInfo.container.setAttribute("hide-arrow", "true");
+ } else {
+ if (left + barWidth > winWidth) {
+ left = winWidth - barWidth;
+ this.nodeInfo.container.setAttribute("hide-arrow", "true");
+ } else {
+ this.nodeInfo.container.removeAttribute("hide-arrow");
+ }
+ }
+ this.nodeInfo.container.style.left = left + "px";
+ } else {
+ this.nodeInfo.container.style.left = "0";
+ this.nodeInfo.container.style.top = "0";
+ this.nodeInfo.container.setAttribute("position", "top");
+ this.nodeInfo.container.setAttribute("hide-arrow", "true");
+ }
+ },
+
+ /**
+ * Store page zoom factor.
+ */
+ computeZoomFactor: function Highlighter_computeZoomFactor() {
+ this.zoom =
+ this.win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils)
+ .screenPixelsPerCSSPixel;
+ },
+
+ /////////////////////////////////////////////////////////////////////////
+ //// Event Emitter Mechanism
+
+ addListener: function Highlighter_addListener(aEvent, aListener)
+ {
+ if (!(aEvent in this.events))
+ this.events[aEvent] = [];
+ this.events[aEvent].push(aListener);
+ },
+
+ removeListener: function Highlighter_removeListener(aEvent, aListener)
+ {
+ if (!(aEvent in this.events))
+ return;
+ let idx = this.events[aEvent].indexOf(aListener);
+ if (idx > -1)
+ this.events[aEvent].splice(idx, 1);
+ },
+
+ emitEvent: function Highlighter_emitEvent(aEvent, aArgv)
+ {
+ if (!(aEvent in this.events))
+ return;
+
+ let listeners = this.events[aEvent];
+ let highlighter = this;
+ listeners.forEach(function(aListener) {
+ try {
+ aListener.apply(highlighter, aArgv);
+ } catch(e) {}
+ });
+ },
+
+ removeAllListeners: function Highlighter_removeAllIsteners()
+ {
+ for (let event in this.events) {
+ delete this.events[event];
+ }
+ },
+
+ /////////////////////////////////////////////////////////////////////////
+ //// Event Handling
+
+ attachMouseListeners: function Highlighter_attachMouseListeners()
+ {
+ this.browser.addEventListener("mousemove", this, true);
+ this.browser.addEventListener("click", this, true);
+ this.browser.addEventListener("dblclick", this, true);
+ this.browser.addEventListener("mousedown", this, true);
+ this.browser.addEventListener("mouseup", this, true);
+ },
+
+ detachMouseListeners: function Highlighter_detachMouseListeners()
+ {
+ this.browser.removeEventListener("mousemove", this, true);
+ this.browser.removeEventListener("click", this, true);
+ this.browser.removeEventListener("dblclick", this, true);
+ this.browser.removeEventListener("mousedown", this, true);
+ this.browser.removeEventListener("mouseup", this, true);
+ },
+
+ attachPageListeners: function Highlighter_attachPageListeners()
+ {
+ this.browser.addEventListener("resize", this, true);
+ this.browser.addEventListener("scroll", this, true);
+ },
+
+ detachPageListeners: function Highlighter_detachPageListeners()
+ {
+ this.browser.removeEventListener("resize", this, true);
+ this.browser.removeEventListener("scroll", this, true);
+ },
+
+ attachKeysListeners: function Highlighter_attachKeysListeners()
+ {
+ this.browser.addEventListener("keypress", this, true);
+ this.highlighterContainer.addEventListener("keypress", this, true);
+ },
+
+ detachKeysListeners: function Highlighter_detachKeysListeners()
+ {
+ this.browser.removeEventListener("keypress", this, true);
+ this.highlighterContainer.removeEventListener("keypress", this, true);
+ },
+
+ /**
+ * Generic event handler.
+ *
+ * @param nsIDOMEvent aEvent
+ * The DOM event object.
+ */
+ handleEvent: function Highlighter_handleEvent(aEvent)
+ {
+ switch (aEvent.type) {
+ case "click":
+ this.handleClick(aEvent);
+ break;
+ case "mousemove":
+ this.handleMouseMove(aEvent);
+ break;
+ case "resize":
+ case "scroll":
+ this.computeZoomFactor();
+ this.brieflyDisableTransitions();
+ this.invalidateSize();
+ break;
+ case "dblclick":
+ case "mousedown":
+ case "mouseup":
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ break;
+ break;
+ case "keypress":
+ switch (aEvent.keyCode) {
+ case this.chromeWin.KeyEvent.DOM_VK_RETURN:
+ this.locked ? this.unlock() : this.lock();
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ break;
+ case this.chromeWin.KeyEvent.DOM_VK_LEFT:
+ let node;
+ if (this.node) {
+ node = this.node.parentNode;
+ } else {
+ node = this.defaultSelection;
+ }
+ if (node && this.isNodeHighlightable(node)) {
+ this.highlight(node);
+ }
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ break;
+ case this.chromeWin.KeyEvent.DOM_VK_RIGHT:
+ if (this.node) {
+ // Find the first child that is highlightable.
+ for (let i = 0; i < this.node.childNodes.length; i++) {
+ node = this.node.childNodes[i];
+ if (node && this.isNodeHighlightable(node)) {
+ break;
+ }
+ }
+ } else {
+ node = this.defaultSelection;
+ }
+ if (node && this.isNodeHighlightable(node)) {
+ this.highlight(node, true);
+ }
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ break;
+ case this.chromeWin.KeyEvent.DOM_VK_UP:
+ if (this.node) {
+ // Find a previous sibling that is highlightable.
+ node = this.node.previousSibling;
+ while (node && !this.isNodeHighlightable(node)) {
+ node = node.previousSibling;
+ }
+ } else {
+ node = this.defaultSelection;
+ }
+ if (node && this.isNodeHighlightable(node)) {
+ this.highlight(node, true);
+ }
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ break;
+ case this.chromeWin.KeyEvent.DOM_VK_DOWN:
+ if (this.node) {
+ // Find a next sibling that is highlightable.
+ node = this.node.nextSibling;
+ while (node && !this.isNodeHighlightable(node)) {
+ node = node.nextSibling;
+ }
+ } else {
+ node = this.defaultSelection;
+ }
+ if (node && this.isNodeHighlightable(node)) {
+ this.highlight(node, true);
+ }
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ break;
+ }
+ }
+ },
+
+ /**
+ * Disable the CSS transitions for a short time to avoid laggy animations
+ * during scrolling or resizing.
+ */
+ brieflyDisableTransitions: function Highlighter_brieflyDisableTransitions()
+ {
+ if (this.transitionDisabler) {
+ this.chromeWin.clearTimeout(this.transitionDisabler);
+ } else {
+ this.veilContainer.setAttribute("disable-transitions", "true");
+ this.nodeInfo.container.setAttribute("disable-transitions", "true");
+ }
+ this.transitionDisabler =
+ this.chromeWin.setTimeout(function() {
+ this.veilContainer.removeAttribute("disable-transitions");
+ this.nodeInfo.container.removeAttribute("disable-transitions");
+ this.transitionDisabler = null;
+ }.bind(this), 500);
+ },
+
+ /**
+ * Handle clicks.
+ *
+ * @param nsIDOMEvent aEvent
+ * The DOM event.
+ */
+ handleClick: function Highlighter_handleClick(aEvent)
+ {
+ // Stop inspection when the user clicks on a node.
+ if (aEvent.button == 0) {
+ let win = aEvent.target.ownerDocument.defaultView;
+ this.lock();
+ win.focus();
+ }
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ },
+
+ /**
+ * Handle mousemoves in panel.
+ *
+ * @param nsiDOMEvent aEvent
+ * The MouseEvent triggering the method.
+ */
+ handleMouseMove: function Highlighter_handleMouseMove(aEvent)
+ {
+ let element = LayoutHelpers.getElementFromPoint(aEvent.target.ownerDocument,
+ aEvent.clientX, aEvent.clientY);
+ if (element && element != this.node) {
+ this.highlight(element);
+ }
+ },
+};
+
+///////////////////////////////////////////////////////////////////////////
+
--- a/browser/devtools/highlighter/inspector.jsm
+++ b/browser/devtools/highlighter/inspector.jsm
@@ -47,37 +47,21 @@ const Ci = Components.interfaces;
const Cr = Components.results;
var EXPORTED_SYMBOLS = ["InspectorUI"];
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/TreePanel.jsm");
Cu.import("resource:///modules/devtools/CssRuleView.jsm");
-
-const INSPECTOR_INVISIBLE_ELEMENTS = {
- "head": true,
- "base": true,
- "basefont": true,
- "isindex": true,
- "link": true,
- "meta": true,
- "script": true,
- "style": true,
- "title": true,
-};
+Cu.import("resource:///modules/highlighter.jsm");
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
// Inspector notifications dispatched through the nsIObserverService.
const INSPECTOR_NOTIFICATIONS = {
- // Fires once the Inspector highlights an element in the page.
- HIGHLIGHTING: "inspector-highlighting",
-
- // Fires once the Inspector stops highlighting any element.
- UNHIGHLIGHTING: "inspector-unhighlighting",
-
// Fires once the Inspector completes the initialization and opens up on
// screen.
OPENED: "inspector-opened",
// Fires once the Inspector is closed.
CLOSED: "inspector-closed",
// Fires once the Inspector is destroyed. Not fired on tab switch.
@@ -94,693 +78,16 @@ const INSPECTOR_NOTIFICATIONS = {
// Event notifications for the attribute-value editor
EDITOR_OPENED: "inspector-editor-opened",
EDITOR_CLOSED: "inspector-editor-closed",
EDITOR_SAVED: "inspector-editor-saved",
};
///////////////////////////////////////////////////////////////////////////
-//// Highlighter
-
-/**
- * A highlighter mechanism.
- *
- * The highlighter is built dynamically once the Inspector is invoked:
- * <stack id="highlighter-container">
- * <vbox id="highlighter-veil-container">...</vbox>
- * <box id="highlighter-controls>...</vbox>
- * </stack>
- *
- * @param object aInspector
- * The InspectorUI instance.
- */
-function Highlighter(aInspector)
-{
- this.IUI = aInspector;
- this._init();
-}
-
-Highlighter.prototype = {
- _init: function Highlighter__init()
- {
- this.browser = this.IUI.browser;
- this.chromeDoc = this.IUI.chromeDoc;
-
- let stack = this.browser.parentNode;
- this.win = this.browser.contentWindow;
- this._highlighting = false;
-
- this.highlighterContainer = this.chromeDoc.createElement("stack");
- this.highlighterContainer.id = "highlighter-container";
-
- this.veilContainer = this.chromeDoc.createElement("vbox");
- this.veilContainer.id = "highlighter-veil-container";
-
- // The controlsBox will host the different interactive
- // elements of the highlighter (buttons, toolbars, ...).
- let controlsBox = this.chromeDoc.createElement("box");
- controlsBox.id = "highlighter-controls";
- this.highlighterContainer.appendChild(this.veilContainer);
- this.highlighterContainer.appendChild(controlsBox);
-
- stack.appendChild(this.highlighterContainer);
-
- // The veil will make the whole page darker except
- // for the region of the selected box.
- this.buildVeil(this.veilContainer);
-
- this.buildInfobar(controlsBox);
-
- if (!this.IUI.store.getValue(this.winID, "inspecting")) {
- this.veilContainer.setAttribute("locked", true);
- this.nodeInfo.container.setAttribute("locked", true);
- }
-
- this.browser.addEventListener("resize", this, true);
- this.browser.addEventListener("scroll", this, true);
-
- this.transitionDisabler = null;
-
- this.computeZoomFactor();
- this.handleResize();
- },
-
- /**
- * Build the veil:
- *
- * <vbox id="highlighter-veil-container">
- * <box id="highlighter-veil-topbox" class="highlighter-veil"/>
- * <hbox id="highlighter-veil-middlebox">
- * <box id="highlighter-veil-leftbox" class="highlighter-veil"/>
- * <box id="highlighter-veil-transparentbox"/>
- * <box id="highlighter-veil-rightbox" class="highlighter-veil"/>
- * </hbox>
- * <box id="highlighter-veil-bottombox" class="highlighter-veil"/>
- * </vbox>
- *
- * @param nsIDOMElement aParent
- * The container of the veil boxes.
- */
- buildVeil: function Highlighter_buildVeil(aParent)
- {
- // We will need to resize these boxes to surround a node.
- // See highlightRectangle().
-
- this.veilTopBox = this.chromeDoc.createElement("box");
- this.veilTopBox.id = "highlighter-veil-topbox";
- this.veilTopBox.className = "highlighter-veil";
-
- this.veilMiddleBox = this.chromeDoc.createElement("hbox");
- this.veilMiddleBox.id = "highlighter-veil-middlebox";
-
- this.veilLeftBox = this.chromeDoc.createElement("box");
- this.veilLeftBox.id = "highlighter-veil-leftbox";
- this.veilLeftBox.className = "highlighter-veil";
-
- this.veilTransparentBox = this.chromeDoc.createElement("box");
- this.veilTransparentBox.id = "highlighter-veil-transparentbox";
-
- // We don't need any references to veilRightBox and veilBottomBox.
- // These boxes are automatically resized (flex=1)
-
- let veilRightBox = this.chromeDoc.createElement("box");
- veilRightBox.id = "highlighter-veil-rightbox";
- veilRightBox.className = "highlighter-veil";
-
- let veilBottomBox = this.chromeDoc.createElement("box");
- veilBottomBox.id = "highlighter-veil-bottombox";
- veilBottomBox.className = "highlighter-veil";
-
- this.veilMiddleBox.appendChild(this.veilLeftBox);
- this.veilMiddleBox.appendChild(this.veilTransparentBox);
- this.veilMiddleBox.appendChild(veilRightBox);
-
- aParent.appendChild(this.veilTopBox);
- aParent.appendChild(this.veilMiddleBox);
- aParent.appendChild(veilBottomBox);
- },
-
- /**
- * Build the node Infobar.
- *
- * <box id="highlighter-nodeinfobar-container">
- * <box id="Highlighter-nodeinfobar-arrow-top"/>
- * <vbox id="highlighter-nodeinfobar">
- * <label id="highlighter-nodeinfobar-tagname"/>
- * <label id="highlighter-nodeinfobar-id"/>
- * <vbox id="highlighter-nodeinfobar-classes"/>
- * </vbox>
- * <box id="Highlighter-nodeinfobar-arrow-bottom"/>
- * </box>
- *
- * @param nsIDOMElement aParent
- * The container of the infobar.
- */
- buildInfobar: function Highlighter_buildInfobar(aParent)
- {
- let container = this.chromeDoc.createElement("box");
- container.id = "highlighter-nodeinfobar-container";
- container.setAttribute("position", "top");
- container.setAttribute("disabled", "true");
-
- let nodeInfobar = this.chromeDoc.createElement("hbox");
- nodeInfobar.id = "highlighter-nodeinfobar";
-
- let arrowBoxTop = this.chromeDoc.createElement("box");
- arrowBoxTop.className = "highlighter-nodeinfobar-arrow";
- arrowBoxTop.id = "highlighter-nodeinfobar-arrow-top";
-
- let arrowBoxBottom = this.chromeDoc.createElement("box");
- arrowBoxBottom.className = "highlighter-nodeinfobar-arrow";
- arrowBoxBottom.id = "highlighter-nodeinfobar-arrow-bottom";
-
- let tagNameLabel = this.chromeDoc.createElement("label");
- tagNameLabel.id = "highlighter-nodeinfobar-tagname";
- tagNameLabel.className = "plain";
-
- let idLabel = this.chromeDoc.createElement("label");
- idLabel.id = "highlighter-nodeinfobar-id";
- idLabel.className = "plain";
-
- let classesBox = this.chromeDoc.createElement("hbox");
- classesBox.id = "highlighter-nodeinfobar-classes";
-
- nodeInfobar.appendChild(tagNameLabel);
- nodeInfobar.appendChild(idLabel);
- nodeInfobar.appendChild(classesBox);
- container.appendChild(arrowBoxTop);
- container.appendChild(nodeInfobar);
- container.appendChild(arrowBoxBottom);
-
- aParent.appendChild(container);
-
- let barHeight = container.getBoundingClientRect().height;
-
- this.nodeInfo = {
- tagNameLabel: tagNameLabel,
- idLabel: idLabel,
- classesBox: classesBox,
- container: container,
- barHeight: barHeight,
- };
- },
-
- /**
- * Destroy the nodes.
- */
- destroy: function Highlighter_destroy()
- {
- this.IUI.win.clearTimeout(this.transitionDisabler);
- this.browser.removeEventListener("scroll", this, true);
- this.browser.removeEventListener("resize", this, true);
- this.boundCloseEventHandler = null;
- this._contentRect = null;
- this._highlightRect = null;
- this._highlighting = false;
- this.veilTopBox = null;
- this.veilLeftBox = null;
- this.veilMiddleBox = null;
- this.veilTransparentBox = null;
- this.veilContainer = null;
- this.node = null;
- this.nodeInfo = null;
- this.highlighterContainer.parentNode.removeChild(this.highlighterContainer);
- this.highlighterContainer = null;
- this.win = null
- this.browser = null;
- this.chromeDoc = null;
- this.IUI = null;
- },
-
- /**
- * Is the highlighter highlighting? Public method for querying the state
- * of the highlighter.
- */
- get isHighlighting() {
- return this._highlighting;
- },
-
- /**
- * Highlight this.node, unhilighting first if necessary.
- *
- * @param boolean aScroll
- * Boolean determining whether to scroll or not.
- */
- highlight: function Highlighter_highlight(aScroll)
- {
- let rect = null;
-
- if (this.node && this.isNodeHighlightable(this.node)) {
-
- if (aScroll) {
- this.node.scrollIntoView();
- }
-
- let clientRect = this.node.getBoundingClientRect();
-
- // Go up in the tree of frames to determine the correct rectangle.
- // clientRect is read-only, we need to be able to change properties.
- rect = {top: clientRect.top,
- left: clientRect.left,
- width: clientRect.width,
- height: clientRect.height};
-
- let frameWin = this.node.ownerDocument.defaultView;
-
- // We iterate through all the parent windows.
- while (true) {
-
- // Does the selection overflow on the right of its window?
- let diffx = frameWin.innerWidth - (rect.left + rect.width);
- if (diffx < 0) {
- rect.width += diffx;
- }
-
- // Does the selection overflow on the bottom of its window?
- let diffy = frameWin.innerHeight - (rect.top + rect.height);
- if (diffy < 0) {
- rect.height += diffy;
- }
-
- // Does the selection overflow on the left of its window?
- if (rect.left < 0) {
- rect.width += rect.left;
- rect.left = 0;
- }
-
- // Does the selection overflow on the top of its window?
- if (rect.top < 0) {
- rect.height += rect.top;
- rect.top = 0;
- }
-
- // Selection has been clipped to fit in its own window.
-
- // Are we in the top-level window?
- if (frameWin.parent === frameWin || !frameWin.frameElement) {
- break;
- }
-
- // We are in an iframe.
- // We take into account the parent iframe position and its
- // offset (borders and padding).
- let frameRect = frameWin.frameElement.getBoundingClientRect();
-
- let [offsetTop, offsetLeft] =
- this.IUI.getIframeContentOffset(frameWin.frameElement);
-
- rect.top += frameRect.top + offsetTop;
- rect.left += frameRect.left + offsetLeft;
-
- frameWin = frameWin.parent;
- }
- }
-
- this.highlightRectangle(rect);
-
- this.moveInfobar();
-
- if (this._highlighting) {
- Services.obs.notifyObservers(null,
- INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, null);
- }
- },
-
- /**
- * Highlight the given node.
- *
- * @param nsIDOMNode aNode
- * a DOM element to be highlighted
- * @param object aParams
- * extra parameters object
- */
- highlightNode: function Highlighter_highlightNode(aNode, aParams)
- {
- this.node = aNode;
- this.updateInfobar();
- this.highlight(aParams && aParams.scroll);
- },
-
- /**
- * Highlight a rectangular region.
- *
- * @param object aRect
- * The rectangle region to highlight.
- * @returns boolean
- * True if the rectangle was highlighted, false otherwise.
- */
- highlightRectangle: function Highlighter_highlightRectangle(aRect)
- {
- if (!aRect) {
- this.unhighlight();
- return;
- }
-
- let oldRect = this._contentRect;
-
- if (oldRect && aRect.top == oldRect.top && aRect.left == oldRect.left &&
- aRect.width == oldRect.width && aRect.height == oldRect.height) {
- return this._highlighting; // same rectangle
- }
-
- // adjust rect for zoom scaling
- let aRectScaled = {};
- for (let prop in aRect) {
- aRectScaled[prop] = aRect[prop] * this.zoom;
- }
-
- if (aRectScaled.left >= 0 && aRectScaled.top >= 0 &&
- aRectScaled.width > 0 && aRectScaled.height > 0) {
-
- this.veilTransparentBox.style.visibility = "visible";
-
- // The bottom div and the right div are flexibles (flex=1).
- // We don't need to resize them.
- this.veilTopBox.style.height = aRectScaled.top + "px";
- this.veilLeftBox.style.width = aRectScaled.left + "px";
- this.veilMiddleBox.style.height = aRectScaled.height + "px";
- this.veilTransparentBox.style.width = aRectScaled.width + "px";
-
- this._highlighting = true;
- } else {
- this.unhighlight();
- }
-
- this._contentRect = aRect; // save orig (non-scaled) rect
- this._highlightRect = aRectScaled; // and save the scaled rect.
-
- return this._highlighting;
- },
-
- /**
- * Clear the highlighter surface.
- */
- unhighlight: function Highlighter_unhighlight()
- {
- this._highlighting = false;
- this.veilMiddleBox.style.height = 0;
- this.veilTransparentBox.style.width = 0;
- this.veilTransparentBox.style.visibility = "hidden";
- Services.obs.notifyObservers(null,
- INSPECTOR_NOTIFICATIONS.UNHIGHLIGHTING, null);
- },
-
- /**
- * Update node information (tagName#id.class)
- */
- updateInfobar: function Highlighter_updateInfobar()
- {
- // Tag name
- this.nodeInfo.tagNameLabel.textContent = this.node.tagName;
-
- // ID
- this.nodeInfo.idLabel.textContent = this.node.id ? "#" + this.node.id : "";
-
- // Classes
- let classes = this.nodeInfo.classesBox;
- while (classes.hasChildNodes()) {
- classes.removeChild(classes.firstChild);
- }
-
- if (this.node.className) {
- let fragment = this.chromeDoc.createDocumentFragment();
- for (let i = 0; i < this.node.classList.length; i++) {
- let classLabel = this.chromeDoc.createElement("label");
- classLabel.className = "highlighter-nodeinfobar-class plain";
- classLabel.textContent = "." + this.node.classList[i];
- fragment.appendChild(classLabel);
- }
- classes.appendChild(fragment);
- }
- },
-
- /**
- * Move the Infobar to the right place in the highlighter.
- */
- moveInfobar: function Highlighter_moveInfobar()
- {
- if (this._highlightRect) {
- let winHeight = this.win.innerHeight * this.zoom;
- let winWidth = this.win.innerWidth * this.zoom;
-
- let rect = {top: this._highlightRect.top,
- left: this._highlightRect.left,
- width: this._highlightRect.width,
- height: this._highlightRect.height};
-
- rect.top = Math.max(rect.top, 0);
- rect.left = Math.max(rect.left, 0);
- rect.width = Math.max(rect.width, 0);
- rect.height = Math.max(rect.height, 0);
-
- rect.top = Math.min(rect.top, winHeight);
- rect.left = Math.min(rect.left, winWidth);
-
- this.nodeInfo.container.removeAttribute("disabled");
- // Can the bar be above the node?
- if (rect.top < this.nodeInfo.barHeight) {
- // No. Can we move the toolbar under the node?
- if (rect.top + rect.height +
- this.nodeInfo.barHeight > winHeight) {
- // No. Let's move it inside.
- this.nodeInfo.container.style.top = rect.top + "px";
- this.nodeInfo.container.setAttribute("position", "overlap");
- } else {
- // Yes. Let's move it under the node.
- this.nodeInfo.container.style.top = rect.top + rect.height + "px";
- this.nodeInfo.container.setAttribute("position", "bottom");
- }
- } else {
- // Yes. Let's move it on top of the node.
- this.nodeInfo.container.style.top =
- rect.top - this.nodeInfo.barHeight + "px";
- this.nodeInfo.container.setAttribute("position", "top");
- }
-
- let barWidth = this.nodeInfo.container.getBoundingClientRect().width;
- let left = rect.left + rect.width / 2 - barWidth / 2;
-
- // Make sure the whole infobar is visible
- if (left < 0) {
- left = 0;
- this.nodeInfo.container.setAttribute("hide-arrow", "true");
- } else {
- if (left + barWidth > winWidth) {
- left = winWidth - barWidth;
- this.nodeInfo.container.setAttribute("hide-arrow", "true");
- } else {
- this.nodeInfo.container.removeAttribute("hide-arrow");
- }
- }
- this.nodeInfo.container.style.left = left + "px";
- } else {
- this.nodeInfo.container.style.left = "0";
- this.nodeInfo.container.style.top = "0";
- this.nodeInfo.container.setAttribute("position", "top");
- this.nodeInfo.container.setAttribute("hide-arrow", "true");
- }
- },
-
- /**
- * Return the midpoint of a line from pointA to pointB.
- *
- * @param object aPointA
- * An object with x and y properties.
- * @param object aPointB
- * An object with x and y properties.
- * @returns object
- * An object with x and y properties.
- */
- midPoint: function Highlighter_midPoint(aPointA, aPointB)
- {
- let pointC = { };
- pointC.x = (aPointB.x - aPointA.x) / 2 + aPointA.x;
- pointC.y = (aPointB.y - aPointA.y) / 2 + aPointA.y;
- return pointC;
- },
-
- /**
- * Return the node under the highlighter rectangle. Useful for testing.
- * Calculation based on midpoint of diagonal from top left to bottom right
- * of panel.
- *
- * @returns nsIDOMNode|null
- * Returns the node under the current highlighter rectangle. Null is
- * returned if there is no node highlighted.
- */
- get highlitNode()
- {
- // Not highlighting? Bail.
- if (!this._highlighting || !this._contentRect) {
- return null;
- }
-
- let a = {
- x: this._contentRect.left,
- y: this._contentRect.top
- };
-
- let b = {
- x: a.x + this._contentRect.width,
- y: a.y + this._contentRect.height
- };
-
- // Get midpoint of diagonal line.
- let midpoint = this.midPoint(a, b);
-
- return this.IUI.elementFromPoint(this.win.document, midpoint.x,
- midpoint.y);
- },
-
- /**
- * Is the specified node highlightable?
- *
- * @param nsIDOMNode aNode
- * the DOM element in question
- * @returns boolean
- * True if the node is highlightable or false otherwise.
- */
- isNodeHighlightable: function Highlighter_isNodeHighlightable(aNode)
- {
- if (aNode.nodeType != aNode.ELEMENT_NODE) {
- return false;
- }
- let nodeName = aNode.nodeName.toLowerCase();
- return !INSPECTOR_INVISIBLE_ELEMENTS[nodeName];
- },
-
- /**
- * Store page zoom factor.
- */
- computeZoomFactor: function Highlighter_computeZoomFactor() {
- this.zoom =
- this.win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
- .getInterface(Components.interfaces.nsIDOMWindowUtils)
- .screenPixelsPerCSSPixel;
- },
-
- /////////////////////////////////////////////////////////////////////////
- //// Event Handling
-
- attachInspectListeners: function Highlighter_attachInspectListeners()
- {
- this.browser.addEventListener("mousemove", this, true);
- this.browser.addEventListener("click", this, true);
- this.browser.addEventListener("dblclick", this, true);
- this.browser.addEventListener("mousedown", this, true);
- this.browser.addEventListener("mouseup", this, true);
- },
-
- detachInspectListeners: function Highlighter_detachInspectListeners()
- {
- this.browser.removeEventListener("mousemove", this, true);
- this.browser.removeEventListener("click", this, true);
- this.browser.removeEventListener("dblclick", this, true);
- this.browser.removeEventListener("mousedown", this, true);
- this.browser.removeEventListener("mouseup", this, true);
- },
-
-
- /**
- * Generic event handler.
- *
- * @param nsIDOMEvent aEvent
- * The DOM event object.
- */
- handleEvent: function Highlighter_handleEvent(aEvent)
- {
- switch (aEvent.type) {
- case "click":
- this.handleClick(aEvent);
- break;
- case "mousemove":
- this.handleMouseMove(aEvent);
- break;
- case "resize":
- this.computeZoomFactor();
- this.brieflyDisableTransitions();
- this.handleResize(aEvent);
- break;
- case "dblclick":
- case "mousedown":
- case "mouseup":
- aEvent.stopPropagation();
- aEvent.preventDefault();
- break;
- case "scroll":
- this.brieflyDisableTransitions();
- this.highlight();
- break;
- }
- },
-
- /**
- * Disable the CSS transitions for a short time to avoid laggy animations
- * during scrolling or resizing.
- */
- brieflyDisableTransitions: function Highlighter_brieflyDisableTransitions()
- {
- if (this.transitionDisabler) {
- this.IUI.win.clearTimeout(this.transitionDisabler);
- } else {
- this.veilContainer.setAttribute("disable-transitions", "true");
- this.nodeInfo.container.setAttribute("disable-transitions", "true");
- }
- this.transitionDisabler =
- this.IUI.win.setTimeout(function() {
- this.veilContainer.removeAttribute("disable-transitions");
- this.nodeInfo.container.removeAttribute("disable-transitions");
- this.transitionDisabler = null;
- }.bind(this), 500);
- },
-
- /**
- * Handle clicks.
- *
- * @param nsIDOMEvent aEvent
- * The DOM event.
- */
- handleClick: function Highlighter_handleClick(aEvent)
- {
- // Stop inspection when the user clicks on a node.
- if (aEvent.button == 0) {
- let win = aEvent.target.ownerDocument.defaultView;
- this.IUI.stopInspecting();
- win.focus();
- }
- aEvent.preventDefault();
- aEvent.stopPropagation();
- },
-
- /**
- * Handle mousemoves in panel when InspectorUI.inspecting is true.
- *
- * @param nsiDOMEvent aEvent
- * The MouseEvent triggering the method.
- */
- handleMouseMove: function Highlighter_handleMouseMove(aEvent)
- {
- let element = this.IUI.elementFromPoint(aEvent.target.ownerDocument,
- aEvent.clientX, aEvent.clientY);
- if (element && element != this.node) {
- this.IUI.inspectNode(element);
- }
- },
-
- /**
- * Handle window resize events.
- */
- handleResize: function Highlighter_handleResize()
- {
- this.highlight();
- },
-};
-
-///////////////////////////////////////////////////////////////////////////
//// InspectorUI
/**
* Main controller class for the Inspector.
*
* @constructor
* @param nsIDOMWindow aWindow
* The chrome window for which the Inspector instance is created.
@@ -970,18 +277,21 @@ InspectorUI.prototype = {
// initialize the HTML Breadcrumbs
this.breadcrumbs = new HTMLBreadcrumbs(this);
this.isDirty = false;
this.progressListener = new InspectorProgressListener(this);
+ this.chromeWin.addEventListener("keypress", this, false);
+
// initialize the highlighter
- this.initializeHighlighter();
+ this.highlighter = new Highlighter(this.chromeWin);
+ this.highlighterReady();
},
/**
* Register the Rule View in the Sidebar.
*/
registerRuleView: function IUI_registerRuleView()
{
let isOpen = this.isRuleViewOpen.bind(this);
@@ -1008,27 +318,16 @@ InspectorUI.prototype = {
* Register and initialize any included tools.
*/
initTools: function IUI_initTools()
{
// Extras go here.
},
/**
- * Initialize highlighter.
- */
- initializeHighlighter: function IUI_initializeHighlighter()
- {
- this.highlighter = new Highlighter(this);
- this.browser.addEventListener("keypress", this, true);
- this.highlighter.highlighterContainer.addEventListener("keypress", this, true);
- this.highlighterReady();
- },
-
- /**
* Initialize the InspectorStore.
*/
initializeStore: function IUI_initializeStore()
{
// First time opened, add the TabSelect listener
if (this.store.isEmpty()) {
this.tabbrowser.tabContainer.addEventListener("TabSelect", this, false);
}
@@ -1091,31 +390,29 @@ InspectorUI.prototype = {
this.store.setValue(this.winID, "inspecting", this.inspecting);
this.store.setValue(this.winID, "isDirty", this.isDirty);
}
if (this.store.isEmpty()) {
this.tabbrowser.tabContainer.removeEventListener("TabSelect", this, false);
}
+ this.chromeWin.removeEventListener("keypress", this, false);
+
this.stopInspecting();
- this.browser.removeEventListener("keypress", this, true);
this.saveToolState(this.winID);
this.toolsDo(function IUI_toolsHide(aTool) {
this.unregisterTool(aTool);
}.bind(this));
// close the sidebar
this.hideSidebar();
if (this.highlighter) {
- this.highlighter.highlighterContainer.removeEventListener("keypress",
- this,
- true);
this.highlighter.destroy();
this.highlighter = null;
}
if (this.breadcrumbs) {
this.breadcrumbs.destroy();
this.breadcrumbs = null;
}
@@ -1142,52 +439,44 @@ InspectorUI.prototype = {
startInspecting: function IUI_startInspecting()
{
// if currently editing an attribute value, starting
// "live inspection" mode closes the editor
if (this.treePanel && this.treePanel.editingContext)
this.treePanel.closeEditor();
this.inspectToolbutton.checked = true;
- this.highlighter.attachInspectListeners();
this.inspecting = true;
this.toolsDim(true);
- this.highlighter.veilContainer.removeAttribute("locked");
- this.highlighter.nodeInfo.container.removeAttribute("locked");
+ this.highlighter.unlock();
},
/**
* Stop inspecting webpage, detach page listeners, disable highlighter
* event listeners.
* @param aPreventScroll
* Prevent scroll in the HTML tree?
*/
stopInspecting: function IUI_stopInspecting(aPreventScroll)
{
if (!this.inspecting) {
return;
}
this.inspectToolbutton.checked = false;
- // Detach event listeners from content window and child windows to disable
- // highlighting. We still want to be notified if the user presses "ESCAPE"
- // to close the inspector, or "RETURN" to unlock the node, so we don't
- // remove the "keypress" event until the highlighter is removed.
- this.highlighter.detachInspectListeners();
this.inspecting = false;
this.toolsDim(false);
- if (this.highlighter.node) {
- this.select(this.highlighter.node, true, true, !aPreventScroll);
+ if (this.highlighter.getNode()) {
+ this.select(this.highlighter.getNode(), true, true, !aPreventScroll);
} else {
this.select(null, true, true);
}
- this.highlighter.veilContainer.setAttribute("locked", true);
- this.highlighter.nodeInfo.container.setAttribute("locked", true);
+ this.highlighter.lock();
},
/**
* Select an object in the tree view.
* @param aNode
* node to inspect
* @param forceUpdate
* force an update?
@@ -1202,17 +491,17 @@ InspectorUI.prototype = {
this.treePanel.closeEditor();
if (!aNode)
aNode = this.defaultSelection;
if (forceUpdate || aNode != this.selection) {
this.selection = aNode;
if (!this.inspecting) {
- this.highlighter.highlightNode(this.selection);
+ this.highlighter.highlight(this.selection);
}
}
this.breadcrumbs.update();
this.chromeWin.Tilt.update(aNode);
this.toolsSelect(aScroll);
},
@@ -1221,37 +510,53 @@ InspectorUI.prototype = {
* Called when the highlighted node is changed by a tool.
*
* @param object aUpdater
* The tool that triggered the update (if any), that tool's
* onChanged will not be called.
*/
nodeChanged: function IUI_nodeChanged(aUpdater)
{
- this.highlighter.highlight();
+ this.highlighter.invalidateSize();
this.toolsOnChanged(aUpdater);
},
/////////////////////////////////////////////////////////////////////////
//// Event Handling
highlighterReady: function IUI_highlighterReady()
{
// Setup the InspectorStore or restore state
this.initializeStore();
+ let self = this;
+
+ this.highlighter.addListener("locked", function() {
+ self.stopInspecting();
+ });
+
+ this.highlighter.addListener("unlocked", function() {
+ self.startInspecting();
+ });
+
+ this.highlighter.addListener("nodeselected", function() {
+ self.select(self.highlighter.getNode(), false, false);
+ });
+
if (this.store.getValue(this.winID, "inspecting")) {
this.startInspecting();
}
this.restoreToolState(this.winID);
this.win.focus();
Services.obs.notifyObservers({wrappedJSObject: this},
INSPECTOR_NOTIFICATIONS.OPENED, null);
+
+ this.highlighter.highlight();
},
/**
* Main callback handler for events.
*
* @param event
* The event to be handled.
*/
@@ -1308,84 +613,16 @@ InspectorUI.prototype = {
break;
case "keypress":
switch (event.keyCode) {
case this.chromeWin.KeyEvent.DOM_VK_ESCAPE:
this.closeInspectorUI(false);
event.preventDefault();
event.stopPropagation();
break;
- case this.chromeWin.KeyEvent.DOM_VK_RETURN:
- this.toggleInspection();
- event.preventDefault();
- event.stopPropagation();
- break;
- case this.chromeWin.KeyEvent.DOM_VK_LEFT:
- let node;
- if (this.selection) {
- node = this.selection.parentNode;
- } else {
- node = this.defaultSelection;
- }
- if (node && this.highlighter.isNodeHighlightable(node)) {
- this.inspectNode(node, true);
- }
- event.preventDefault();
- event.stopPropagation();
- break;
- case this.chromeWin.KeyEvent.DOM_VK_RIGHT:
- if (this.selection) {
- // Find the first child that is highlightable.
- for (let i = 0; i < this.selection.childNodes.length; i++) {
- node = this.selection.childNodes[i];
- if (node && this.highlighter.isNodeHighlightable(node)) {
- break;
- }
- }
- } else {
- node = this.defaultSelection;
- }
- if (node && this.highlighter.isNodeHighlightable(node)) {
- this.inspectNode(node, true);
- }
- event.preventDefault();
- event.stopPropagation();
- break;
- case this.chromeWin.KeyEvent.DOM_VK_UP:
- if (this.selection) {
- // Find a previous sibling that is highlightable.
- node = this.selection.previousSibling;
- while (node && !this.highlighter.isNodeHighlightable(node)) {
- node = node.previousSibling;
- }
- } else {
- node = this.defaultSelection;
- }
- if (node && this.highlighter.isNodeHighlightable(node)) {
- this.inspectNode(node, true);
- }
- event.preventDefault();
- event.stopPropagation();
- break;
- case this.chromeWin.KeyEvent.DOM_VK_DOWN:
- if (this.selection) {
- // Find a next sibling that is highlightable.
- node = this.selection.nextSibling;
- while (node && !this.highlighter.isNodeHighlightable(node)) {
- node = node.nextSibling;
- }
- } else {
- node = this.defaultSelection;
- }
- if (node && this.highlighter.isNodeHighlightable(node)) {
- this.inspectNode(node, true);
- }
- event.preventDefault();
- event.stopPropagation();
- break;
}
break;
}
},
/////////////////////////////////////////////////////////////////////////
//// CssRuleView methods
@@ -1498,88 +735,23 @@ InspectorUI.prototype = {
* @param aNode
* the element in the document to inspect
* @param aScroll
* force scroll?
*/
inspectNode: function IUI_inspectNode(aNode, aScroll)
{
this.select(aNode, true, true);
- this.highlighter.highlightNode(aNode, { scroll: aScroll });
- },
-
- /**
- * Find an element from the given coordinates. This method descends through
- * frames to find the element the user clicked inside frames.
- *
- * @param DOMDocument aDocument the document to look into.
- * @param integer aX
- * @param integer aY
- * @returns Node|null the element node found at the given coordinates.
- */
- elementFromPoint: function IUI_elementFromPoint(aDocument, aX, aY)
- {
- let node = aDocument.elementFromPoint(aX, aY);
- if (node && node.contentDocument) {
- if (node instanceof Ci.nsIDOMHTMLIFrameElement) {
- let rect = node.getBoundingClientRect();
-
- // Gap between the iframe and its content window.
- let [offsetTop, offsetLeft] = this.getIframeContentOffset(node);
-
- aX -= rect.left + offsetLeft;
- aY -= rect.top + offsetTop;
-
- if (aX < 0 || aY < 0) {
- // Didn't reach the content document, still over the iframe.
- return node;
- }
- }
- if (node instanceof Ci.nsIDOMHTMLIFrameElement ||
- node instanceof Ci.nsIDOMHTMLFrameElement) {
- let subnode = this.elementFromPoint(node.contentDocument, aX, aY);
- if (subnode) {
- node = subnode;
- }
- }
- }
- return node;
+ this.highlighter.highlight(aNode, aScroll);
},
///////////////////////////////////////////////////////////////////////////
//// Utility functions
/**
- * Returns iframe content offset (iframe border + padding).
- * Note: this function shouldn't need to exist, had the platform provided a
- * suitable API for determining the offset between the iframe's content and
- * its bounding client rect. Bug 626359 should provide us with such an API.
- *
- * @param aIframe
- * The iframe.
- * @returns array [offsetTop, offsetLeft]
- * offsetTop is the distance from the top of the iframe and the
- * top of the content document.
- * offsetLeft is the distance from the left of the iframe and the
- * left of the content document.
- */
- getIframeContentOffset: function IUI_getIframeContentOffset(aIframe)
- {
- let style = aIframe.contentWindow.getComputedStyle(aIframe, null);
-
- let paddingTop = parseInt(style.getPropertyValue("padding-top"));
- let paddingLeft = parseInt(style.getPropertyValue("padding-left"));
-
- let borderTop = parseInt(style.getPropertyValue("border-top-width"));
- let borderLeft = parseInt(style.getPropertyValue("border-left-width"));
-
- return [borderTop + paddingTop, borderLeft + paddingLeft];
- },
-
- /**
* Retrieve the unique ID of a window object.
*
* @param nsIDOMWindow aWindow
* @returns integer ID
*/
getWindowID: function IUI_getWindowID(aWindow)
{
if (!aWindow) {
@@ -1924,16 +1096,22 @@ InspectorUI.prototype = {
}
}.bind(this));
this.sidebarTools.forEach(function(tool) {
if (tool != activeSidebarTool)
this.chromeDoc.getElementById(
this.getToolbarButtonId(tool.id)).removeAttribute("checked");
}.bind(this));
}
+ if (this.store.getValue(this.winID, "inspecting")) {
+ this.highlighter.unlock();
+ } else {
+ this.highlighter.lock();
+ }
+
Services.obs.notifyObservers(null, INSPECTOR_NOTIFICATIONS.STATE_RESTORED, null);
},
/**
* For each tool in the tools collection select the current node that is
* selected in the highlighter
* @param aScroll boolean
* Do you want to scroll the treepanel?
--- a/browser/devtools/highlighter/test/Makefile.in
+++ b/browser/devtools/highlighter/test/Makefile.in
@@ -61,18 +61,19 @@ include $(topsrcdir)/config/rules.mk
browser_inspector_bug_566084_location_changed.js \
browser_inspector_infobar.js \
browser_inspector_bug_690361.js \
browser_inspector_bug_672902_keyboard_shortcuts.js \
browser_inspector_keybindings.js \
browser_inspector_breadcrumbs.html \
browser_inspector_breadcrumbs.js \
browser_inspector_bug_699308_iframe_navigation.js \
- browser_inspector_changes.js \
- browser_inspector_ruleviewstore.js \
- browser_inspector_duplicate_ruleview.js \
+ browser_inspector_changes.js \
+ browser_inspector_ruleviewstore.js \
+ browser_inspector_duplicate_ruleview.js \
+ head.js \
$(NULL)
# Disabled due to constant failures
# browser_inspector_treePanel_click.js \
libs:: $(_BROWSER_FILES)
$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
--- a/browser/devtools/highlighter/test/browser_inspector_breadcrumbs.js
+++ b/browser/devtools/highlighter/test/browser_inspector_breadcrumbs.js
@@ -43,32 +43,29 @@ function test()
function runTests()
{
Services.obs.removeObserver(runTests,
InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
cursor = 0;
executeSoon(function() {
- Services.obs.addObserver(nodeSelected,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
-
+ InspectorUI.highlighter.addListener("nodeselected", nodeSelected);
InspectorUI.inspectNode(nodes[0].node);
});
}
function nodeSelected()
{
executeSoon(function() {
performTest();
cursor++;
if (cursor >= nodes.length) {
- Services.obs.removeObserver(nodeSelected,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
+ InspectorUI.highlighter.removeListener("nodeselected", nodeSelected);
Services.obs.addObserver(finishUp,
InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
executeSoon(function() {
InspectorUI.closeInspectorUI();
});
} else {
let node = nodes[cursor].node;
--- a/browser/devtools/highlighter/test/browser_inspector_bug_665880.js
+++ b/browser/devtools/highlighter/test/browser_inspector_bug_665880.js
@@ -29,27 +29,25 @@ function test()
}
function runObjectInspectionTest()
{
Services.obs.removeObserver(runObjectInspectionTest,
InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
executeSoon(function() {
- Services.obs.addObserver(performTestComparison,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+ InspectorUI.highlighter.addListener("nodeselected", performTestComparison);
InspectorUI.inspectNode(objectNode);
});
}
function performTestComparison()
{
- Services.obs.removeObserver(performTestComparison,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
+ InspectorUI.highlighter.removeListener("nodeselected", performTestComparison);
is(InspectorUI.selection, objectNode, "selection matches node");
Services.obs.addObserver(finishUp,
InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
InspectorUI.closeInspectorUI();
}
--- a/browser/devtools/highlighter/test/browser_inspector_bug_672902_keyboard_shortcuts.js
+++ b/browser/devtools/highlighter/test/browser_inspector_bug_672902_keyboard_shortcuts.js
@@ -33,94 +33,79 @@ function test()
}
function findAndHighlightNode()
{
Services.obs.removeObserver(findAndHighlightNode,
InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
executeSoon(function() {
- Services.obs.addObserver(highlightBodyNode,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING,
- false);
+ InspectorUI.highlighter.addListener("nodeselected", highlightBodyNode);
// Test that navigating around without a selected node gets us to the
// body element.
node = doc.querySelector("body");
EventUtils.synthesizeKey("VK_RIGHT", { });
});
}
function highlightBodyNode()
{
- Services.obs.removeObserver(highlightBodyNode,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
+ InspectorUI.highlighter.removeListener("nodeselected", highlightBodyNode);
is(InspectorUI.selection, node, "selected body element");
executeSoon(function() {
- Services.obs.addObserver(highlightHeaderNode,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING,
- false);
+ InspectorUI.highlighter.addListener("nodeselected", highlightHeaderNode);
// Test that moving to the child works.
node = doc.querySelector("h1");
EventUtils.synthesizeKey("VK_RIGHT", { });
});
}
function highlightHeaderNode()
{
- Services.obs.removeObserver(highlightHeaderNode,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
+ InspectorUI.highlighter.removeListener("nodeselected", highlightHeaderNode);
is(InspectorUI.selection, node, "selected h1 element");
executeSoon(function() {
- Services.obs.addObserver(highlightParagraphNode,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING,
- false);
+ InspectorUI.highlighter.addListener("nodeselected", highlightParagraphNode);
// Test that moving to the next sibling works.
node = doc.querySelector("p");
EventUtils.synthesizeKey("VK_DOWN", { });
});
}
function highlightParagraphNode()
{
- Services.obs.removeObserver(highlightParagraphNode,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
+ InspectorUI.highlighter.removeListener("nodeselected", highlightParagraphNode);
is(InspectorUI.selection, node, "selected p element");
executeSoon(function() {
- Services.obs.addObserver(highlightHeaderNodeAgain,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING,
- false);
+ InspectorUI.highlighter.addListener("nodeselected", highlightHeaderNodeAgain);
// Test that moving to the previous sibling works.
node = doc.querySelector("h1");
EventUtils.synthesizeKey("VK_UP", { });
});
}
function highlightHeaderNodeAgain()
{
- Services.obs.removeObserver(highlightHeaderNodeAgain,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
+ InspectorUI.highlighter.removeListener("nodeselected", highlightHeaderNodeAgain);
is(InspectorUI.selection, node, "selected h1 element");
executeSoon(function() {
- Services.obs.addObserver(highlightParentNode,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING,
- false);
+ InspectorUI.highlighter.addListener("nodeselected", highlightParentNode);
// Test that moving to the parent works.
node = doc.querySelector("body");
EventUtils.synthesizeKey("VK_LEFT", { });
});
}
function highlightParentNode()
{
- Services.obs.removeObserver(highlightParentNode,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
+ InspectorUI.highlighter.removeListener("nodeselected", highlightParentNode);
is(InspectorUI.selection, node, "selected body element");
// Test that locking works.
EventUtils.synthesizeKey("VK_RETURN", { });
executeSoon(isTheNodeLocked);
}
function isTheNodeLocked()
--- a/browser/devtools/highlighter/test/browser_inspector_bug_674871.js
+++ b/browser/devtools/highlighter/test/browser_inspector_bug_674871.js
@@ -52,45 +52,39 @@ function test()
}
function runTests()
{
Services.obs.removeObserver(runTests,
InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
executeSoon(function() {
- Services.obs.addObserver(isTheIframeSelected,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+ InspectorUI.highlighter.addListener("nodeselected", isTheIframeSelected);
moveMouseOver(iframeNode, 1, 1);
});
}
function isTheIframeSelected()
{
- Services.obs.removeObserver(isTheIframeSelected,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
+ InspectorUI.highlighter.removeListener("nodeselected", isTheIframeSelected);
is(InspectorUI.selection, iframeNode, "selection matches node");
iframeNode.style.marginBottom = doc.defaultView.innerHeight + "px";
doc.defaultView.scrollBy(0, 40);
executeSoon(function() {
- Services.obs.addObserver(isTheIframeContentSelected,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
-
+ InspectorUI.highlighter.addListener("nodeselected", isTheIframeContentSelected);
moveMouseOver(iframeNode, 40, 40);
});
}
function isTheIframeContentSelected()
{
- Services.obs.removeObserver(isTheIframeContentSelected,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
-
+ InspectorUI.highlighter.removeListener("nodeselected", isTheIframeContentSelected);
is(InspectorUI.selection, iframeBodyNode, "selection matches node");
// 184 == 200 + 11(border) + 13(padding) - 40(scroll)
is(InspectorUI.highlighter._highlightRect.height, 184,
"highlighter height");
Services.obs.addObserver(finishUp,
InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
InspectorUI.closeInspectorUI();
--- a/browser/devtools/highlighter/test/browser_inspector_editor.js
+++ b/browser/devtools/highlighter/test/browser_inspector_editor.js
@@ -53,17 +53,17 @@ function runEditorTests()
// start the tests
doNextStep();
}
function highlighterTrap()
{
// bug 696107
- Services.obs.removeObserver(highlighterTrap, InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
+ InspectorUI.highlighter.removeListener("nodeselected", highlighterTrap);
ok(false, "Highlighter moved. Shouldn't be here!");
finishUp();
}
function doEditorTestSteps()
{
let treePanel = InspectorUI.treePanel;
let editor = treePanel.treeBrowserDocument.getElementById("attribute-editor");
@@ -110,32 +110,31 @@ function doEditorTestSteps()
is(treePanel.editingContext.repObj, div, "editor session has correct reference to div");
is(treePanel.editingContext.attrObj, attrValNode_id, "editor session has correct reference to `id` attribute-value node in HTML panel");
is(treePanel.editingContext.attrName, "id", "editor session knows correct attribute-name");
editorInput.value = "Hello World";
editorInput.focus();
- Services.obs.addObserver(highlighterTrap,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+ InspectorUI.highlighter.addListener("nodeselected", highlighterTrap);
// hit <enter> to save the textbox value
executeSoon(function() {
// Extra key to test that keyboard handlers have been removed. bug 696107.
EventUtils.synthesizeKey("VK_LEFT", {}, attrValNode_id.ownerDocument.defaultView);
EventUtils.synthesizeKey("VK_RETURN", {}, attrValNode_id.ownerDocument.defaultView);
});
// two `yield` statements, to trap both the "SAVED" and "CLOSED" events that will be triggered
yield;
yield; // End of Step 2
// remove this from previous step
- Services.obs.removeObserver(highlighterTrap, InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
+ InspectorUI.highlighter.removeListener("nodeselected", highlighterTrap);
// Step 3: validate that the previous editing session saved correctly, then open editor on `class` attribute value
ok(!treePanel.editingContext, "Step 3: editor session ended");
editorVisible = editor.classList.contains("editing");
ok(!editorVisible, "editor popup hidden");
attrValNodeHighlighted = attrValNode_id.classList.contains("editingAttributeValue");
ok(!attrValNodeHighlighted, "`id` attribute-value node is no longer editor-highlighted");
is(div.getAttribute("id"), "Hello World", "`id` attribute-value successfully updated");
--- a/browser/devtools/highlighter/test/browser_inspector_highlighter.js
+++ b/browser/devtools/highlighter/test/browser_inspector_highlighter.js
@@ -98,47 +98,43 @@ function runSelectionTests(subject)
{
Services.obs.removeObserver(runSelectionTests,
InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
is(subject.wrappedJSObject, InspectorUI,
"InspectorUI accessible in the observer");
executeSoon(function() {
- Services.obs.addObserver(performTestComparisons,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+ InspectorUI.highlighter.addListener("nodeselected", performTestComparisons);
EventUtils.synthesizeMouse(h1, 2, 2, {type: "mousemove"}, content);
});
}
function performTestComparisons(evt)
{
- Services.obs.removeObserver(performTestComparisons,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
+ InspectorUI.highlighter.removeListener("nodeselected", performTestComparisons);
InspectorUI.stopInspecting();
- ok(InspectorUI.highlighter.isHighlighting, "highlighter is highlighting");
- is(InspectorUI.highlighter.highlitNode, h1, "highlighter matches selection")
+ ok(isHighlighting(), "highlighter is highlighting");
+ is(getHighlitNode(), h1, "highlighter matches selection")
is(InspectorUI.selection, h1, "selection matches node");
- is(InspectorUI.selection, InspectorUI.highlighter.highlitNode, "selection matches highlighter");
+ is(InspectorUI.selection, getHighlitNode(), "selection matches highlighter");
div = doc.querySelector("div#checkOutThisWickedSpread");
executeSoon(function() {
- Services.obs.addObserver(finishTestComparisons,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+ InspectorUI.highlighter.addListener("nodeselected", finishTestComparisons);
InspectorUI.inspectNode(div);
});
}
function finishTestComparisons()
{
- Services.obs.removeObserver(finishTestComparisons,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
+ InspectorUI.highlighter.removeListener("nodeselected", finishTestComparisons);
// get dimensions of div element
let divDims = div.getBoundingClientRect();
let divWidth = divDims.width;
let divHeight = divDims.height;
// get dimensions of transparent veil box over element
let veilBoxDims =
--- a/browser/devtools/highlighter/test/browser_inspector_iframeTest.js
+++ b/browser/devtools/highlighter/test/browser_inspector_iframeTest.js
@@ -90,44 +90,42 @@ function setupIframeTests()
InspectorUI.openInspectorUI();
}
function runIframeTests()
{
Services.obs.removeObserver(runIframeTests,
InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
- Services.obs.addObserver(performTestComparisons1,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
- executeSoon(moveMouseOver.bind(this, div1));
+ executeSoon(function() {
+ InspectorUI.highlighter.addListener("nodeselected", performTestComparisons1);
+ moveMouseOver(div1)
+ });
}
function performTestComparisons1()
{
- Services.obs.removeObserver(performTestComparisons1,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+ InspectorUI.highlighter.removeListener("nodeselected", performTestComparisons1);
is(InspectorUI.selection, div1, "selection matches div1 node");
- is(InspectorUI.highlighter.highlitNode, div1, "highlighter matches selection");
+ is(getHighlitNode(), div1, "highlighter matches selection");
executeSoon(function() {
- Services.obs.addObserver(performTestComparisons2,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+ InspectorUI.highlighter.addListener("nodeselected", performTestComparisons2);
moveMouseOver(div2);
});
}
function performTestComparisons2()
{
- Services.obs.removeObserver(performTestComparisons2,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+ InspectorUI.highlighter.removeListener("nodeselected", performTestComparisons2);
is(InspectorUI.selection, div2, "selection matches div2 node");
- is(InspectorUI.highlighter.highlitNode, div2, "highlighter matches selection");
+ is(getHighlitNode(), div2, "highlighter matches selection");
finish();
}
function test() {
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
--- a/browser/devtools/highlighter/test/browser_inspector_initialization.js
+++ b/browser/devtools/highlighter/test/browser_inspector_initialization.js
@@ -167,29 +167,29 @@ function openInspectorForContextTest()
InspectorUI.openInspectorUI(salutation);
});
}
function inspectNodesFromContextTestWhileOpen()
{
Services.obs.removeObserver(inspectNodesFromContextTestWhileOpen, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
Services.obs.addObserver(inspectNodesFromContextTestTrap, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
- Services.obs.addObserver(inspectNodesFromContextTestHighlight, InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+ InspectorUI.highlighter.addListener("nodeselected", inspectNodesFromContextTestHighlight);
is(InspectorUI.selection, salutation, "Inspector is highlighting salutation");
closing = doc.getElementById("closing");
ok(closing, "we have the closing statement");
executeSoon(function() {
InspectorUI.openInspectorUI(closing);
});
}
function inspectNodesFromContextTestHighlight()
{
winId = InspectorUI.winID;
- Services.obs.removeObserver(inspectNodesFromContextTestHighlight, InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
+ InspectorUI.highlighter.removeListener("nodeselected", inspectNodesFromContextTestHighlight);
Services.obs.addObserver(finishInspectorTests, InspectorUI.INSPECTOR_NOTIFICATIONS.DESTROYED, false);
is(InspectorUI.selection, closing, "InspectorUI.selection is header");
executeSoon(function() {
InspectorUI.closeInspectorUI();
});
}
function inspectNodesFromContextTestTrap()
--- a/browser/devtools/highlighter/test/browser_inspector_keybindings.js
+++ b/browser/devtools/highlighter/test/browser_inspector_keybindings.js
@@ -27,28 +27,24 @@ function test()
}
function highlightNode()
{
Services.obs.removeObserver(highlightNode,
InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
executeSoon(function() {
- Services.obs.addObserver(lockNode,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
-
+ InspectorUI.highlighter.addListener("nodeselected", lockNode);
InspectorUI.inspectNode(node);
});
}
function lockNode()
{
- Services.obs.removeObserver(lockNode,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
-
+ InspectorUI.highlighter.removeListener("nodeselected", lockNode);
EventUtils.synthesizeKey("VK_RETURN", { });
executeSoon(isTheNodeLocked);
}
function isTheNodeLocked()
{
is(InspectorUI.selection, node, "selection matches node");
--- a/browser/devtools/highlighter/test/browser_inspector_registertools.js
+++ b/browser/devtools/highlighter/test/browser_inspector_registertools.js
@@ -91,23 +91,23 @@ function setupHighlighterTests()
function inspectorOpen()
{
info("we received the inspector-opened notification");
Services.obs.removeObserver(inspectorOpen, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
toolsLength = InspectorUI.tools.length;
toolEvents = InspectorUI.toolEvents.length;
info("tools registered");
- Services.obs.addObserver(startToolTests, InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+ InspectorUI.highlighter.addListener("nodeselected", startToolTests);
InspectorUI.inspectNode(h1);
}
function startToolTests(evt)
{
- Services.obs.removeObserver(startToolTests, InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
+ InspectorUI.highlighter.removeListener("nodeselected", startToolTests);
InspectorUI.stopInspecting();
info("Getting InspectorUI.tools");
let tools = InspectorUI.tools;
tool1 = InspectorUI.tools["tool_1"];
tool2 = InspectorUI.tools["tool_2"];
tool3 = InspectorUI.tools["tool_3"];
--- a/browser/devtools/highlighter/test/browser_inspector_scrolling.js
+++ b/browser/devtools/highlighter/test/browser_inspector_scrolling.js
@@ -68,32 +68,33 @@ function toggleInspector()
Services.obs.addObserver(inspectNode, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
InspectorUI.toggleInspectorUI();
}
function inspectNode()
{
Services.obs.removeObserver(inspectNode,
InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
- Services.obs.addObserver(performScrollingTest,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+
+ InspectorUI.highlighter.addListener("nodeselected", performScrollingTest);
executeSoon(function() {
InspectorUI.inspectNode(div);
});
}
function performScrollingTest()
{
- Services.obs.removeObserver(performScrollingTest,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+ InspectorUI.highlighter.removeListener("nodeselected", performScrollingTest);
- EventUtils.synthesizeMouseScroll(div, 10, 10,
- {axis:"vertical", delta:50, type:"MozMousePixelScroll"},
- iframe.contentWindow);
+ executeSoon(function() {
+ EventUtils.synthesizeMouseScroll(div, 10, 10,
+ {axis:"vertical", delta:50, type:"MozMousePixelScroll"},
+ iframe.contentWindow);
+ });
gBrowser.selectedBrowser.addEventListener("scroll", function() {
gBrowser.selectedBrowser.removeEventListener("scroll", arguments.callee,
false);
is(iframe.contentDocument.body.scrollTop, 50, "inspected iframe scrolled");
div = iframe = doc = null;
--- a/browser/devtools/highlighter/test/browser_inspector_treePanel_click.js
+++ b/browser/devtools/highlighter/test/browser_inspector_treePanel_click.js
@@ -29,34 +29,31 @@ function test() {
function runTests() {
Services.obs.removeObserver(runTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
Services.obs.addObserver(testNode1, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false);
InspectorUI.select(node1, true, true, true);
InspectorUI.openTreePanel();
}
function testNode1() {
- dump("testNode1\n");
Services.obs.removeObserver(testNode1, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY);
is(InspectorUI.selection, node1, "selection matches node");
- is(InspectorUI.highlighter.node, node1, "selection matches node");
+ is(getHighlitNode(), node1, "selection matches node");
testNode2();
}
function testNode2() {
- dump("testNode2\n")
- Services.obs.addObserver(testHighlightingNode2, InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+ InspectorUI.highlighter.addListener("nodeselected", testHighlightingNode2);
InspectorUI.treePanelSelect("node2");
}
function testHighlightingNode2() {
- dump("testHighlightingNode2\n")
- Services.obs.removeObserver(testHighlightingNode2, InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING);
+ InspectorUI.highlighter.removeListener("nodeselected", testHighlightingNode2);
is(InspectorUI.selection, node2, "selection matches node");
- is(InspectorUI.highlighter.node, node2, "selection matches node");
+ is(getHighlitNode(), node2, "selection matches node");
Services.obs.addObserver(finishUp, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
InspectorUI.closeInspectorUI();
}
function finishUp() {
Services.obs.removeObserver(finishUp, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED);
doc = node1 = node2 = null;
gBrowser.removeCurrentTab();
--- a/browser/devtools/highlighter/test/browser_inspector_treeSelection.js
+++ b/browser/devtools/highlighter/test/browser_inspector_treeSelection.js
@@ -71,31 +71,30 @@ function setupSelectionTests()
InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
InspectorUI.openInspectorUI();
}
function runSelectionTests()
{
Services.obs.removeObserver(runSelectionTests,
InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
- Services.obs.addObserver(performTestComparisons,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+
executeSoon(function() {
+ InspectorUI.highlighter.addListener("nodeselected", performTestComparisons);
InspectorUI.inspectNode(h1);
});
}
function performTestComparisons(evt)
{
- Services.obs.removeObserver(performTestComparisons,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+ InspectorUI.highlighter.removeListener("nodeselected", performTestComparisons);
is(h1, InspectorUI.selection, "selection matches node");
- ok(InspectorUI.highlighter.isHighlighting, "highlighter is highlighting");
- is(InspectorUI.highlighter.highlitNode, h1, "highlighter highlighting correct node");
+ ok(isHighlighting(), "highlighter is highlighting");
+ is(getHighlitNode(), h1, "highlighter highlighting correct node");
finishUp();
}
function finishUp() {
InspectorUI.closeInspectorUI();
doc = h1 = null;
gBrowser.removeCurrentTab();
new file mode 100644
--- /dev/null
+++ b/browser/devtools/highlighter/test/head.js
@@ -0,0 +1,78 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is DevTools test code.
+ *
+ * The Initial Developer of the Original Code is
+ * The Mozilla Foundation.
+ *
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Paul Rouget <paul@mozilla.com> (Original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const Cu = Components.utils;
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+
+function isHighlighting()
+{
+ let veil = InspectorUI.highlighter.veilTransparentBox;
+ return !(veil.style.visibility == "hidden");
+}
+
+function getHighlitNode()
+{
+ let h = InspectorUI.highlighter;
+ if (!isHighlighting() || !h._contentRect)
+ return null;
+
+ let a = {
+ x: h._contentRect.left,
+ y: h._contentRect.top
+ };
+
+ let b = {
+ x: a.x + h._contentRect.width,
+ y: a.y + h._contentRect.height
+ };
+
+ // Get midpoint of diagonal line.
+ let midpoint = midPoint(a, b);
+
+ return LayoutHelpers.getElementFromPoint(h.win.document, midpoint.x,
+ midpoint.y);
+}
+
+
+function midPoint(aPointA, aPointB)
+{
+ let pointC = { };
+ pointC.x = (aPointB.x - aPointA.x) / 2 + aPointA.x;
+ pointC.y = (aPointB.y - aPointA.y) / 2 + aPointA.y;
+ return pointC;
+}
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -7,9 +7,10 @@ browser.jar:
content/browser/splitview.css (styleeditor/splitview.css)
content/browser/styleeditor.css (styleeditor/styleeditor.css)
content/browser/devtools/csshtmltree.xul (styleinspector/csshtmltree.xul)
content/browser/devtools/cssruleview.xul (styleinspector/cssruleview.xul)
content/browser/devtools/styleinspector.css (styleinspector/styleinspector.css)
content/browser/orion.js (sourceeditor/orion/orion.js)
content/browser/orion.css (sourceeditor/orion/orion.css)
content/browser/orion-mozilla.css (sourceeditor/orion/mozilla.css)
+ content/browser/source-editor-overlay.xul (sourceeditor/source-editor-overlay.xul)
--- a/browser/devtools/scratchpad/scratchpad.xul
+++ b/browser/devtools/scratchpad/scratchpad.xul
@@ -40,29 +40,31 @@
- ***** END LICENSE BLOCK ***** -->
#endif
<!DOCTYPE window [
<!ENTITY % scratchpadDTD SYSTEM "chrome://browser/locale/devtools/scratchpad.dtd" >
%scratchpadDTD;
]>
<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<?xul-overlay href="chrome://browser/content/source-editor-overlay.xul"?>
<window id="main-window"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="&window.title;"
windowtype="devtools:scratchpad"
screenX="4" screenY="4"
width="640" height="480"
persist="screenX screenY width height sizemode">
<script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
<script type="application/javascript" src="chrome://browser/content/scratchpad.js"/>
<commandset id="editMenuCommands"/>
+<commandset id="sourceEditorCommands"/>
<commandset id="sp-commandset">
<command id="sp-cmd-newWindow" oncommand="Scratchpad.openScratchpad();"/>
<command id="sp-cmd-openFile" oncommand="Scratchpad.openFile();"/>
<command id="sp-cmd-save" oncommand="Scratchpad.saveFile();"/>
<command id="sp-cmd-saveas" oncommand="Scratchpad.saveFileAs();"/>
<!-- TODO: bug 650340 - implement printFile()
@@ -137,16 +139,42 @@
<key id="sp-key-errorConsole"
key="&errorConsoleCmd.commandkey;"
command="sp-cmd-errorConsole"
modifiers="accel,shift"/>
<key id="sp-key-webConsole"
key="&webConsoleCmd.commandkey;"
command="sp-cmd-webConsole"
modifiers="accel,shift"/>
+ <key id="key_find"
+ key="&findCmd.key;"
+ command="cmd_find"
+ modifiers="accel"/>
+#ifdef XP_MACOSX
+ <key id="key_findAgain"
+ key="&findAgainCmd.key;"
+ command="cmd_findAgain"
+ modifiers="accel"/>
+ <key id="key_findPrevious"
+ key="&findPreviousCmd.key;"
+ command="cmd_findPrevious"
+ modifiers="accel,shift"/>
+#else
+ <key id="key_findAgain"
+ keycode="VK_F3"
+ command="cmd_findAgain"/>
+ <key id="key_findPrevious"
+ keycode="VK_F3"
+ command="cmd_findPrevious"
+ modifiers="shift"/>
+#endif
+ <key id="key_gotoLine"
+ key="&gotoLineCmd.key;"
+ command="cmd_gotoLine"
+ modifiers="accel"/>
</keyset>
<menubar id="sp-menubar">
<menu id="sp-file-menu" label="&fileMenu.label;"
accesskey="&fileMenu.accesskey;">
<menupopup id="sp-menu-filepopup">
<menuitem id="sp-menu-newscratchpad"
@@ -218,33 +246,33 @@
accesskey="&pasteCmd.accesskey;"
command="cmd_paste"/>
<menuseparator/>
<menuitem id="sp-menu-selectAll"
label="&selectAllCmd.label;"
key="key_selectAll"
accesskey="&selectAllCmd.accesskey;"
command="cmd_selectAll"/>
-
- <!-- TODO: bug 650345 - implement search and replace
+ <menuseparator/>
<menuitem id="sp-menu-find"
- label="&findOnCmd.label;"
- accesskey="&findOnCmd.accesskey;"
+ label="&findCmd.label;"
+ accesskey="&findCmd.accesskey;"
key="key_find"
- disabled="true"
command="cmd_find"/>
<menuitem id="sp-menu-findAgain"
label="&findAgainCmd.label;"
accesskey="&findAgainCmd.accesskey;"
key="key_findAgain"
- disabled="true"
command="cmd_findAgain"/>
- <menuseparator id="sp-execute-separator"/>
- -->
-
+ <menuseparator/>
+ <menuitem id="sp-menu-gotoLine"
+ label="&gotoLineCmd.label;"
+ accesskey="&gotoLineCmd.accesskey;"
+ key="key_gotoLine"
+ command="cmd_gotoLine"/>
</menupopup>
</menu>
<menu id="sp-execute-menu" label="&executeMenu.label;"
accesskey="&executeMenu.accesskey;">
<menupopup id="sp-menu_executepopup">
<menuitem id="sp-text-run"
label="&run.label;"
--- a/browser/devtools/scratchpad/test/Makefile.in
+++ b/browser/devtools/scratchpad/test/Makefile.in
@@ -53,15 +53,17 @@ include $(topsrcdir)/config/rules.mk
browser_scratchpad_ui.js \
browser_scratchpad_bug_646070_chrome_context_pref.js \
browser_scratchpad_bug_660560_tab.js \
browser_scratchpad_open.js \
browser_scratchpad_restore.js \
browser_scratchpad_bug_679467_falsy.js \
browser_scratchpad_bug_699130_edit_ui_updates.js \
browser_scratchpad_bug_669612_unsaved.js \
- head.js \
browser_scratchpad_bug_653427_confirm_close.js \
browser_scratchpad_bug684546_reset_undo.js \
browser_scratchpad_bug690552_display_outputs_errors.js \
+ browser_scratchpad_bug650345_find_ui.js \
+ browser_scratchpad_bug714942_goto_line_ui.js \
+ head.js \
libs:: $(_BROWSER_TEST_FILES)
$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug650345_find_ui.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function browserLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", browserLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,<p>test the Find feature in Scratchpad";
+}
+
+function runTests(aWindow, aScratchpad)
+{
+ let editor = aScratchpad.editor;
+ let text = "foobar bug650345\nBug650345 bazbaz\nfoobar omg\ntest";
+ editor.setText(text);
+
+ let needle = "foobar";
+ editor.setSelection(0, needle.length);
+
+ let oldPrompt = Services.prompt;
+ Services.prompt = {
+ prompt: function() { return true; },
+ };
+
+ let findKey = "F";
+ info("test Ctrl/Cmd-" + findKey + " (find)");
+ EventUtils.synthesizeKey(findKey, {accelKey: true}, aWindow);
+ let selection = editor.getSelection();
+ let newIndex = text.indexOf(needle, needle.length);
+ is(selection.start, newIndex, "selection.start is correct");
+ is(selection.end, newIndex + needle.length, "selection.end is correct");
+
+ info("test cmd_find");
+ aWindow.goDoCommand("cmd_find");
+ selection = editor.getSelection();
+ is(selection.start, 0, "selection.start is correct");
+ is(selection.end, needle.length, "selection.end is correct");
+
+ let findNextKey = Services.appinfo.OS == "Darwin" ? "G" : "VK_F3";
+ let findNextKeyOptions = Services.appinfo.OS == "Darwin" ?
+ {accelKey: true} : {};
+
+ info("test " + findNextKey + " (findNext)");
+ EventUtils.synthesizeKey(findNextKey, findNextKeyOptions, aWindow);
+ selection = editor.getSelection();
+ is(selection.start, newIndex, "selection.start is correct");
+ is(selection.end, newIndex + needle.length, "selection.end is correct");
+
+ info("test cmd_findAgain");
+ aWindow.goDoCommand("cmd_findAgain");
+ selection = editor.getSelection();
+ is(selection.start, 0, "selection.start is correct");
+ is(selection.end, needle.length, "selection.end is correct");
+
+ let findPreviousKey = Services.appinfo.OS == "Darwin" ? "G" : "VK_F3";
+ let findPreviousKeyOptions = Services.appinfo.OS == "Darwin" ?
+ {accelKey: true, shiftKey: true} : {shiftKey: true};
+
+ info("test " + findPreviousKey + " (findPrevious)");
+ EventUtils.synthesizeKey(findPreviousKey, findPreviousKeyOptions, aWindow);
+ selection = editor.getSelection();
+ is(selection.start, newIndex, "selection.start is correct");
+ is(selection.end, newIndex + needle.length, "selection.end is correct");
+
+ info("test cmd_findPrevious");
+ aWindow.goDoCommand("cmd_findPrevious");
+ selection = editor.getSelection();
+ is(selection.start, 0, "selection.start is correct");
+ is(selection.end, needle.length, "selection.end is correct");
+
+ needle = "BAZbaz";
+ newIndex = text.toLowerCase().indexOf(needle.toLowerCase());
+
+ Services.prompt = {
+ prompt: function(aWindow, aTitle, aMessage, aValue) {
+ aValue.value = needle;
+ return true;
+ },
+ };
+
+ info("test Ctrl/Cmd-" + findKey + " (find) with a custom value");
+ EventUtils.synthesizeKey(findKey, {accelKey: true}, aWindow);
+ selection = editor.getSelection();
+ is(selection.start, newIndex, "selection.start is correct");
+ is(selection.end, newIndex + needle.length, "selection.end is correct");
+
+ Services.prompt = oldPrompt;
+
+ finish();
+}
+
new file mode 100644
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug714942_goto_line_ui.js
@@ -0,0 +1,45 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.selectedBrowser.addEventListener("load", function browserLoad() {
+ gBrowser.selectedBrowser.removeEventListener("load", browserLoad, true);
+ openScratchpad(runTests);
+ }, true);
+
+ content.location = "data:text/html,<p>test the 'Jump to line' feature in Scratchpad";
+}
+
+function runTests(aWindow, aScratchpad)
+{
+ let editor = aScratchpad.editor;
+ let text = "foobar bug650345\nBug650345 bazbaz\nfoobar omg\ntest";
+ editor.setText(text);
+ editor.setCaretOffset(0);
+
+ let oldPrompt = Services.prompt;
+ let desiredValue = null;
+ Services.prompt = {
+ prompt: function(aWindow, aTitle, aMessage, aValue) {
+ aValue.value = desiredValue;
+ return true;
+ },
+ };
+
+ desiredValue = 3;
+ EventUtils.synthesizeKey("J", {accelKey: true}, aWindow);
+ is(editor.getCaretOffset(), 34, "caret offset is correct");
+
+ desiredValue = 2;
+ aWindow.goDoCommand("cmd_gotoLine")
+ is(editor.getCaretOffset(), 17, "caret offset is correct (again)");
+
+ Services.prompt = oldPrompt;
+
+ finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/LayoutHelpers.jsm
@@ -0,0 +1,204 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Mozilla LayoutHelpers Module.
+ *
+ * The Initial Developer of the Original Code is
+ * The Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Rob Campbell <rcampbell@mozilla.com> (original author)
+ * Mihai Șucan <mihai.sucan@gmail.com>
+ * Julian Viereck <jviereck@mozilla.com>
+ * Paul Rouget <paul@mozilla.com>
+ * Kyle Simpson <ksimpson@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+var EXPORTED_SYMBOLS = ["LayoutHelpers"];
+
+LayoutHelpers = {
+
+ /**
+ * Compute the position and the dimensions for the visible portion
+ * of a node, relativalely to the root window.
+ *
+ * @param nsIDOMNode aNode
+ * a DOM element to be highlighted
+ */
+ getDirtyRect: function LH_getDirectyRect(aNode) {
+ let frameWin = aNode.ownerDocument.defaultView;
+ let clientRect = aNode.getBoundingClientRect();
+
+ // Go up in the tree of frames to determine the correct rectangle.
+ // clientRect is read-only, we need to be able to change properties.
+ rect = {top: clientRect.top,
+ left: clientRect.left,
+ width: clientRect.width,
+ height: clientRect.height};
+
+ // We iterate through all the parent windows.
+ while (true) {
+
+ // Does the selection overflow on the right of its window?
+ let diffx = frameWin.innerWidth - (rect.left + rect.width);
+ if (diffx < 0) {
+ rect.width += diffx;
+ }
+
+ // Does the selection overflow on the bottom of its window?
+ let diffy = frameWin.innerHeight - (rect.top + rect.height);
+ if (diffy < 0) {
+ rect.height += diffy;
+ }
+
+ // Does the selection overflow on the left of its window?
+ if (rect.left < 0) {
+ rect.width += rect.left;
+ rect.left = 0;
+ }
+
+ // Does the selection overflow on the top of its window?
+ if (rect.top < 0) {
+ rect.height += rect.top;
+ rect.top = 0;
+ }
+
+ // Selection has been clipped to fit in its own window.
+
+ // Are we in the top-level window?
+ if (frameWin.parent === frameWin || !frameWin.frameElement) {
+ break;
+ }
+
+ // We are in an iframe.
+ // We take into account the parent iframe position and its
+ // offset (borders and padding).
+ let frameRect = frameWin.frameElement.getBoundingClientRect();
+
+ let [offsetTop, offsetLeft] =
+ this.getIframeContentOffset(frameWin.frameElement);
+
+ rect.top += frameRect.top + offsetTop;
+ rect.left += frameRect.left + offsetLeft;
+
+ frameWin = frameWin.parent;
+ }
+
+ return rect;
+ },
+
+ /**
+ * Returns iframe content offset (iframe border + padding).
+ * Note: this function shouldn't need to exist, had the platform provided a
+ * suitable API for determining the offset between the iframe's content and
+ * its bounding client rect. Bug 626359 should provide us with such an API.
+ *
+ * @param aIframe
+ * The iframe.
+ * @returns array [offsetTop, offsetLeft]
+ * offsetTop is the distance from the top of the iframe and the
+ * top of the content document.
+ * offsetLeft is the distance from the left of the iframe and the
+ * left of the content document.
+ */
+ getIframeContentOffset: function LH_getIframeContentOffset(aIframe) {
+ let style = aIframe.contentWindow.getComputedStyle(aIframe, null);
+
+ let paddingTop = parseInt(style.getPropertyValue("padding-top"));
+ let paddingLeft = parseInt(style.getPropertyValue("padding-left"));
+
+ let borderTop = parseInt(style.getPropertyValue("border-top-width"));
+ let borderLeft = parseInt(style.getPropertyValue("border-left-width"));
+
+ return [borderTop + paddingTop, borderLeft + paddingLeft];
+ },
+
+ /**
+ * Apply the page zoom factor.
+ */
+ getZoomedRect: function LH_getZoomedRect(aWin, aRect) {
+ // get page zoom factor, if any
+ let zoom =
+ aWin.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils)
+ .screenPixelsPerCSSPixel;
+
+ // adjust rect for zoom scaling
+ let aRectScaled = {};
+ for (let prop in aRect) {
+ aRectScaled[prop] = aRect[prop] * zoom;
+ }
+
+ return aRectScaled;
+ },
+
+
+ /**
+ * Find an element from the given coordinates. This method descends through
+ * frames to find the element the user clicked inside frames.
+ *
+ * @param DOMDocument aDocument the document to look into.
+ * @param integer aX
+ * @param integer aY
+ * @returns Node|null the element node found at the given coordinates.
+ */
+ getElementFromPoint: function LH_elementFromPoint(aDocument, aX, aY)
+ {
+ let node = aDocument.elementFromPoint(aX, aY);
+ if (node && node.contentDocument) {
+ if (node instanceof Ci.nsIDOMHTMLIFrameElement) {
+ let rect = node.getBoundingClientRect();
+
+ // Gap between the iframe and its content window.
+ let [offsetTop, offsetLeft] = LayoutHelpers.getIframeContentOffset(node);
+
+ aX -= rect.left + offsetLeft;
+ aY -= rect.top + offsetTop;
+
+ if (aX < 0 || aY < 0) {
+ // Didn't reach the content document, still over the iframe.
+ return node;
+ }
+ }
+ if (node instanceof Ci.nsIDOMHTMLIFrameElement ||
+ node instanceof Ci.nsIDOMHTMLFrameElement) {
+ let subnode = this.getElementFromPoint(node.contentDocument, aX, aY);
+ if (subnode) {
+ node = subnode;
+ }
+ }
+ }
+ return node;
+ },
+};
--- a/browser/devtools/shared/Makefile.in
+++ b/browser/devtools/shared/Makefile.in
@@ -49,8 +49,9 @@ ifdef ENABLE_TESTS
DIRS += test
endif
include $(topsrcdir)/config/rules.mk
libs::
$(NSINSTALL) $(srcdir)/Templater.jsm $(FINAL_TARGET)/modules/devtools
$(NSINSTALL) $(srcdir)/Promise.jsm $(FINAL_TARGET)/modules/devtools
+ $(NSINSTALL) $(srcdir)/LayoutHelpers.jsm $(FINAL_TARGET)/modules/devtools
--- a/browser/devtools/sourceeditor/Makefile.in
+++ b/browser/devtools/sourceeditor/Makefile.in
@@ -47,11 +47,12 @@ include $(DEPTH)/config/autoconf.mk
ifdef ENABLE_TESTS
DIRS += test
endif
EXTRA_JS_MODULES = \
source-editor.jsm \
source-editor-orion.jsm \
source-editor-textarea.jsm \
+ source-editor-ui.jsm \
$(NULL)
include $(topsrcdir)/config/rules.mk
--- a/browser/devtools/sourceeditor/source-editor-orion.jsm
+++ b/browser/devtools/sourceeditor/source-editor-orion.jsm
@@ -39,16 +39,17 @@
"use strict";
const Cu = Components.utils;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/source-editor-ui.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
"@mozilla.org/widget/clipboardhelper;1",
"nsIClipboardHelper");
const ORION_SCRIPT = "chrome://browser/content/orion.js";
const ORION_IFRAME = "data:text/html;charset=utf8,<!DOCTYPE html>" +
"<html style='height:100%' dir='ltr'>" +
@@ -121,16 +122,18 @@ function SourceEditor() {
// Update the SourceEditor defaults from user preferences.
SourceEditor.DEFAULTS.TAB_SIZE =
Services.prefs.getIntPref(SourceEditor.PREFS.TAB_SIZE);
SourceEditor.DEFAULTS.EXPAND_TAB =
Services.prefs.getBoolPref(SourceEditor.PREFS.EXPAND_TAB);
this._onOrionSelection = this._onOrionSelection.bind(this);
+
+ this.ui = new SourceEditorUI(this);
}
SourceEditor.prototype = {
_view: null,
_iframe: null,
_model: null,
_undoStack: null,
_linesRuler: null,
@@ -139,16 +142,23 @@ SourceEditor.prototype = {
_annotationModel: null,
_dragAndDrop: null,
_mode: null,
_expandTab: null,
_tabSize: null,
_iframeWindow: null,
/**
+ * The Source Editor user interface manager.
+ * @type object
+ * An instance of the SourceEditorUI.
+ */
+ ui: null,
+
+ /**
* The editor container element.
* @type nsIDOMElement
*/
parentElement: null,
/**
* Initialize the editor.
*
@@ -199,16 +209,17 @@ SourceEditor.prototype = {
this._iframe.addEventListener("load", onIframeLoad, true);
this._iframe.setAttribute("src", ORION_IFRAME);
aElement.appendChild(this._iframe);
this.parentElement = aElement;
this._config = aConfig;
this._onReadyCallback = aCallback;
+ this.ui.init();
},
/**
* The editor iframe load event handler.
* @private
*/
_onIframeLoad: function SE__onIframeLoad()
{
@@ -267,21 +278,32 @@ SourceEditor.prototype = {
this.setMode(config.mode || SourceEditor.DEFAULTS.MODE);
this._undoStack = new UndoStack(this._view,
config.undoLimit || SourceEditor.DEFAULTS.UNDO_LIMIT);
this._dragAndDrop = new TextDND(this._view, this._undoStack);
- this._view.setAction("undo", this.undo.bind(this));
- this._view.setAction("redo", this.redo.bind(this));
- this._view.setAction("tab", this._doTab.bind(this));
- this._view.setAction("Unindent Lines", this._doUnindentLines.bind(this));
- this._view.setAction("enter", this._doEnter.bind(this));
+ let actions = {
+ "undo": [this.undo, this],
+ "redo": [this.redo, this],
+ "tab": [this._doTab, this],
+ "Unindent Lines": [this._doUnindentLines, this],
+ "enter": [this._doEnter, this],
+ "Find...": [this.ui.find, this.ui],
+ "Find Next Occurrence": [this.ui.findNext, this.ui],
+ "Find Previous Occurrence": [this.ui.findPrevious, this.ui],
+ "Goto Line...": [this.ui.gotoLine, this.ui],
+ };
+
+ for (let name in actions) {
+ let action = actions[name];
+ this._view.setAction(name, action[0].bind(action[1]));
+ }
let keys = (config.keys || []).concat(DEFAULT_KEYBINDINGS);
keys.forEach(function(aKey) {
let binding = new KeyBinding(aKey.code, aKey.accel, aKey.shift, aKey.alt);
this._view.setKeyBinding(binding, aKey.action);
if (aKey.callback) {
this._view.setAction(aKey.action, aKey.callback);
@@ -291,16 +313,17 @@ SourceEditor.prototype = {
/**
* The Orion "Load" event handler. This is called when the Orion editor
* completes the initialization.
* @private
*/
_onOrionLoad: function SE__onOrionLoad()
{
+ this.ui.onReady();
if (this._onReadyCallback) {
this._onReadyCallback(this);
this._onReadyCallback = null;
}
},
/**
* The "tab" editor action implementation. This adds support for expanded tabs
@@ -878,23 +901,27 @@ SourceEditor.prototype = {
destroy: function SE_destroy()
{
if (Services.appinfo.OS == "Linux") {
this._view.removeEventListener("Selection", this._onOrionSelection);
}
this._onOrionSelection = null;
this._view.destroy();
+ this.ui.destroy();
+ this.ui = null;
+
this.parentElement.removeChild(this._iframe);
this.parentElement = null;
this._iframeWindow = null;
this._iframe = null;
this._undoStack = null;
this._styler = null;
this._linesRuler = null;
this._dragAndDrop = null;
this._annotationModel = null;
this._annotationStyler = null;
this._view = null;
this._model = null;
this._config = null;
+ this._lastFind = null;
},
};
new file mode 100644
--- /dev/null
+++ b/browser/devtools/sourceeditor/source-editor-overlay.xul
@@ -0,0 +1,47 @@
+<?xml version="1.0"?>
+<!-- ***** BEGIN LICENSE BLOCK *****
+ - Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ -
+ - The contents of this file are subject to the Mozilla Public License Version
+ - 1.1 (the "License"); you may not use this file except in compliance with
+ - the License. You may obtain a copy of the License at
+ - http://www.mozilla.org/MPL/
+ -
+ - Software distributed under the License is distributed on an "AS IS" basis,
+ - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ - for the specific language governing rights and limitations under the
+ - License.
+ -
+ - The Original Code is Source Editor.
+ -
+ - The Initial Developer of the Original Code is
+ - The Mozilla Foundation.
+ - Portions created by the Initial Developer are Copyright (C) 2012
+ - the Initial Developer. All Rights Reserved.
+ -
+ - Contributor(s):
+ - Mihai Sucan <mihai.sucan@gmail.com> (original author)
+ -
+ - Alternatively, the contents of this file may be used under the terms of
+ - either the GNU General Public License Version 2 or later (the "GPL"), or
+ - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ - in which case the provisions of the GPL or the LGPL are applicable instead
+ - of those above. If you wish to allow use of your version of this file only
+ - under the terms of either the GPL or the LGPL, and not to allow others to
+ - use your version of this file under the terms of the MPL, indicate your
+ - decision by deleting the provisions above and replace them with the notice
+ - and other provisions required by the GPL or the LGPL. If you do not delete
+ - the provisions above, a recipient may use your version of this file under
+ - the terms of any one of the MPL, the GPL or the LGPL.
+ -
+ - ***** END LICENSE BLOCK ***** -->
+
+<overlay id="sourceEditorOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <commandset id="sourceEditorCommands">
+ <command id="cmd_find" oncommand="goDoCommand('cmd_find')"/>
+ <command id="cmd_findAgain" oncommand="goDoCommand('cmd_findAgain')" disabled="true"/>
+ <command id="cmd_findPrevious" oncommand="goDoCommand('cmd_findPrevious')" disabled="true"/>
+ <command id="cmd_gotoLine" oncommand="goDoCommand('cmd_gotoLine')"/>
+ </commandset>
+</overlay>
--- a/browser/devtools/sourceeditor/source-editor-textarea.jsm
+++ b/browser/devtools/sourceeditor/source-editor-textarea.jsm
@@ -39,19 +39,33 @@
"use strict";
const Cu = Components.utils;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/source-editor-ui.jsm");
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+/**
+ * Default key bindings in the textarea editor.
+ */
+const DEFAULT_KEYBINDINGS = [
+ {
+ _action: "_doTab",
+ keyCode: Ci.nsIDOMKeyEvent.DOM_VK_TAB,
+ shiftKey: false,
+ accelKey: false,
+ altKey: false,
+ },
+];
+
var EXPORTED_SYMBOLS = ["SourceEditor"];
/**
* The SourceEditor object constructor. The SourceEditor component allows you to
* provide users with an editor tailored to the specific needs of editing source
* code, aimed primarily at web developers.
*
* The editor used here is a simple textarea. This is used as a fallback
@@ -64,28 +78,37 @@ function SourceEditor() {
SourceEditor.DEFAULTS.TAB_SIZE =
Services.prefs.getIntPref(SourceEditor.PREFS.TAB_SIZE);
SourceEditor.DEFAULTS.EXPAND_TAB =
Services.prefs.getBoolPref(SourceEditor.PREFS.EXPAND_TAB);
this._listeners = {};
this._lastSelection = {};
+
+ this.ui = new SourceEditorUI(this);
}
SourceEditor.prototype = {
_textbox: null,
_editor: null,
_listeners: null,
_lineDelimiter: null,
_editActionListener: null,
_expandTab: null,
_tabSize: null,
/**
+ * The Source Editor user interface manager.
+ * @type object
+ * An instance of the SourceEditorUI.
+ */
+ ui: null,
+
+ /**
* The editor container element.
* @type nsIDOMElement
*/
parentElement: null,
/**
* Initialize the editor.
*
@@ -131,17 +154,17 @@ SourceEditor.prototype = {
this._textbox.style.MozTabSize = this._tabSize;
this._textbox.setAttribute("value", aConfig.placeholderText || "");
this._textbox.setAttribute("class", "monospace");
this._textbox.style.direction = "ltr";
this._textbox.readOnly = aConfig.readOnly;
// Make sure that the SourceEditor Selection events are fired properly.
- // Also make sure that the Tab key inserts spaces when expandTab is true.
+ // Also make sure that the configured keyboard bindings work.
this._textbox.addEventListener("select", this._onSelect.bind(this), false);
this._textbox.addEventListener("keypress", this._onKeyPress.bind(this), false);
this._textbox.addEventListener("keyup", this._onSelect.bind(this), false);
this._textbox.addEventListener("click", this._onSelect.bind(this), false);
// Mimic the mode change.
this.setMode(aConfig.mode || SourceEditor.DEFAULTS.MODE);
@@ -156,39 +179,71 @@ SourceEditor.prototype = {
this._editActionListener = new EditActionListener(this);
this._editor.addEditActionListener(this._editActionListener);
this._lineDelimiter = win.navigator.platform.indexOf("Win") > -1 ?
"\r\n" : "\n";
this._config = aConfig;
+ for each (let key in DEFAULT_KEYBINDINGS) {
+ for (let prop in key) {
+ if (prop == "accelKey") {
+ let newProp = Services.appinfo.OS == "Darwin" ? "metaKey" : "ctrlKey";
+ key[newProp] = key[prop];
+ delete key[prop];
+ break;
+ }
+ }
+ }
+
+ this.ui.init();
+ this.ui.onReady();
+
if (aCallback) {
aCallback(this);
}
},
/**
- * The textbox keypress event handler allows users to indent code using the
- * Tab key.
+ * The textbox keypress event handler calls the configured action for keyboard
+ * event.
*
* @private
* @param nsIDOMEvent aEvent
* The DOM object for the event.
+ * @see DEFAULT_KEYBINDINGS
*/
_onKeyPress: function SE__onKeyPress(aEvent)
{
- if (aEvent.keyCode != aEvent.DOM_VK_TAB || aEvent.shiftKey ||
- aEvent.metaKey || aEvent.ctrlKey || aEvent.altKey) {
- return;
+ for each (let key in DEFAULT_KEYBINDINGS) {
+ let matched = true;
+ for (let prop in key) {
+ if (prop.charAt(0) != "_" && aEvent[prop] !== key[prop]) {
+ matched = false;
+ break;
+ }
+ }
+ if (matched) {
+ let context = key._context ? this[key._context] : this;
+ context[key._action].call(context);
+ aEvent.preventDefault();
+ break;
+ }
}
+ },
- aEvent.preventDefault();
-
- let caret = this.getCaretOffset();
+ /**
+ * The Tab keypress event handler. This allows the user to indent the code
+ * with spaces, when expandTab is true.
+ */
+ _doTab: function SE__doTab()
+ {
+ let selection = this.getSelection();
+ let caret = selection.start;
let indent = "\t";
if (this._expandTab) {
let text = this._textbox.value;
let lineStart = caret;
while (lineStart > 0) {
let c = text.charAt(lineStart - 1);
if (c == "\r" || c == "\n") {
@@ -196,18 +251,18 @@ SourceEditor.prototype = {
}
lineStart--;
}
let offset = caret - lineStart;
let spaces = this._tabSize - (offset % this._tabSize);
indent = (new Array(spaces + 1)).join(" ");
}
- this.setText(indent, caret, caret);
- this.setCaretOffset(caret + indent.length);
+ this.setText(indent, selection.start, selection.end);
+ this.setCaretOffset(selection.start + indent.length);
},
/**
* The textbox keyup, click and select event handler tracks selection
* changes. This method invokes the SourceEditor Selection event handlers.
*
* @see SourceEditor.EVENTS.SELECTION
* @private
@@ -611,18 +666,18 @@ SourceEditor.prototype = {
* @param number [aColumn=0]
* Optional. The new caret column location. Columns start from 0.
*/
setCaretPosition: function SE_setCaretPosition(aLine, aColumn)
{
aColumn = aColumn || 0;
let text = this._textbox.value;
- let i = 0, n = text.length, c0, c1;
- let line = 0, col = 0;
+ let i = -1, n = text.length, c0, c1;
+ let line = 0, col = -1;
while (i < n) {
c1 = text.charAt(i++);
if (line < aLine && (c1 == "\r" || (c0 != "\r" && c1 == "\n"))) {
// Count lines and reset the column only until we reach the desired line
// such that if the desired column is out of boundaries we will stop
// after the given number of characters from the line start.
line++;
col = 0;
@@ -704,24 +759,29 @@ SourceEditor.prototype = {
if (aListener.domType) {
aListener.target.removeEventListener(aListener.domType,
aListener.handler, false);
}
}, this);
}
this._editor.removeEditActionListener(this._editActionListener);
+
+ this.ui.destroy();
+ this.ui = null;
+
this.parentElement.removeChild(this._textbox);
this.parentElement = null;
this._editor = null;
this._textbox = null;
this._config = null;
this._listeners = null;
this._lastSelection = null;
this._editActionListener = null;
+ this._lastFind = null;
},
};
/**
* The nsIEditActionListener for the nsIEditor of the xul:textbox used by the
* SourceEditor. This listener traces text changes such that SourceEditor
* TextChanged event handlers get their events.
*
new file mode 100644
--- /dev/null
+++ b/browser/devtools/sourceeditor/source-editor-ui.jsm
@@ -0,0 +1,288 @@
+/* vim:set ts=2 sw=2 sts=2 et tw=80:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Source Editor component.
+ *
+ * The Initial Developer of the Original Code is
+ * The Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Mihai Sucan <mihai.sucan@gmail.com> (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK *****/
+
+"use strict";
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+var EXPORTED_SYMBOLS = ["SourceEditorUI"];
+
+/**
+ * The Source Editor component user interface.
+ */
+function SourceEditorUI(aEditor)
+{
+ this.editor = aEditor;
+}
+
+SourceEditorUI.prototype = {
+ /**
+ * Initialize the user interface. This is called by the SourceEditor.init()
+ * method.
+ */
+ init: function SEU_init()
+ {
+ this._ownerWindow = this.editor.parentElement.ownerDocument.defaultView;
+ },
+
+ /**
+ * The UI onReady function is executed once the Source Editor completes
+ * initialization and it is ready for usage. Currently this code sets up the
+ * nsIController.
+ */
+ onReady: function SEU_onReady()
+ {
+ if (this._ownerWindow.controllers) {
+ this._controller = new SourceEditorController(this.editor);
+ this._ownerWindow.controllers.insertControllerAt(0, this._controller);
+ }
+ },
+
+ /**
+ * The "go to line" command UI. This displays a prompt that allows the user to
+ * input the line number to jump to.
+ */
+ gotoLine: function SEU_gotoLine()
+ {
+ let oldLine = this.editor.getCaretPosition ?
+ this.editor.getCaretPosition().line : null;
+ let newLine = {value: oldLine !== null ? oldLine + 1 : ""};
+
+ let result = Services.prompt.prompt(this._ownerWindow,
+ SourceEditorUI.strings.GetStringFromName("gotoLineCmd.promptTitle"),
+ SourceEditorUI.strings.GetStringFromName("gotoLineCmd.promptMessage"),
+ newLine, null, {});
+
+ newLine.value = parseInt(newLine.value);
+ if (result && !isNaN(newLine.value) && --newLine.value != oldLine) {
+ if (this.editor.getLineCount) {
+ let lines = this.editor.getLineCount() - 1;
+ this.editor.setCaretPosition(Math.max(0, Math.min(lines, newLine.value)));
+ } else {
+ this.editor.setCaretPosition(Math.max(0, newLine.value));
+ }
+ }
+
+ return true;
+ },
+
+ /**
+ * The "find" command UI. This displays a prompt that allows the user to input
+ * the string to search for in the code. By default the current selection is
+ * used as a search string, or the last search string.
+ */
+ find: function SEU_find()
+ {
+ let str = {value: this.editor.getSelectedText()};
+ if (!str.value && this.editor.lastFind) {
+ str.value = this.editor.lastFind.str;
+ }
+
+ let result = Services.prompt.prompt(this._ownerWindow,
+ SourceEditorUI.strings.GetStringFromName("findCmd.promptTitle"),
+ SourceEditorUI.strings.GetStringFromName("findCmd.promptMessage"),
+ str, null, {});
+
+ if (result && str.value) {
+ let start = this.editor.getSelection().end;
+ let pos = this.editor.find(str.value, {ignoreCase: true, start: start});
+ if (pos == -1) {
+ this.editor.find(str.value, {ignoreCase: true});
+ }
+ this._onFind();
+ }
+
+ return true;
+ },
+
+ /**
+ * Find the next occurrence of the last search string.
+ */
+ findNext: function SEU_findNext()
+ {
+ let lastFind = this.editor.lastFind;
+ if (lastFind) {
+ this.editor.findNext(true);
+ this._onFind();
+ }
+
+ return true;
+ },
+
+ /**
+ * Find the previous occurrence of the last search string.
+ */
+ findPrevious: function SEU_findPrevious()
+ {
+ let lastFind = this.editor.lastFind;
+ if (lastFind) {
+ this.editor.findPrevious(true);
+ this._onFind();
+ }
+
+ return true;
+ },
+
+ /**
+ * This executed after each find/findNext/findPrevious operation.
+ * @private
+ */
+ _onFind: function SEU__onFind()
+ {
+ let lastFind = this.editor.lastFind;
+ if (lastFind && lastFind.index > -1) {
+ this.editor.setSelection(lastFind.index, lastFind.index + lastFind.str.length);
+ }
+
+ if (this._ownerWindow.goUpdateCommand) {
+ this._ownerWindow.goUpdateCommand("cmd_findAgain");
+ this._ownerWindow.goUpdateCommand("cmd_findPrevious");
+ }
+ },
+
+ /**
+ * Destroy the SourceEditorUI instance. This is called by the
+ * SourceEditor.destroy() method.
+ */
+ destroy: function SEU_destroy()
+ {
+ this._ownerWindow = null;
+ this.editor = null;
+ this._controller = null;
+ },
+};
+
+/**
+ * The Source Editor nsIController implements features that need to be available
+ * from XUL commands.
+ *
+ * @constructor
+ * @param object aEditor
+ * SourceEditor object instance for which the controller is instanced.
+ */
+function SourceEditorController(aEditor)
+{
+ this._editor = aEditor;
+}
+
+SourceEditorController.prototype = {
+ /**
+ * Check if a command is supported by the controller.
+ *
+ * @param string aCommand
+ * The command name you want to check support for.
+ * @return boolean
+ * True if the command is supported, false otherwise.
+ */
+ supportsCommand: function SEC_supportsCommand(aCommand)
+ {
+ let result;
+
+ switch (aCommand) {
+ case "cmd_find":
+ case "cmd_findAgain":
+ case "cmd_findPrevious":
+ case "cmd_gotoLine":
+ result = true;
+ break;
+ default:
+ result = false;
+ break;
+ }
+
+ return result;
+ },
+
+ /**
+ * Check if a command is enabled or not.
+ *
+ * @param string aCommand
+ * The command name you want to check if it is enabled or not.
+ * @return boolean
+ * True if the command is enabled, false otherwise.
+ */
+ isCommandEnabled: function SEC_isCommandEnabled(aCommand)
+ {
+ let result;
+
+ switch (aCommand) {
+ case "cmd_find":
+ case "cmd_gotoLine":
+ result = true;
+ break;
+ case "cmd_findAgain":
+ case "cmd_findPrevious":
+ result = this._editor.lastFind && this._editor.lastFind.lastFound != -1;
+ break;
+ default:
+ result = false;
+ break;
+ }
+
+ return result;
+ },
+
+ /**
+ * Perform a command.
+ *
+ * @param string aCommand
+ * The command name you want to execute.
+ * @return void
+ */
+ doCommand: function SEC_doCommand(aCommand)
+ {
+ switch (aCommand) {
+ case "cmd_find":
+ this._editor.ui.find();
+ break;
+ case "cmd_findAgain":
+ this._editor.ui.findNext();
+ break;
+ case "cmd_findPrevious":
+ this._editor.ui.findPrevious();
+ break;
+ case "cmd_gotoLine":
+ this._editor.ui.gotoLine();
+ break;
+ }
+ },
+
+ onEvent: function() { }
+};
--- a/browser/devtools/sourceeditor/source-editor.jsm
+++ b/browser/devtools/sourceeditor/source-editor.jsm
@@ -36,22 +36,28 @@
*
* ***** END LICENSE BLOCK *****/
"use strict";
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/source-editor-ui.jsm");
const PREF_EDITOR_COMPONENT = "devtools.editor.component";
+const SOURCEEDITOR_L10N = "chrome://browser/locale/devtools/sourceeditor.properties";
var component = Services.prefs.getCharPref(PREF_EDITOR_COMPONENT);
var obj = {};
try {
+ if (component == "ui") {
+ throw new Error("The UI editor component is not available.");
+ }
Cu.import("resource:///modules/source-editor-" + component + ".jsm", obj);
} catch (ex) {
Cu.reportError(ex);
Cu.reportError("SourceEditor component failed to load: " + component);
// If the component does not exist, clear the user pref back to the default.
Services.prefs.clearUserPref(PREF_EDITOR_COMPONENT);
@@ -61,16 +67,20 @@ try {
}
// Export the SourceEditor.
var SourceEditor = obj.SourceEditor;
var EXPORTED_SYMBOLS = ["SourceEditor"];
// Add the constants used by all SourceEditors.
+XPCOMUtils.defineLazyGetter(SourceEditorUI, "strings", function() {
+ return Services.strings.createBundle(SOURCEEDITOR_L10N);
+});
+
/**
* Known SourceEditor preferences.
*/
SourceEditor.PREFS = {
TAB_SIZE: "devtools.editor.tabsize",
EXPAND_TAB: "devtools.editor.expandtab",
COMPONENT: PREF_EDITOR_COMPONENT,
};
@@ -137,8 +147,166 @@ SourceEditor.EVENTS = {
* object properties:
* - oldValue - the old selection range.
* - newValue - the new selection range.
* Both ranges are objects which hold two properties: start and end.
*/
SELECTION: "Selection",
};
+/**
+ * Extend a destination object with properties from a source object.
+ *
+ * @param object aDestination
+ * @param object aSource
+ */
+function extend(aDestination, aSource)
+{
+ for (let name in aSource) {
+ if (!aDestination.hasOwnProperty(name)) {
+ aDestination[name] = aSource[name];
+ }
+ }
+}
+
+/**
+ * Add methods common to all components.
+ */
+extend(SourceEditor.prototype, {
+ _lastFind: null,
+
+ /**
+ * Find a string in the editor.
+ *
+ * @param string aString
+ * The string you want to search for. If |aString| is not given the
+ * currently selected text is used.
+ * @param object [aOptions]
+ * Optional find options:
+ * - start: (integer) offset to start searching from. Default: 0 if
+ * backwards is false. If backwards is true then start = text.length.
+ * - ignoreCase: (boolean) tells if you want the search to be case
+ * insensitive or not. Default: false.
+ * - backwards: (boolean) tells if you want the search to go backwards
+ * from the given |start| offset. Default: false.
+ * @return integer
+ * The offset where the string was found.
+ */
+ find: function SE_find(aString, aOptions)
+ {
+ if (typeof(aString) != "string") {
+ return -1;
+ }
+
+ aOptions = aOptions || {};
+
+ let str = aOptions.ignoreCase ? aString.toLowerCase() : aString;
+
+ let text = this.getText();
+ if (aOptions.ignoreCase) {
+ text = text.toLowerCase();
+ }
+
+ let index = aOptions.backwards ?
+ text.lastIndexOf(str, aOptions.start) :
+ text.indexOf(str, aOptions.start);
+
+ let lastFoundIndex = index;
+ if (index == -1 && this.lastFind && this.lastFind.index > -1 &&
+ this.lastFind.str === aString &&
+ this.lastFind.ignoreCase === !!aOptions.ignoreCase) {
+ lastFoundIndex = this.lastFind.index;
+ }
+
+ this._lastFind = {
+ str: aString,
+ index: index,
+ lastFound: lastFoundIndex,
+ ignoreCase: !!aOptions.ignoreCase,
+ };
+
+ return index;
+ },
+
+ /**
+ * Find the next occurrence of the last search operation.
+ *
+ * @param boolean aWrap
+ * Tells if you want to restart the search from the beginning of the
+ * document if the string is not found.
+ * @return integer
+ * The offset where the string was found.
+ */
+ findNext: function SE_findNext(aWrap)
+ {
+ if (!this.lastFind && this.lastFind.lastFound == -1) {
+ return -1;
+ }
+
+ let options = {
+ start: this.lastFind.lastFound + this.lastFind.str.length,
+ ignoreCase: this.lastFind.ignoreCase,
+ };
+
+ let index = this.find(this.lastFind.str, options);
+ if (index == -1 && aWrap) {
+ options.start = 0;
+ index = this.find(this.lastFind.str, options);
+ }
+
+ return index;
+ },
+
+ /**
+ * Find the previous occurrence of the last search operation.
+ *
+ * @param boolean aWrap
+ * Tells if you want to restart the search from the end of the
+ * document if the string is not found.
+ * @return integer
+ * The offset where the string was found.
+ */
+ findPrevious: function SE_findPrevious(aWrap)
+ {
+ if (!this.lastFind && this.lastFind.lastFound == -1) {
+ return -1;
+ }
+
+ let options = {
+ start: this.lastFind.lastFound - this.lastFind.str.length,
+ ignoreCase: this.lastFind.ignoreCase,
+ backwards: true,
+ };
+
+ let index;
+ if (options.start > 0) {
+ index = this.find(this.lastFind.str, options);
+ } else {
+ index = this._lastFind.index = -1;
+ }
+
+ if (index == -1 && aWrap) {
+ options.start = this.getCharCount() - 1;
+ index = this.find(this.lastFind.str, options);
+ }
+
+ return index;
+ },
+});
+
+/**
+ * Retrieve the last find operation result. This object holds the following
+ * properties:
+ * - str: the last search string.
+ * - index: stores the result of the most recent find operation. This is the
+ * index in the text where |str| was found or -1 otherwise.
+ * - lastFound: tracks the index where |str| was last found, throughout
+ * multiple find operations. This can be -1 if |str| was never found in the
+ * document.
+ * - ignoreCase: tells if the search was case insensitive or not.
+ * @type object
+ */
+Object.defineProperty(SourceEditor.prototype, "lastFind", {
+ get: function() { return this._lastFind; },
+ enumerable: true,
+ configurable: true,
+});
+
--- a/browser/devtools/sourceeditor/test/Makefile.in
+++ b/browser/devtools/sourceeditor/test/Makefile.in
@@ -48,12 +48,13 @@ include $(topsrcdir)/config/rules.mk
browser_sourceeditor_initialization.js \
browser_bug684862_paste_html.js \
browser_bug687573_vscroll.js \
browser_bug687568_pagescroll.js \
browser_bug687580_drag_and_drop.js \
browser_bug684546_reset_undo.js \
browser_bug695035_middle_click_paste.js \
browser_bug687160_line_api.js \
+ browser_bug650345_find.js \
head.js \
libs:: $(_BROWSER_TEST_FILES)
$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/sourceeditor/test/browser_bug650345_find.js
@@ -0,0 +1,147 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource:///modules/source-editor.jsm");
+
+let testWin;
+let editor;
+
+function test()
+{
+ waitForExplicitFinish();
+
+ const windowUrl = "data:text/xml,<?xml version='1.0'?>" +
+ "<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
+ " title='test for bug 650345' width='600' height='500'><hbox flex='1'/></window>";
+ const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+ testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
+ testWin.addEventListener("load", function onWindowLoad() {
+ testWin.removeEventListener("load", onWindowLoad, false);
+ waitForFocus(initEditor, testWin);
+ }, false);
+}
+
+function initEditor()
+{
+ let hbox = testWin.document.querySelector("hbox");
+ editor = new SourceEditor();
+ editor.init(hbox, {}, editorLoaded);
+}
+
+function editorLoaded()
+{
+ editor.focus();
+
+ let text = "foobar bug650345\nBug650345 bazbaz\nfoobar omg\ntest";
+ editor.setText(text);
+
+ let needle = "foobar";
+ is(editor.find(), -1, "find() works");
+ ok(!editor.lastFind, "no editor.lastFind yet");
+
+ is(editor.find(needle), 0, "find('" + needle + "') works");
+ is(editor.lastFind.str, needle, "lastFind.str is correct");
+ is(editor.lastFind.index, 0, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, 0, "lastFind.lastFound is correct");
+ is(editor.lastFind.ignoreCase, false, "lastFind.ignoreCase is correct");
+
+ let newIndex = text.indexOf(needle, needle.length);
+ is(editor.findNext(), newIndex, "findNext() works");
+ is(editor.lastFind.str, needle, "lastFind.str is correct");
+ is(editor.lastFind.index, newIndex, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct");
+ is(editor.lastFind.ignoreCase, false, "lastFind.ignoreCase is correct");
+
+ is(editor.findNext(), -1, "findNext() works again");
+ is(editor.lastFind.index, -1, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct");
+
+ is(editor.findPrevious(), 0, "findPrevious() works");
+ is(editor.lastFind.index, 0, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, 0, "lastFind.lastFound is correct");
+
+ is(editor.findPrevious(), -1, "findPrevious() works again");
+ is(editor.lastFind.index, -1, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, 0, "lastFind.lastFound is correct");
+
+ is(editor.findNext(), newIndex, "findNext() works");
+ is(editor.lastFind.index, newIndex, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct");
+
+ is(editor.findNext(true), 0, "findNext(true) works");
+ is(editor.lastFind.index, 0, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, 0, "lastFind.lastFound is correct");
+
+ is(editor.findNext(true), newIndex, "findNext(true) works again");
+ is(editor.lastFind.index, newIndex, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct");
+
+ is(editor.findPrevious(true), 0, "findPrevious(true) works");
+ is(editor.lastFind.index, 0, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, 0, "lastFind.lastFound is correct");
+
+ is(editor.findPrevious(true), newIndex, "findPrevious(true) works again");
+ is(editor.lastFind.index, newIndex, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct");
+
+ needle = "error";
+ is(editor.find(needle), -1, "find('" + needle + "') works");
+ is(editor.lastFind.str, needle, "lastFind.str is correct");
+ is(editor.lastFind.index, -1, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, -1, "lastFind.lastFound is correct");
+ is(editor.lastFind.ignoreCase, false, "lastFind.ignoreCase is correct");
+
+ is(editor.findNext(), -1, "findNext() works");
+ is(editor.lastFind.str, needle, "lastFind.str is correct");
+ is(editor.lastFind.index, -1, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, -1, "lastFind.lastFound is correct");
+ is(editor.findNext(true), -1, "findNext(true) works");
+
+ is(editor.findPrevious(), -1, "findPrevious() works");
+ is(editor.findPrevious(true), -1, "findPrevious(true) works");
+
+ needle = "bug650345";
+ newIndex = text.indexOf(needle);
+
+ is(editor.find(needle), newIndex, "find('" + needle + "') works");
+ is(editor.findNext(), -1, "findNext() works");
+ is(editor.findNext(true), newIndex, "findNext(true) works");
+ is(editor.findPrevious(), -1, "findPrevious() works");
+ is(editor.findPrevious(true), newIndex, "findPrevious(true) works");
+ is(editor.lastFind.index, newIndex, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, newIndex, "lastFind.lastFound is correct");
+
+ is(editor.find(needle, {ignoreCase: 1}), newIndex,
+ "find('" + needle + "', {ignoreCase: 1}) works");
+ is(editor.lastFind.ignoreCase, true, "lastFind.ignoreCase is correct");
+
+ let newIndex2 = text.toLowerCase().indexOf(needle, newIndex + needle.length);
+ is(editor.findNext(), newIndex2, "findNext() works");
+ is(editor.findNext(), -1, "findNext() works");
+ is(editor.lastFind.index, -1, "lastFind.index is correct");
+ is(editor.lastFind.lastFound, newIndex2, "lastFind.lastFound is correct");
+
+ is(editor.findNext(true), newIndex, "findNext(true) works");
+
+ is(editor.findPrevious(), -1, "findPrevious() works");
+ is(editor.findPrevious(true), newIndex2, "findPrevious(true) works");
+ is(editor.findPrevious(), newIndex, "findPrevious() works again");
+
+ needle = "foobar";
+ newIndex = text.indexOf(needle, 2);
+ is(editor.find(needle, {start: 2}), newIndex,
+ "find('" + needle + "', {start:2}) works");
+ is(editor.findNext(), -1, "findNext() works");
+ is(editor.findNext(true), 0, "findNext(true) works");
+
+ editor.destroy();
+
+ testWin.close();
+ testWin = editor = null;
+
+ waitForFocus(finish, window);
+}
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
@@ -87,29 +87,27 @@ function setupHighlighterTests()
}
function runSelectionTests()
{
Services.obs.removeObserver(runSelectionTests,
InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
executeSoon(function() {
- Services.obs.addObserver(performTestComparisons,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+ InspectorUI.highlighter.addListener("nodeselected", performTestComparisons);
EventUtils.synthesizeMouse(h1, 2, 2, {type: "mousemove"}, content);
});
}
function performTestComparisons(evt)
{
- Services.obs.removeObserver(performTestComparisons,
- InspectorUI.INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, false);
+ InspectorUI.highlighter.removeListener("nodeselected", performTestComparisons);
InspectorUI.stopInspecting();
- ok(InspectorUI.highlighter.isHighlighting, "highlighter is highlighting");
+ is(InspectorUI.highlighter.node, h1, "node selected");
is(InspectorUI.selection, h1, "selection matches node");
HUDService.activateHUDForContext(gBrowser.selectedTab);
let hudId = HUDService.getHudIdByWindow(content);
let hud = HUDService.hudReferences[hudId];
let jsterm = hud.jsterm;
outputNode = hud.outputNode;
--- a/browser/locales/en-US/chrome/browser/devtools/scratchpad.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/scratchpad.dtd
@@ -62,16 +62,36 @@
<!ENTITY pasteCmd.label "Paste">
<!ENTITY pasteCmd.key "V">
<!ENTITY pasteCmd.accesskey "P">
<!ENTITY selectAllCmd.label "Select All">
<!ENTITY selectAllCmd.key "A">
<!ENTITY selectAllCmd.accesskey "A">
+<!ENTITY findCmd.label "Find…">
+<!ENTITY findCmd.key "F">
+<!ENTITY findCmd.accesskey "F">
+
+<!ENTITY findAgainCmd.label "Find Again…">
+<!-- LOCALIZATION NOTE (findAgainCmd.key): This key is used only on Macs.
+ - Windows and Linux builds use the F3 key which is not localizable on purpose.
+ -->
+<!ENTITY findAgainCmd.key "G">
+<!ENTITY findAgainCmd.accesskey "g">
+<!-- LOCALIZATION NOTE (findPreviousCmd.key): This key is used only on Macs.
+ - Windows and Linux builds use the Shift-F3 key which is not localizable on
+ - purpose.
+ -->
+<!ENTITY findPreviousCmd.key "G">
+
+<!ENTITY gotoLineCmd.label "Jump to line…">
+<!ENTITY gotoLineCmd.key "J">
+<!ENTITY gotoLineCmd.accesskey "J">
+
<!ENTITY run.label "Run">
<!ENTITY run.accesskey "R">
<!ENTITY run.key "r">
<!ENTITY inspect.label "Inspect">
<!ENTITY inspect.accesskey "I">
<!ENTITY inspect.key "i">
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/devtools/sourceeditor.properties
@@ -0,0 +1,30 @@
+# LOCALIZATION NOTE These strings are used inside the Source Editor component.
+# This component is used whenever source code is displayed for the purpose of
+# being edited, inside the Firefox developer tools - current examples are the
+# Scratchpad and the Style Editor tools.
+
+# LOCALIZATION NOTE The correct localization of this file might be to keep it
+# in English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best documentation
+# on web development on the web.
+
+# LOCALIZATION NOTE (findCmd.promptTitle): This is the dialog title used
+# when the user wants to search for a string in the code. You can
+# access this feature by pressing Ctrl-F on Windows/Linux or Cmd-F on Mac.
+findCmd.promptTitle=Find…
+
+# LOCALIZATION NOTE (gotoLineCmd.promptMessage): This is the message shown when
+# the user wants to search for a string in the code. You can
+# access this feature by pressing Ctrl-F on Windows/Linux or Cmd-F on Mac.
+findCmd.promptMessage=Search for:
+
+# LOCALIZATION NOTE (gotoLineCmd.promptTitle): This is the dialog title used
+# when the user wants to jump to a specific line number in the code. You can
+# access this feature by pressing Ctrl-J on Windows/Linux or Cmd-J on Mac.
+gotoLineCmd.promptTitle=Go to line…
+
+# LOCALIZATION NOTE (gotoLineCmd.promptMessage): This is the message shown when
+# the user wants to jump to a specific line number in the code. You can
+# access this feature by pressing Ctrl-J on Windows/Linux or Cmd-J on Mac.
+gotoLineCmd.promptMessage=Jump to line number:
--- a/browser/locales/jar.mn
+++ b/browser/locales/jar.mn
@@ -22,16 +22,17 @@
locale/browser/devtools/tilt.properties (%chrome/browser/devtools/tilt.properties)
locale/browser/devtools/scratchpad.properties (%chrome/browser/devtools/scratchpad.properties)
locale/browser/devtools/scratchpad.dtd (%chrome/browser/devtools/scratchpad.dtd)
locale/browser/devtools/styleeditor.properties (%chrome/browser/devtools/styleeditor.properties)
locale/browser/devtools/styleeditor.dtd (%chrome/browser/devtools/styleeditor.dtd)
locale/browser/devtools/styleinspector.properties (%chrome/browser/devtools/styleinspector.properties)
locale/browser/devtools/styleinspector.dtd (%chrome/browser/devtools/styleinspector.dtd)
locale/browser/devtools/webConsole.dtd (%chrome/browser/devtools/webConsole.dtd)
+ locale/browser/devtools/sourceeditor.properties (%chrome/browser/devtools/sourceeditor.properties)
locale/browser/openLocation.dtd (%chrome/browser/openLocation.dtd)
locale/browser/openLocation.properties (%chrome/browser/openLocation.properties)
* locale/browser/pageInfo.dtd (%chrome/browser/pageInfo.dtd)
locale/browser/pageInfo.properties (%chrome/browser/pageInfo.properties)
locale/browser/quitDialog.properties (%chrome/browser/quitDialog.properties)
* locale/browser/safeMode.dtd (%chrome/browser/safeMode.dtd)
locale/browser/sanitize.dtd (%chrome/browser/sanitize.dtd)
locale/browser/search.properties (%chrome/browser/search.properties)