author | Marco Bonardo <mbonardo@mozilla.com> |
Mon, 27 Feb 2012 13:46:22 +0100 | |
changeset 87841 | c6fe976aac16f404887a5fc725f10e355328cc43 |
parent 87840 | 8ea9dc2f857031a0642d9ad6bc28c0b1ca8a7278 (current diff) |
parent 87816 | 7d7179d2d8096d5ef290779da549d0d11c1fea92 (diff) |
child 87842 | f91b74bfcb1fb8ef99bba7f11535c23364896b01 |
push id | 22160 |
push user | mbrubeck@mozilla.com |
push date | Tue, 28 Feb 2012 17:21:33 +0000 |
treeherder | mozilla-central@dde4e0089a18 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 13.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1023,16 +1023,19 @@ pref("services.sync.prefs.sync.xpinstall #endif // Disable the error console pref("devtools.errorconsole.enabled", false); // Enable the Inspector pref("devtools.inspector.enabled", true); pref("devtools.inspector.htmlHeight", 112); +pref("devtools.inspector.htmlPanelOpen", false); +pref("devtools.inspector.sidebarOpen", false); +pref("devtools.inspector.activeSidebar", "ruleview"); // Enable the Debugger pref("devtools.debugger.enabled", false); // The default Debugger UI height pref("devtools.debugger.ui.height", 250); // Enable the style inspector
--- a/browser/base/content/browser-sets.inc +++ b/browser/base/content/browser-sets.inc @@ -20,16 +20,17 @@ # the Initial Developer. All Rights Reserved. # # Contributor(s): # Ben Goodger <ben@bengoodger.com> (v2.0) # Blake Ross <blakeross@telocity.com> # Shawn Wilsher <me@shawnwilsher.com> # Ehsan Akhgari <ehsan.akhgari@gmail.com> # Rob Campbell <rcampbell@mozilla.com> +# Paul Rouget <paul@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 @@ -148,31 +149,39 @@ <commandset id="inspectorCommands"> <command id="Inspector:Inspect" oncommand="InspectorUI.toggleInspection();"/> <command id="Inspector:Sidebar" oncommand="InspectorUI.toggleSidebar();"/> <command id="Inspector:Tilt" oncommand="Tilt.initialize();"/> + <command id="Inspector:HTMLPanel" + oncommand="InspectorUI.toggleHTMLPanel();"/> + <command id="Inspector:CopyInner" + oncommand="InspectorUI.copyInnerHTML();"/> + <command id="Inspector:CopyOuter" + oncommand="InspectorUI.copyOuterHTML();"/> + <command id="Inspector:DeleteNode" + oncommand="InspectorUI.deleteNode();"/> </commandset> <broadcasterset id="mainBroadcasterSet"> <broadcaster id="viewBookmarksSidebar" autoCheck="false" label="&bookmarksButton.label;" type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/bookmarks/bookmarksPanel.xul" oncommand="toggleSidebar('viewBookmarksSidebar');"/> <!-- for both places and non-places, the sidebar lives at chrome://browser/content/history/history-panel.xul so there are no problems when switching between versions --> <broadcaster id="viewHistorySidebar" autoCheck="false" sidebartitle="&historyButton.label;" type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/history/history-panel.xul" oncommand="toggleSidebar('viewHistorySidebar');"/> - + <broadcaster id="viewWebPanelsSidebar" autoCheck="false" type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/web-panels.xul" oncommand="toggleSidebar('viewWebPanelsSidebar');"/> <!-- popup blocking menu items --> <broadcaster id="blockedPopupAllowSite" accesskey="&allowPopups.accesskey;" oncommand="gPopupBlockerObserver.toggleAllowPopupsForSite(event);"/>
--- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -33,16 +33,17 @@ # Dão Gottwald <dao@mozilla.com> # Ehsan Akhgari <ehsan.akhgari@gmail.com> # Robert Strong <robert.bugzilla@gmail.com> # Rob Campbell <rcampbell@mozilla.com> # Patrick Walton <pcwalton@mozilla.com> # David Dahl <ddahl@mozilla.com> # Frank Yan <fyan@mozilla.com> # Victor Porof <vporof@mozilla.com> +# Paul Rouget <paul@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 @@ -222,30 +223,31 @@ class="editBookmarkPanelBottomButton" label="&editBookmark.done.label;" default="true" oncommand="StarUI.panel.hidePopup();"/> #endif </hbox> </panel> - <panel id="inspector-tree-panel" - orient="vertical" - hidden="true" - ignorekeys="true" - noautofocus="true" - noautohide="true" - titlebar="normal" - close="true" - label="&inspectPanelTitle.label;"> - <hbox id="tree-panel-resizer-box" align="end"> - <spacer flex="1" /> - <resizer dir="bottomend" /> - </hbox> - </panel> + <menupopup id="inspector-node-popup"> + <menuitem id="inspectorHTMLCopyInner" + label="&inspectorHTMLCopyInner.label;" + accesskey="&inspectorHTMLCopyInner.accesskey;" + command="Inspector:CopyInner"/> + <menuitem id="inspectorHTMLCopyOuter" + label="&inspectorHTMLCopyOuter.label;" + accesskey="&inspectorHTMLCopyOuter.accesskey;" + command="Inspector:CopyOuter"/> + <menuseparator/> + <menuitem id="inspectorHTMLDelete" + label="&inspectorHTMLDelete.label;" + accesskey="&inspectorHTMLDelete.accesskey;" + command="Inspector:DeleteNode"/> + </menupopup> <menupopup id="toolbar-context-menu" onpopupshowing="onViewToolbarsPopupShowing(event);"> <menuseparator/> <menuitem command="cmd_ToggleTabsOnTop" type="checkbox" label="&viewTabsOnTop.label;" accesskey="&viewTabsOnTop.accesskey;"/> @@ -989,55 +991,54 @@ </hbox> </hbox> <vbox id="browser-bottombox" layer="true"> <toolbar id="inspector-toolbar" class="devtools-toolbar" nowindowdrag="true" hidden="true"> - <vbox flex="1"> - <resizer id="inspector-top-resizer" flex="1" - dir="top" disabled="true" - element="inspector-tree-box"/> - <hbox> #ifdef XP_MACOSX - <toolbarbutton id="highlighter-closebutton" - oncommand="InspectorUI.closeInspectorUI(false);" - tooltiptext="&inspectCloseButton.tooltiptext;"/> + <toolbarbutton id="highlighter-closebutton" + oncommand="InspectorUI.closeInspectorUI(false);" + tooltiptext="&inspectCloseButton.tooltiptext;"/> #endif - <toolbarbutton id="inspector-inspect-toolbutton" - class="devtools-toolbarbutton" - label="&inspectButton.label;" - accesskey="&inspectButton.accesskey;" - command="Inspector:Inspect"/> - <arrowscrollbox id="inspector-breadcrumbs" - flex="1" orient="horizontal" - clicktoscroll="true"/> - <hbox id="inspector-tools"> - <toolbarbutton id="inspector-3D-button" - class="devtools-toolbarbutton" - hidden="true" - label="&inspect3DViewButton.label;" - accesskey="&inspect3DViewButton.accesskey;" - command="Inspector:Tilt"/> - <toolbarbutton id="inspector-style-button" - class="devtools-toolbarbutton" - label="&inspectStyleButton.label;" - accesskey="&inspectStyleButton.accesskey;" - command="Inspector:Sidebar"/> - <!-- registered tools go here --> - </hbox> + <toolbarbutton id="inspector-inspect-toolbutton" + class="devtools-toolbarbutton" + label="&inspectButton.label;" + accesskey="&inspectButton.accesskey;" + command="Inspector:Inspect"/> + <toolbarbutton id="inspector-treepanel-toolbutton" + class="devtools-toolbarbutton" + label="&htmlPanel.label;" + accesskey="&htmlPanel.accesskey;" + tooltiptext="&htmlPanel.tooltiptext;" + command="Inspector:HTMLPanel"/> + <arrowscrollbox id="inspector-breadcrumbs" + flex="1" orient="horizontal" + clicktoscroll="true"/> + <hbox id="inspector-tools"> + <toolbarbutton id="inspector-3D-button" + class="devtools-toolbarbutton" + hidden="true" + label="&inspect3DViewButton.label;" + accesskey="&inspect3DViewButton.accesskey;" + command="Inspector:Tilt"/> + <toolbarbutton id="inspector-style-button" + class="devtools-toolbarbutton" + label="&inspectStyleButton.label;" + accesskey="&inspectStyleButton.accesskey;" + command="Inspector:Sidebar"/> + <!-- registered tools go here --> + </hbox> #ifndef XP_MACOSX - <toolbarbutton id="highlighter-closebutton" - oncommand="InspectorUI.closeInspectorUI(false);" - tooltiptext="&inspectCloseButton.tooltiptext;"/> + <toolbarbutton id="highlighter-closebutton" + oncommand="InspectorUI.closeInspectorUI(false);" + tooltiptext="&inspectCloseButton.tooltiptext;"/> #endif - </hbox> - </vbox> </toolbar> <toolbar id="addon-bar" toolbarname="&addonBarCmd.label;" accesskey="&addonBarCmd.accesskey;" collapsed="true" class="toolbar-primary chromeclass-toolbar" context="toolbar-context-menu" toolboxid="navigator-toolbox" mode="icons" iconsize="small" defaulticonsize="small" lockiconsize="true"
--- a/browser/base/content/highlighter.css +++ b/browser/base/content/highlighter.css @@ -29,24 +29,16 @@ #highlighter-veil-middlebox:-moz-locale-dir(rtl) { -moz-box-direction: reverse; } .inspector-breadcrumbs-button { direction: ltr; } -#inspector-top-resizer { - display: none; -} - -#inspector-toolbar[treepanel-open] > vbox > #inspector-top-resizer { - display: -moz-box; -} - /* * Node Infobar */ #highlighter-nodeinfobar-container { position: absolute; max-width: 95%; }
--- a/browser/devtools/debugger/debugger-view.js +++ b/browser/devtools/debugger/debugger-view.js @@ -344,17 +344,17 @@ DebuggerView.Properties = { */ _addScope: function DVP__addScope(aName, aId) { // make sure the parent container exists if (!this._vars) { return null; } // compute the id of the element if not specified - aId = aId || (aName + "-scope"); + aId = aId || (aName.toLowerCase().trim().replace(" ", "-") + "-scope"); // contains generic nodes and functionality let element = this._createPropertyElement(aName, aId, "scope", this._vars); // make sure the element was created successfully if (!element) { dump("The debugger scope container wasn't created properly: " + aId); return null; @@ -536,22 +536,22 @@ DebuggerView.Properties = { * default id set as aVar.id->aKey-property. * * @param object aVar * The parent variable element. * @param {Array} aProperty * An array containing the key and grip properties, specifying * the value and/or type & class of the variable (if the type * is not specified, it will be inferred from the value). - * e.g. ["someProp0": 42] - * ["someProp1": true] - * ["someProp2": "nasu"] - * ["someProp3": { type: "undefined" }] - * ["someProp4": { type: "null" }] - * ["someProp5": { type: "object", class: "Object" }] + * e.g. ["someProp0", 42] + * ["someProp1", true] + * ["someProp2", "nasu"] + * ["someProp3", { type: "undefined" }] + * ["someProp4", { type: "null" }] + * ["someProp5", { type: "object", class: "Object" }] * @param string aName * Optional, the property name. * @paarm string aId * Optional, an id for the property html node. * @return object * The newly created html node representing the added prop. */ _addProperty: function DVP__addProperty(aVar, aProperty, aName, aId) {
--- a/browser/devtools/debugger/test/browser_dbg_debuggerstatement.js +++ b/browser/devtools/debugger/test/browser_dbg_debuggerstatement.js @@ -29,17 +29,18 @@ function test_early_debugger_statement(a { let paused = function(aEvent, aPacket) { ok(false, "Pause shouldn't be called before we've attached!\n"); finish_test(); }; gClient.addListener("paused", paused); // This should continue without nesting an event loop and calling // the onPaused hook, because we haven't attached yet. - gTab.linkedBrowser.contentWindow.wrappedJSObject.runDebuggerStatement(); + // TODO: uncomment this when bug 723563 is fixed. + //gTab.linkedBrowser.contentWindow.wrappedJSObject.runDebuggerStatement(); gClient.removeListener("paused", paused); // Now attach and resume... gClient.request({ to: aActor.threadActor, type: "attach" }, function(aResponse) { gClient.request({ to: aActor.threadActor, type: "resume" }, function(aResponse) { test_debugger_statement(aActor); });
--- a/browser/devtools/debugger/test/browser_dbg_listtabs.js +++ b/browser/devtools/debugger/test/browser_dbg_listtabs.js @@ -82,17 +82,17 @@ function test_attach_removed_tab() removeTab(gTab2); gTab2 = null; gClient.addListener("paused", function(aEvent, aPacket) { ok(false, "Attaching to an exited tab actor shouldn't generate a pause."); finish_test(); }); gClient.request({ to: gTab2Actor, type: "attach" }, function(aResponse) { - is(aResponse.type, "exited", "Tab should consider itself exited."); + is(aResponse.error, "noSuchActor", "Tab should be gone."); finish_test(); }); } function finish_test() { gClient.close(function() { finish();
--- a/browser/devtools/highlighter/TreePanel.jsm +++ b/browser/devtools/highlighter/TreePanel.jsm @@ -59,74 +59,51 @@ const INSPECTOR_URI = "chrome://browser/ */ function TreePanel(aContext, aIUI) { this._init(aContext, aIUI); }; TreePanel.prototype = { showTextNodesWithWhitespace: false, id: "treepanel", // DO NOT LOCALIZE - openInDock: true, + _open: false, /** * The tree panel container element. * @returns xul:panel|xul:vbox|null * xul:panel is returned when the tree panel is not docked, or * xul:vbox when when the tree panel is docked. * null is returned when no container is available. */ get container() { - if (this.openInDock) { - return this.document.getElementById("inspector-tree-box"); - } - - return this.document.getElementById("inspector-tree-panel"); + return this.document.getElementById("inspector-tree-box"); }, /** * Main TreePanel boot-strapping method. Initialize the TreePanel with the * originating context and the InspectorUI global. * @param aContext nsIDOMWindow (xulwindow) * @param aIUI global InspectorUI object */ _init: function TP__init(aContext, aIUI) { this.IUI = aIUI; this.window = aContext; this.document = this.window.document; + this.button = + this.IUI.chromeDoc.getElementById("inspector-treepanel-toolbutton"); domplateUtils.setDOM(this.window); this.DOMHelpers = new DOMHelpers(this.window); let isOpen = this.isOpen.bind(this); - this.registrationObject = { - id: this.id, - label: this.IUI.strings.GetStringFromName("htmlPanel.label"), - tooltiptext: this.IUI.strings.GetStringFromName("htmlPanel.tooltiptext"), - accesskey: this.IUI.strings.GetStringFromName("htmlPanel.accesskey"), - context: this, - get isOpen() isOpen(), - show: this.open, - hide: this.close, - onSelect: this.select, - panel: this.openInDock ? null : this.container, - unregister: this.destroy, - }; this.editingEvents = {}; - - if (!this.openInDock) { - this._boundClose = this.close.bind(this); - this.container.addEventListener("popuphiding", this._boundClose, false); - } - - // Register the HTML panel with the highlighter - this.IUI.registerTool(this.registrationObject); }, /** * Initialization function for the TreePanel. */ initializeIFrame: function TP_initializeIFrame() { if (!this.initializingTreePanel || this.treeLoaded) { @@ -149,138 +126,96 @@ TreePanel.prototype = { this.select(this.IUI.selection, true); }, /** * Open the inspector's tree panel and initialize it. */ open: function TP_open() { - if (this.initializingTreePanel && !this.treeLoaded) { + if (this._open) { return; } + this._open = true; + + this.button.setAttribute("checked", true); this.initializingTreePanel = true; - if (!this.openInDock) - this.container.hidden = false; this.treeIFrame = this.document.getElementById("inspector-tree-iframe"); if (!this.treeIFrame) { this.treeIFrame = this.document.createElement("iframe"); this.treeIFrame.setAttribute("id", "inspector-tree-iframe"); this.treeIFrame.flex = 1; this.treeIFrame.setAttribute("type", "content"); - } - - if (this.openInDock) { // Create vbox - this.openDocked(); - return; + this.treeIFrame.setAttribute("context", "inspector-node-popup"); } - let resizerBox = this.document.getElementById("tree-panel-resizer-box"); - this.treeIFrame = this.container.insertBefore(this.treeIFrame, resizerBox); - - let boundLoadedInitializeTreePanel = function loadedInitializeTreePanel() - { - this.treeIFrame.removeEventListener("load", - boundLoadedInitializeTreePanel, true); - this.initializeIFrame(); - }.bind(this); - - let boundTreePanelShown = function treePanelShown() - { - this.container.removeEventListener("popupshown", - boundTreePanelShown, false); - - this.treeIFrame.addEventListener("load", - boundLoadedInitializeTreePanel, true); - - let src = this.treeIFrame.getAttribute("src"); - if (src != INSPECTOR_URI) { - this.treeIFrame.setAttribute("src", INSPECTOR_URI); - } else { - this.treeIFrame.contentWindow.location.reload(); - } - }.bind(this); - - this.container.addEventListener("popupshown", boundTreePanelShown, false); - - const panelWidthRatio = 7 / 8; - const panelHeightRatio = 1 / 5; - - let width = parseInt(this.IUI.win.outerWidth * panelWidthRatio); - let height = parseInt(this.IUI.win.outerHeight * panelHeightRatio); - let y = Math.min(this.document.defaultView.screen.availHeight - height, - this.IUI.win.innerHeight); - - this.container.openPopup(this.browser, "overlap", 0, 0, - false, false); - - this.container.moveTo(80, y); - this.container.sizeTo(width, height); - }, - - openDocked: function TP_openDocked() - { let treeBox = null; - let toolbar = this.IUI.toolbar.nextSibling; // Addons bar, typically - let toolbarParent = - this.IUI.browser.ownerDocument.getElementById("browser-bottombox"); treeBox = this.document.createElement("vbox"); treeBox.id = "inspector-tree-box"; - treeBox.state = "open"; // for the registerTools API. + treeBox.state = "open"; try { treeBox.height = Services.prefs.getIntPref("devtools.inspector.htmlHeight"); } catch(e) { treeBox.height = 112; } treeBox.minHeight = 64; - treeBox.flex = 1; - toolbarParent.insertBefore(treeBox, toolbar); + + this.splitter = this.document.createElement("splitter"); + this.splitter.id = "inspector-tree-splitter"; - this.IUI.toolbar.setAttribute("treepanel-open", "true"); + let container = this.document.getElementById("appcontent"); + container.appendChild(this.splitter); + container.appendChild(treeBox); treeBox.appendChild(this.treeIFrame); - let boundLoadedInitializeTreePanel = function loadedInitializeTreePanel() + this._boundLoadedInitializeTreePanel = function loadedInitializeTreePanel() { this.treeIFrame.removeEventListener("load", - boundLoadedInitializeTreePanel, true); + this._boundLoadedInitializeTreePanel, true); + delete this._boundLoadedInitializeTreePanel; this.initializeIFrame(); }.bind(this); this.treeIFrame.addEventListener("load", - boundLoadedInitializeTreePanel, true); + this._boundLoadedInitializeTreePanel, true); let src = this.treeIFrame.getAttribute("src"); if (src != INSPECTOR_URI) { this.treeIFrame.setAttribute("src", INSPECTOR_URI); } else { this.treeIFrame.contentWindow.location.reload(); } }, /** * Close the TreePanel. */ close: function TP_close() { - if (this.openInDock) { - this.IUI.toolbar.removeAttribute("treepanel-open"); + this._open = false; - let treeBox = this.container; - Services.prefs.setIntPref("devtools.inspector.htmlHeight", treeBox.height); - let treeBoxParent = treeBox.parentNode; - treeBoxParent.removeChild(treeBox); - } else { - this.container.hidePopup(); + // Stop caring about the tree iframe load if it's in progress. + if (this._boundLoadedInitializeTreePanel) { + this.treeIFrame.removeEventListener("load", + this._boundLoadedInitializeTreePanel, true); + delete this._boundLoadedInitializeTreePanel; } + this.button.removeAttribute("checked"); + let treeBox = this.container; + Services.prefs.setIntPref("devtools.inspector.htmlHeight", treeBox.height); + let treeBoxParent = treeBox.parentNode; + treeBoxParent.removeChild(this.splitter); + treeBoxParent.removeChild(treeBox); + if (this.treePanelDiv) { this.treePanelDiv.ownerPanel = null; let parent = this.treePanelDiv.parentNode; parent.removeChild(this.treePanelDiv); delete this.treePanelDiv; delete this.treeBrowserDocument; } @@ -288,20 +223,25 @@ TreePanel.prototype = { }, /** * Is the TreePanel open? * @returns boolean */ isOpen: function TP_isOpen() { - if (this.openInDock) - return this.treeLoaded && this.container; + return this._open; + }, - return this.treeLoaded && this.container.state == "open"; + /** + * Toggle the TreePanel. + */ + toggle: function TP_toggle() + { + this.isOpen() ? this.close() : this.open(); }, /** * Create the ObjectBox for the given object. * @param object nsIDOMNode * @param isRoot boolean - Is this the root object? * @returns InsideOutBox */ @@ -665,16 +605,30 @@ TreePanel.prototype = { else return child.repObject; } } return null; }, /** + * Remove a node box from the tree view. + * @param aElement + * The DOM node to remove from the HTML IOBox. + */ + deleteChildBox: function TP_deleteChildBox(aElement) + { + let childBox = this.ioBox.findObjectBox(aElement); + if (!childBox) { + return; + } + childBox.parentNode.removeChild(childBox); + }, + + /** * Destructor function. Cleanup. */ destroy: function TP_destroy() { if (this.isOpen()) { this.close(); } @@ -700,21 +654,16 @@ TreePanel.prototype = { parent.removeChild(this.treeIFrame); delete this.treeIFrame; } if (this.ioBox) { this.ioBox.destroy(); delete this.ioBox; } - - if (!this.openInDock) { - this.container.removeEventListener("popuphiding", this._boundClose, false); - delete this._boundClose; - } } }; /** * DOMHelpers * Makes DOM traversal easier. Goes through iframes. *
--- a/browser/devtools/highlighter/highlighter.jsm +++ b/browser/devtools/highlighter/highlighter.jsm @@ -38,32 +38,39 @@ * 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 Cc = Components.classes; +const Ci = Components.interfaces; + Cu.import("resource:///modules/devtools/LayoutHelpers.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.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, }; +const PSEUDO_CLASSES = [":hover", ":active", ":focus"]; + // add ":visited" and ":link" after bug 713106 is fixed + /** * 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: @@ -104,16 +111,18 @@ const INSPECTOR_INVISIBLE_ELEMENTS = { * * 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 + * "pseudoclasstoggled" - A pseudo-class lock has changed on the selected node + * * Structure: * * <stack id="highlighter-container"> * <vbox id="highlighter-veil-container">...</vbox> * <box id="highlighter-controls>...</vbox> * </stack> * @@ -234,16 +243,27 @@ Highlighter.prototype = { this.invalidateSize(!!aScroll); if (oldNode !== this.node) { this.emitEvent("nodeselected"); } }, /** + * Notify that a pseudo-class lock was toggled on the highlighted element + * + * @param aPseudo - The pseudo-class to toggle, e.g. ":hover". + */ + pseudoClassLockToggled: function Highlighter_pseudoClassLockToggled(aPseudo) + { + this.emitEvent("pseudoclasstoggled", [aPseudo]); + this.updateInfobar(); + }, + + /** * Update the highlighter size and position. */ invalidateSize: function Highlighter_invalidateSize(aScroll) { let rect = null; if (this.node && this.isNodeHighlightable(this.node)) { @@ -441,40 +461,91 @@ Highlighter.prototype = { let tagNameLabel = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span"); tagNameLabel.id = "highlighter-nodeinfobar-tagname"; let idLabel = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span"); idLabel.id = "highlighter-nodeinfobar-id"; let classesBox = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span"); classesBox.id = "highlighter-nodeinfobar-classes"; + + let pseudoClassesBox = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span"); + pseudoClassesBox.id = "highlighter-nodeinfobar-pseudo-classes"; + // Add some content to force a better boundingClientRect down below. - classesBox.textContent = " "; + pseudoClassesBox.textContent = " "; nodeInfobar.appendChild(tagNameLabel); nodeInfobar.appendChild(idLabel); nodeInfobar.appendChild(classesBox); + nodeInfobar.appendChild(pseudoClassesBox); container.appendChild(arrowBoxTop); container.appendChild(nodeInfobar); container.appendChild(arrowBoxBottom); aParent.appendChild(container); + nodeInfobar.onclick = (function _onInfobarRightClick(aEvent) { + if (aEvent.button == 2) { + this.openPseudoClassMenu(); + } + }).bind(this); + let barHeight = container.getBoundingClientRect().height; this.nodeInfo = { tagNameLabel: tagNameLabel, idLabel: idLabel, classesBox: classesBox, + pseudoClassesBox: pseudoClassesBox, container: container, barHeight: barHeight, }; }, /** + * Open the infobar's pseudo-class context menu. + */ + openPseudoClassMenu: function Highlighter_openPseudoClassMenu() + { + let menu = this.chromeDoc.createElement("menupopup"); + menu.id = "infobar-context-menu"; + + let popupSet = this.chromeDoc.getElementById("mainPopupSet"); + popupSet.appendChild(menu); + + let fragment = this.buildPseudoClassMenu(); + menu.appendChild(fragment); + + menu.openPopup(this.nodeInfo.pseudoClassesBox, "end_before", 0, 0, true, false); + }, + + /** + * Create the menuitems for toggling the selection's pseudo-class state + * + * @returns DocumentFragment. The menuitems for toggling pseudo-classes. + */ + buildPseudoClassMenu: function IUI_buildPseudoClassesMenu() + { + let fragment = this.chromeDoc.createDocumentFragment(); + for (let i = 0; i < PSEUDO_CLASSES.length; i++) { + let pseudo = PSEUDO_CLASSES[i]; + let item = this.chromeDoc.createElement("menuitem"); + item.setAttribute("type", "checkbox"); + item.setAttribute("label", pseudo); + item.addEventListener("command", + this.pseudoClassLockToggled.bind(this, pseudo), false); + item.setAttribute("checked", DOMUtils.hasPseudoClassLock(this.node, + pseudo)); + fragment.appendChild(item); + } + return fragment; + }, + + /** * 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) @@ -538,16 +609,24 @@ Highlighter.prototype = { // ID this.nodeInfo.idLabel.textContent = this.node.id ? "#" + this.node.id : ""; // Classes let classes = this.nodeInfo.classesBox; classes.textContent = this.node.classList.length ? "." + Array.join(this.node.classList, ".") : ""; + + // Pseudo-classes + let pseudos = PSEUDO_CLASSES.filter(function(pseudo) { + return DOMUtils.hasPseudoClassLock(this.node, pseudo); + }, this); + + let pseudoBox = this.nodeInfo.pseudoClassesBox; + pseudoBox.textContent = pseudos.join(""); }, /** * Move the Infobar to the right place in the highlighter. */ moveInfobar: function Highlighter_moveInfobar() { if (this._highlightRect) { @@ -612,18 +691,18 @@ Highlighter.prototype = { } }, /** * Store page zoom factor. */ computeZoomFactor: function Highlighter_computeZoomFactor() { this.zoom = - this.win.QueryInterface(Components.interfaces.nsIInterfaceRequestor) - .getInterface(Components.interfaces.nsIDOMWindowUtils) + this.win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) .screenPixelsPerCSSPixel; }, ///////////////////////////////////////////////////////////////////////// //// Event Emitter Mechanism addListener: function Highlighter_addListener(aEvent, aListener) { @@ -800,8 +879,11 @@ Highlighter.prototype = { if (element && element != this.node) { this.highlight(element); } }, }; /////////////////////////////////////////////////////////////////////////// +XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () { + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils) +});
--- a/browser/devtools/highlighter/inspector.jsm +++ b/browser/devtools/highlighter/inspector.jsm @@ -37,16 +37,17 @@ * 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 Cc = Components.classes; const Cu = Components.utils; 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"); @@ -77,16 +78,18 @@ const INSPECTOR_NOTIFICATIONS = { RULEVIEWREADY: "inspector-ruleview-ready", // Event notifications for the attribute-value editor EDITOR_OPENED: "inspector-editor-opened", EDITOR_CLOSED: "inspector-editor-closed", EDITOR_SAVED: "inspector-editor-saved", }; +const PSEUDO_CLASSES = [":hover", ":active", ":focus"]; + /////////////////////////////////////////////////////////////////////////// //// InspectorUI /** * Main controller class for the Inspector. * * @constructor * @param nsIDOMWindow aWindow @@ -103,17 +106,16 @@ function InspectorUI(aWindow) this.INSPECTOR_NOTIFICATIONS = INSPECTOR_NOTIFICATIONS; } InspectorUI.prototype = { browser: null, tools: null, toolEvents: null, inspecting: false, - treePanelEnabled: true, ruleViewEnabled: true, isDirty: false, store: null, /** * Toggle the inspector interface elements on or off. * * @param aEvent @@ -132,49 +134,87 @@ InspectorUI.prototype = { * Show the Sidebar. */ showSidebar: function IUI_showSidebar() { this.sidebarBox.removeAttribute("hidden"); this.sidebarSplitter.removeAttribute("hidden"); this.stylingButton.checked = true; - // Activate the first tool in the sidebar, only if none previously- - // selected. We'll want to do a followup to remember selected tool-states. + // If no tool is already selected, show the last-used sidebar if available, + // otherwise just show the first. + if (!Array.some(this.sidebarToolbar.children, function(btn) btn.hasAttribute("checked"))) { - let firstButtonId = this.getToolbarButtonId(this.sidebarTools[0].id); - this.chromeDoc.getElementById(firstButtonId).click(); + + let activePanel = this.sidebarTools[0]; + let activeId = this.store.getValue(this.winID, "activeSidebar"); + if (activeId && this.tools[activeId]) { + activePanel = this.tools[activeId]; + } + this.activateSidebarPanel(activePanel.id); } + + this.store.setValue(this.winID, "sidebarOpen", true); + Services.prefs.setBoolPref("devtools.inspector.sidebarOpen", true); }, /** - * Hide the Sidebar. + * Tear down the sidebar. */ - hideSidebar: function IUI_hideSidebar() + _destroySidebar: function IUI_destroySidebar() { this.sidebarBox.setAttribute("hidden", "true"); this.sidebarSplitter.setAttribute("hidden", "true"); this.stylingButton.checked = false; }, /** + * Hide the sidebar. + */ + hideSidebar: function IUI_hideSidebar() + { + this._destroySidebar(); + this.store.setValue(this.winID, "sidebarOpen", false); + Services.prefs.setBoolPref("devtools.inspector.sidebarOpen", false); + }, + + /** * Show or hide the sidebar. Called from the Styling button on the * highlighter toolbar. */ toggleSidebar: function IUI_toggleSidebar() { if (!this.isSidebarOpen) { this.showSidebar(); } else { this.hideSidebar(); } }, /** + * Activate a sidebar panel by id. + */ + activateSidebarPanel: function IUI_activateSidebarPanel(aID) + { + let buttonId = this.getToolbarButtonId(aID); + this.chromeDoc.getElementById(buttonId).click(); + }, + + get activeSidebarPanel() + { + for each (let tool in this.sidebarTools) { + if (this.sidebarDeck.selectedPanel == this.getToolIframe(tool)) { + return tool.id; + } + } + return null; + }, + + /** * Getter to test if the Sidebar is open or not. */ get isSidebarOpen() { return this.stylingButton.checked && !this.sidebarBox.hidden && !this.sidebarSplitter.hidden; }, @@ -188,16 +228,32 @@ InspectorUI.prototype = { if (this.inspecting) { this.stopInspecting(); } else { this.startInspecting(); } }, /** + * Toggle the TreePanel. + */ + toggleHTMLPanel: function TP_toggle() + { + if (this.treePanel.isOpen()) { + this.treePanel.close(); + Services.prefs.setBoolPref("devtools.inspector.htmlPanelOpen", false); + this.store.setValue(this.winID, "htmlPanelOpen", false); + } else { + this.treePanel.open(); + Services.prefs.setBoolPref("devtools.inspector.htmlPanelOpen", true); + this.store.setValue(this.winID, "htmlPanelOpen", true); + } + }, + + /** * Is the inspector UI open? Simply check if the toolbar is visible or not. * * @returns boolean */ get isInspectorOpen() { return this.toolbar && !this.toolbar.hidden && this.highlighter; }, @@ -255,19 +311,17 @@ InspectorUI.prototype = { this.toolbar = this.chromeDoc.getElementById("inspector-toolbar"); this.inspectMenuitem = this.chromeDoc.getElementById("Tools:Inspect"); this.inspectToolbutton = this.chromeDoc.getElementById("inspector-inspect-toolbutton"); this.initTools(); this.chromeWin.Tilt.setup(); - if (this.treePanelEnabled) { - this.treePanel = new TreePanel(this.chromeWin, this); - } + this.treePanel = new TreePanel(this.chromeWin, this); if (Services.prefs.getBoolPref("devtools.ruleview.enabled") && !this.toolRegistered("ruleview")) { this.registerRuleView(); } if (Services.prefs.getBoolPref("devtools.styleinspector.enabled") && !this.toolRegistered("styleinspector")) { @@ -305,16 +359,17 @@ InspectorUI.prototype = { label: this.strings.GetStringFromName("ruleView.label"), tooltiptext: this.strings.GetStringFromName("ruleView.tooltiptext"), accesskey: this.strings.GetStringFromName("ruleView.accesskey"), context: this, get isOpen() isOpen(), show: this.openRuleView, hide: this.closeRuleView, onSelect: this.selectInRuleView, + onChanged: this.changeInRuleView, panel: null, unregister: this.destroyRuleView, sidebar: true, }; this.registerTool(this.ruleViewObject); }, @@ -344,16 +399,26 @@ InspectorUI.prototype = { } this.isDirty = this.store.getValue(this.winID, "isDirty"); } else { // First time inspecting, set state to no selection + live inspection. this.store.addStore(this.winID); this.store.setValue(this.winID, "selectedNode", null); this.store.setValue(this.winID, "inspecting", true); this.store.setValue(this.winID, "isDirty", this.isDirty); + + this.store.setValue(this.winID, "htmlPanelOpen", + Services.prefs.getBoolPref("devtools.inspector.htmlPanelOpen")); + + this.store.setValue(this.winID, "sidebarOpen", + Services.prefs.getBoolPref("devtools.inspector.sidebarOpen")); + + this.store.setValue(this.winID, "activeSidebar", + Services.prefs.getCharPref("devtools.inspector.activeSidebar")); + this.win.addEventListener("pagehide", this, true); } }, /** * Browse nodes according to the breadcrumbs layout, only for some specific * elements of the UI. */ @@ -395,16 +460,18 @@ InspectorUI.prototype = { */ closeInspectorUI: function IUI_closeInspectorUI(aKeepStore) { // if currently editing an attribute value, closing the // highlighter/HTML panel dismisses the editor if (this.treePanel && this.treePanel.editingContext) this.treePanel.closeEditor(); + this.treePanel.destroy(); + if (this.closing || !this.win || !this.browser) { return; } let winId = new String(this.winID); // retain this to notify observers. this.closing = true; this.toolbar.hidden = true; @@ -412,16 +479,17 @@ InspectorUI.prototype = { this.removeNavigationKeys(); this.progressListener.destroy(); delete this.progressListener; if (!aKeepStore) { this.store.deleteStore(this.winID); this.win.removeEventListener("pagehide", this, true); + this.clearPseudoClassLocks(); } else { // Update the store before closing. if (this.selection) { this.store.setValue(this.winID, "selectedNode", this.selection); } this.store.setValue(this.winID, "inspecting", this.inspecting); this.store.setValue(this.winID, "isDirty", this.isDirty); @@ -430,23 +498,22 @@ InspectorUI.prototype = { if (this.store.isEmpty()) { this.tabbrowser.tabContainer.removeEventListener("TabSelect", this, false); } this.chromeWin.removeEventListener("keypress", this, false); this.stopInspecting(); - this.saveToolState(this.winID); this.toolsDo(function IUI_toolsHide(aTool) { this.unregisterTool(aTool); }.bind(this)); // close the sidebar - this.hideSidebar(); + this._destroySidebar(); if (this.highlighter) { this.highlighter.destroy(); this.highlighter = null; } if (this.breadcrumbs) { this.breadcrumbs.destroy(); @@ -498,65 +565,108 @@ InspectorUI.prototype = { return; } this.inspectToolbutton.checked = false; this.inspecting = false; this.toolsDim(false); if (this.highlighter.getNode()) { - this.select(this.highlighter.getNode(), true, true, !aPreventScroll); + this.select(this.highlighter.getNode(), true, !aPreventScroll); } else { this.select(null, true, true); } this.highlighter.lock(); }, /** - * Select an object in the tree view. + * Select an object in the inspector. * @param aNode * node to inspect * @param forceUpdate * force an update? * @param aScroll boolean * scroll the tree panel? + * @param aFrom [optional] string + * which part of the UI the selection occured from */ - select: function IUI_select(aNode, forceUpdate, aScroll) + select: function IUI_select(aNode, forceUpdate, aScroll, aFrom) { // if currently editing an attribute value, using the // highlighter dismisses the editor if (this.treePanel && this.treePanel.editingContext) this.treePanel.closeEditor(); if (!aNode) aNode = this.defaultSelection; if (forceUpdate || aNode != this.selection) { + if (aFrom != "breadcrumbs") { + this.clearPseudoClassLocks(); + } + this.selection = aNode; if (!this.inspecting) { this.highlighter.highlight(this.selection); } } this.breadcrumbs.update(); this.chromeWin.Tilt.update(aNode); + this.treePanel.select(aNode, aScroll); this.toolsSelect(aScroll); }, + + /** + * Toggle the pseudo-class lock on the currently inspected element. If the + * pseudo-class is :hover or :active, that pseudo-class will also be toggled + * on every ancestor of the element, mirroring real :hover and :active + * behavior. + * + * @param aPseudo the pseudo-class lock to toggle, e.g. ":hover" + */ + togglePseudoClassLock: function IUI_togglePseudoClassLock(aPseudo) + { + if (DOMUtils.hasPseudoClassLock(this.selection, aPseudo)) { + this.breadcrumbs.nodeHierarchy.forEach(function(crumb) { + DOMUtils.removePseudoClassLock(crumb.node, aPseudo); + }); + } else { + let hierarchical = aPseudo == ":hover" || aPseudo == ":active"; + let node = this.selection; + do { + DOMUtils.addPseudoClassLock(node, aPseudo); + node = node.parentNode; + } while (hierarchical && node.parentNode) + } + this.nodeChanged(); + }, + + /** + * Clear all pseudo-class locks applied to elements in the node hierarchy + */ + clearPseudoClassLocks: function IUI_clearPseudoClassLocks() + { + this.breadcrumbs.nodeHierarchy.forEach(function(crumb) { + DOMUtils.clearPseudoClassLocks(crumb.node); + }); + }, /** * 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.invalidateSize(); + this.breadcrumbs.updateSelectors(); this.toolsOnChanged(aUpdater); }, ///////////////////////////////////////////////////////////////////////// //// Event Handling highlighterReady: function IUI_highlighterReady() { @@ -572,27 +682,42 @@ InspectorUI.prototype = { this.highlighter.addListener("unlocked", function() { self.startInspecting(); }); this.highlighter.addListener("nodeselected", function() { self.select(self.highlighter.getNode(), false, false); }); + this.highlighter.addListener("pseudoclasstoggled", function(aPseudo) { + self.togglePseudoClassLock(aPseudo); + }); + if (this.store.getValue(this.winID, "inspecting")) { this.startInspecting(); + this.highlighter.unlock(); + } else { + this.highlighter.lock(); } - this.restoreToolState(this.winID); + Services.obs.notifyObservers(null, INSPECTOR_NOTIFICATIONS.STATE_RESTORED, null); this.win.focus(); + this.highlighter.highlight(); + + if (this.store.getValue(this.winID, "htmlPanelOpen")) { + this.treePanel.open(); + } + + if (this.store.getValue(this.winID, "sidebarOpen")) { + this.showSidebar(); + } + Services.obs.notifyObservers({wrappedJSObject: this}, INSPECTOR_NOTIFICATIONS.OPENED, null); - - this.highlighter.highlight(); }, /** * Main callback handler for events. * * @param event * The event to be handled. */ @@ -709,16 +834,56 @@ InspectorUI.prototype = { this.highlighter.highlight(node, true); } event.preventDefault(); event.stopPropagation(); break; } }, + /** + * Copy the innerHTML of the selected Node to the clipboard. Called via the + * Inspector:CopyInner command. + */ + copyInnerHTML: function IUI_copyInnerHTML() + { + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper); + clipboard.copyString(this.selection.innerHTML); + }, + + /** + * Copy the outerHTML of the selected Node to the clipboard. Called via the + * Inspector:CopyOuter command. + */ + copyOuterHTML: function IUI_copyOuterHTML() + { + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper); + clipboard.copyString(this.selection.outerHTML); + }, + + /** + * Delete the selected node. Called via the Inspector:DeleteNode command. + */ + deleteNode: function IUI_deleteNode() + { + let selection = this.selection; + let parent = this.selection.parentNode; + + // remove the node from the treepanel + this.treePanel.deleteChildBox(selection); + + // remove the node from content + parent.removeChild(selection); + this.breadcrumbs.invalidateHierarchy(); + + // select the parent node in the highlighter, treepanel, breadcrumbs + this.inspectNode(parent); + }, ///////////////////////////////////////////////////////////////////////// //// CssRuleView methods /** * Is the cssRuleView open? */ isRuleViewOpen: function IUI_isRuleViewOpen() @@ -791,16 +956,25 @@ InspectorUI.prototype = { * Update the selected node in the Css Rule View. * @param {nsIDOMnode} the selected node. */ selectInRuleView: function IUI_selectInRuleView(aNode) { if (this.ruleView) this.ruleView.highlight(aNode); }, + + /** + * Update the rules for the current node in the Css Rule View. + */ + changeInRuleView: function IUI_selectInRuleView() + { + if (this.ruleView) + this.ruleView.nodeChanged(); + }, ruleViewChanged: function IUI_ruleViewChanged() { this.isDirty = true; this.nodeChanged(this.ruleViewObject); }, /** @@ -1082,16 +1256,18 @@ InspectorUI.prototype = { * Show the specified tool. * @param aTool Object (see comment for IUI_registerTool) */ toolShow: function IUI_toolShow(aTool) { let btn = this.chromeDoc.getElementById(this.getToolbarButtonId(aTool.id)); btn.setAttribute("checked", "true"); if (aTool.sidebar) { + Services.prefs.setCharPref("devtools.inspector.activeSidebar", aTool.id); + this.store.setValue(this.winID, "activeSidebar", aTool.id); this.sidebarDeck.selectedPanel = this.getToolIframe(aTool); this.sidebarTools.forEach(function(other) { if (other != aTool) this.chromeDoc.getElementById( this.getToolbarButtonId(other.id)).removeAttribute("checked"); }.bind(this)); } @@ -1176,67 +1352,16 @@ InspectorUI.prototype = { // the iframe. if (aRegObj.unregister) aRegObj.unregister.call(aRegObj.context); delete this.tools[aRegObj.id]; }, /** - * Save a list of open tools to the inspector store. - * - * @param aWinID The ID of the window used to save the associated tools - */ - saveToolState: function IUI_saveToolState(aWinID) - { - let openTools = {}; - this.toolsDo(function IUI_toolsSetId(aTool) { - if (aTool.isOpen) { - openTools[aTool.id] = true; - } - }); - this.store.setValue(aWinID, "openTools", openTools); - }, - - /** - * Restore tools previously save using saveToolState(). - * - * @param aWinID The ID of the window to which the associated tools are to be - * restored. - */ - restoreToolState: function IUI_restoreToolState(aWinID) - { - let openTools = this.store.getValue(aWinID, "openTools"); - let activeSidebarTool; - if (openTools) { - this.toolsDo(function IUI_toolsOnShow(aTool) { - if (aTool.id in openTools) { - if (aTool.sidebar && !this.isSidebarOpen) { - this.showSidebar(); - activeSidebarTool = aTool; - } - this.toolShow(aTool); - } - }.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? */ toolsSelect: function IUI_toolsSelect(aScroll) { let selection = this.selection; @@ -1249,33 +1374,33 @@ InspectorUI.prototype = { /** * Dim or undim each tool in the tools collection * @param aState true = dim, false = undim */ toolsDim: function IUI_toolsDim(aState) { this.toolsDo(function IUI_toolsDim(aTool) { - if (aTool.isOpen && "dim" in aTool) { + if ("dim" in aTool) { aTool.dim.call(aTool.context, aState); } }); }, /** * Notify registered tools of changes to the highlighted element. * * @param object aUpdater * The tool that triggered the update (if any), that tool's * onChanged will not be called. */ toolsOnChanged: function IUI_toolsChanged(aUpdater) { this.toolsDo(function IUI_toolsOnChanged(aTool) { - if (aTool.isOpen && ("onChanged" in aTool) && aTool != aUpdater) { + if (("onChanged" in aTool) && aTool != aUpdater) { aTool.onChanged.call(aTool.context); } }); }, /** * Loop through all registered tools and pass each into the provided function * @param aFunction The function to which each tool is to be passed @@ -1659,16 +1784,23 @@ HTMLBreadcrumbs.prototype = { { let text = aNode.tagName.toLowerCase(); if (aNode.id) { text += "#" + aNode.id; } for (let i = 0; i < aNode.classList.length; i++) { text += "." + aNode.classList[i]; } + for (let i = 0; i < PSEUDO_CLASSES.length; i++) { + let pseudo = PSEUDO_CLASSES[i]; + if (DOMUtils.hasPseudoClassLock(aNode, pseudo)) { + text += pseudo; + } + } + return text; }, /** * Build <label>s that represent the node: * <label class="inspector-breadcrumbs-tag">tagName</label> * <label class="inspector-breadcrumbs-id">#id</label> @@ -1684,29 +1816,38 @@ HTMLBreadcrumbs.prototype = { let tagLabel = this.IUI.chromeDoc.createElement("label"); tagLabel.className = "inspector-breadcrumbs-tag plain"; let idLabel = this.IUI.chromeDoc.createElement("label"); idLabel.className = "inspector-breadcrumbs-id plain"; let classesLabel = this.IUI.chromeDoc.createElement("label"); classesLabel.className = "inspector-breadcrumbs-classes plain"; + + let pseudosLabel = this.IUI.chromeDoc.createElement("label"); + pseudosLabel.className = "inspector-breadcrumbs-pseudo-classes plain"; tagLabel.textContent = aNode.tagName.toLowerCase(); idLabel.textContent = aNode.id ? ("#" + aNode.id) : ""; let classesText = ""; for (let i = 0; i < aNode.classList.length; i++) { classesText += "." + aNode.classList[i]; } classesLabel.textContent = classesText; + let pseudos = PSEUDO_CLASSES.filter(function(pseudo) { + return DOMUtils.hasPseudoClassLock(aNode, pseudo); + }, this); + pseudosLabel.textContent = pseudos.join(""); + fragment.appendChild(tagLabel); fragment.appendChild(idLabel); fragment.appendChild(classesLabel); + fragment.appendChild(pseudosLabel); return fragment; }, /** * Open the sibling menu. * * @param aButton the button representing the node. @@ -1736,17 +1877,17 @@ HTMLBreadcrumbs.prototype = { item.setAttribute("checked", "true"); } item.setAttribute("type", "radio"); item.setAttribute("label", this.prettyPrintNodeAsText(nodes[i])); item.onmouseup = (function(aNode) { return function() { - inspector.select(aNode, true, true); + inspector.select(aNode, true, true, "breadcrumbs"); } })(nodes[i]); fragment.appendChild(item); } } this.menu.appendChild(fragment); this.menu.openPopup(aButton, "before_start", 0, 0, true, false); @@ -1890,17 +2031,17 @@ HTMLBreadcrumbs.prototype = { let inspector = this.IUI; button.appendChild(this.prettyPrintNodeAsXUL(aNode)); button.className = "inspector-breadcrumbs-button"; button.setAttribute("tooltiptext", this.prettyPrintNodeAsText(aNode)); button.onBreadcrumbsClick = function onBreadcrumbsClick() { inspector.stopInspecting(); - inspector.select(aNode, true, true); + inspector.select(aNode, true, true, "breadcrumbs"); }; button.onclick = (function _onBreadcrumbsRightClick(aEvent) { if (aEvent.button == 2) { this.openSiblingMenu(button, aNode); } }).bind(this); @@ -2005,16 +2146,30 @@ HTMLBreadcrumbs.prototype = { scroll: function BC_scroll() { // FIXME bug 684352: make sure its immediate neighbors are visible too. let scrollbox = this.container; let element = this.nodeHierarchy[this.currentIndex].button; scrollbox.ensureElementIsVisible(element); }, + + updateSelectors: function BC_updateSelectors() + { + for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) { + let crumb = this.nodeHierarchy[i]; + let button = crumb.button; + + while(button.hasChildNodes()) { + button.removeChild(button.firstChild); + } + button.appendChild(this.prettyPrintNodeAsXUL(crumb.node)); + button.setAttribute("tooltiptext", this.prettyPrintNodeAsText(crumb.node)); + } + }, /** * Update the breadcrumbs display when a new node is selected. */ update: function BC_update() { this.menu.hidePopup(); @@ -2046,16 +2201,18 @@ HTMLBreadcrumbs.prototype = { idx = this.indexOf(selection); this.setCursor(idx); } // Add the first child of the very last node of the breadcrumbs if possible. this.ensureFirstChild(); // Make sure the selected node and its neighbours are visible. this.scroll(); + + this.updateSelectors(); }, } ///////////////////////////////////////////////////////////////////////// //// Initializers XPCOMUtils.defineLazyGetter(InspectorUI.prototype, "strings", @@ -2065,8 +2222,11 @@ XPCOMUtils.defineLazyGetter(InspectorUI. }); XPCOMUtils.defineLazyGetter(this, "StyleInspector", function () { var obj = {}; Cu.import("resource:///modules/devtools/StyleInspector.jsm", obj); return obj.StyleInspector; }); +XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () { + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); +});
--- a/browser/devtools/highlighter/test/Makefile.in +++ b/browser/devtools/highlighter/test/Makefile.in @@ -65,16 +65,19 @@ include $(topsrcdir)/config/rules.mk 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_invalidate.js \ + browser_inspector_sidebarstate.js \ + browser_inspector_treePanel_menu.js \ + browser_inspector_pseudoclass_lock.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_editor.js +++ b/browser/devtools/highlighter/test/browser_inspector_editor.js @@ -29,17 +29,17 @@ function setupEditorTests() Services.obs.addObserver(setupHTMLPanel, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false); InspectorUI.toggleInspectorUI(); } function setupHTMLPanel() { Services.obs.removeObserver(setupHTMLPanel, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED); Services.obs.addObserver(runEditorTests, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false); - InspectorUI.toolShow(InspectorUI.treePanel.registrationObject); + InspectorUI.toggleHTMLPanel(); } function runEditorTests() { Services.obs.removeObserver(runEditorTests, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY); InspectorUI.stopInspecting(); InspectorUI.inspectNode(doc.body, true);
new file mode 100644 --- /dev/null +++ b/browser/devtools/highlighter/test/browser_inspector_pseudoclass_lock.js @@ -0,0 +1,154 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +let DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); + +let doc; +let div; + +let pseudo = ":hover"; + +function test() +{ + waitForExplicitFinish(); + ignoreAllUncaughtExceptions(); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function() { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + doc = content.document; + waitForFocus(createDocument, content); + }, true); + + content.location = "data:text/html,pseudo-class lock tests"; +} + +function createDocument() +{ + div = doc.createElement("div"); + div.textContent = "test div"; + + let head = doc.getElementsByTagName('head')[0]; + let style = doc.createElement('style'); + let rules = doc.createTextNode('div { color: red; } div:hover { color: blue; }'); + + style.appendChild(rules); + head.appendChild(style); + doc.body.appendChild(div); + + setupTests(); +} + +function setupTests() +{ + Services.obs.addObserver(selectNode, + InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false); + InspectorUI.openInspectorUI(); +} + +function selectNode() +{ + Services.obs.removeObserver(selectNode, + InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED); + + executeSoon(function() { + InspectorUI.highlighter.addListener("nodeselected", openRuleView); + InspectorUI.inspectNode(div); + }); +} + +function openRuleView() +{ + Services.obs.addObserver(performTests, + InspectorUI.INSPECTOR_NOTIFICATIONS.RULEVIEWREADY, false); + + InspectorUI.showSidebar(); + InspectorUI.openRuleView(); +} + +function performTests() +{ + Services.obs.removeObserver(performTests, + InspectorUI.INSPECTOR_NOTIFICATIONS.RULEVIEWREADY); + + InspectorUI.highlighter.removeListener("nodeselected", performTests); + + // toggle the class + InspectorUI.highlighter.pseudoClassLockToggled(pseudo); + + testAdded(); + + // toggle the lock off + InspectorUI.highlighter.pseudoClassLockToggled(pseudo); + + testRemoved(); + testRemovedFromUI(); + + // toggle it back on + InspectorUI.highlighter.pseudoClassLockToggled(pseudo); + + // close the inspector + Services.obs.addObserver(testInspectorClosed, + InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false); + InspectorUI.closeInspectorUI(); +} + +function testAdded() +{ + // lock is applied to it and ancestors + let node = div; + do { + is(DOMUtils.hasPseudoClassLock(node, pseudo), true, + "pseudo-class lock has been applied"); + node = node.parentNode; + } while (node.parentNode) + + // infobar selector contains pseudo-class + let pseudoClassesBox = document.getElementById("highlighter-nodeinfobar-pseudo-classes"); + is(pseudoClassesBox.textContent, pseudo, "pseudo-class in infobar selector"); + + // ruleview contains pseudo-class rule + is(InspectorUI.ruleView.element.children.length, 3, + "rule view is showing 3 rules for pseudo-class locked div"); + + is(InspectorUI.ruleView.element.children[1]._ruleEditor.rule.selectorText, + "div:hover", "rule view is showing " + pseudo + " rule"); +} + +function testRemoved() +{ + // lock removed from node and ancestors + let node = div; + do { + is(DOMUtils.hasPseudoClassLock(node, pseudo), false, + "pseudo-class lock has been removed"); + node = node.parentNode; + } while (node.parentNode) +} + +function testRemovedFromUI() +{ + // infobar selector doesn't contain pseudo-class + let pseudoClassesBox = document.getElementById("highlighter-nodeinfobar-pseudo-classes"); + is(pseudoClassesBox.textContent, "", "pseudo-class removed from infobar selector"); + + // ruleview no longer contains pseudo-class rule + is(InspectorUI.ruleView.element.children.length, 2, + "rule view is showing 2 rules after removing lock"); +} + +function testInspectorClosed() +{ + Services.obs.removeObserver(testInspectorClosed, + InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED); + + testRemoved(); + + finishUp(); +} + +function finishUp() +{ + doc = div = null; + gBrowser.removeCurrentTab(); + finish(); +}
--- a/browser/devtools/highlighter/test/browser_inspector_registertools.js +++ b/browser/devtools/highlighter/test/browser_inspector_registertools.js @@ -143,55 +143,16 @@ function startToolTests(evt) InspectorUI.toolShow(tool1); InspectorUI.toolShow(tool3); info("Checking panel states 4"); ok(tool1.isOpen, "Panel 1 is open"); ok(!tool2.isOpen, "Panel 2 is closed"); ok(tool3.isOpen, "Panel 3 is open"); - gBrowser.selectedTab = gBrowser.addTab(); - gBrowser.selectedBrowser.addEventListener("load", function() { - gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); - waitForFocus(testSecondTab, content); - }, true); - - content.location = "data:text/html,registertool new tab test for inspector"; -} - -function testSecondTab() -{ - info("Opened second tab"); - info("Checking panel states 5"); - - let tools = InspectorUI.tools; - ok(!(tool1 in tools), "Panel 1 not in tools"); - ok(!(tool2 in tools), "Panel 2 not in tools"); - ok(!(tool3 in tools), "Panel 3 not in tools"); - - info("Closing current tab"); - Services.obs.addObserver(testOriginalTab, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false); - gBrowser.removeCurrentTab(); -} - -function testOriginalTab() -{ - Services.obs.removeObserver(testOriginalTab, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED); - info("Checking panel states 6"); - - info("Tools: " + InspectorUI.tools); - // reacquaint ourselves with our tools - tool1 = InspectorUI.tools["tool_1"]; - tool2 = InspectorUI.tools["tool_2"]; - tool3 = InspectorUI.tools["tool_3"]; - - ok(tool1.isOpen, "Panel 1 is open after reactivation"); - ok(!tool2.isOpen, "Panel 2 is closed after reactivation"); - ok(tool3.isOpen, "Panel 3 is open after reactivation"); - Services.obs.addObserver(unregisterTools, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false); InspectorUI.closeInspectorUI(true); } function unregisterTools() { Services.obs.removeObserver(unregisterTools, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED); let tools = InspectorUI.tools;
--- a/browser/devtools/highlighter/test/browser_inspector_ruleviewstore.js +++ b/browser/devtools/highlighter/test/browser_inspector_ruleviewstore.js @@ -123,18 +123,18 @@ function inspectorFocusTab1() } function ruleViewOpened2() { let prop = InspectorUI.ruleView._elementStyle.rules[0].textProps[0]; is(prop.name, "background-color", "First prop is the background color prop."); ok(!prop.enabled, "First prop should be disabled."); + InspectorUI.closeInspectorUI(); gBrowser.removeCurrentTab(); - InspectorUI.closeInspectorUI(); finish(); } function test() { waitForExplicitFinish(); tab1 = gBrowser.addTab();
new file mode 100644 --- /dev/null +++ b/browser/devtools/highlighter/test/browser_inspector_sidebarstate.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +let doc; + +function createDocument() +{ + doc.body.innerHTML = '<h1>Sidebar state test</h1>'; + doc.title = "Sidebar State Test"; + + // Open the sidebar and wait for the default view (the rule view) to show. + Services.obs.addObserver(inspectorRuleViewOpened, + InspectorUI.INSPECTOR_NOTIFICATIONS.RULEVIEWREADY, false); + + InspectorUI.openInspectorUI(); + InspectorUI.showSidebar(); +} + +function inspectorRuleViewOpened() +{ + Services.obs.removeObserver(inspectorRuleViewOpened, + InspectorUI.INSPECTOR_NOTIFICATIONS.RULEVIEWREADY); + is(InspectorUI.activeSidebarPanel, "ruleview", "Rule View is selected by default"); + + // Select the computed view and turn off the inspector. + InspectorUI.activateSidebarPanel("styleinspector"); + + Services.obs.addObserver(inspectorClosed, + InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false); + InspectorUI.closeInspectorUI(); +} + +function inspectorClosed() +{ + // Reopen the inspector, expect the computed view to be loaded. + Services.obs.removeObserver(inspectorClosed, + InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED); + + Services.obs.addObserver(computedViewPopulated, + "StyleInspector-populated", false); + + InspectorUI.openInspectorUI(); +} + +function computedViewPopulated() +{ + Services.obs.removeObserver(computedViewPopulated, + "StyleInspector-populated"); + is(InspectorUI.activeSidebarPanel, "styleinspector", "Computed view is selected by default."); + + finishTest(); +} + + +function finishTest() +{ + InspectorUI.closeInspectorUI(); + gBrowser.removeCurrentTab(); + finish(); +} + +function test() +{ + waitForExplicitFinish(); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function() { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + doc = content.document; + waitForFocus(createDocument, content); + }, true); + + content.location = "data:text/html,basic tests for inspector"; +} +
--- a/browser/devtools/highlighter/test/browser_inspector_tab_switch.js +++ b/browser/devtools/highlighter/test/browser_inspector_tab_switch.js @@ -91,16 +91,17 @@ function inspectorTabOpen2() ok(!InspectorUI.treePanel, "Inspector Tree Panel is closed"); ok(!InspectorUI.isSidebarOpen, "Inspector Sidebar is not open"); is(InspectorUI.store.length, 1, "Inspector.store.length = 1"); // Activate the inspector again. executeSoon(function() { Services.obs.addObserver(inspectorUIOpen2, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false); + clearUserPrefs(); InspectorUI.openInspectorUI(); }); } function inspectorUIOpen2() { Services.obs.removeObserver(inspectorUIOpen2, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false); @@ -131,34 +132,34 @@ function inspectorFocusTab1() ok(InspectorUI.inspecting, "Inspector is highlighting"); ok(!InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is not open"); is(InspectorUI.store.length, 2, "Inspector.store.length = 2"); is(InspectorUI.selection, div, "selection matches the div element"); Services.obs.addObserver(inspectorOpenTreePanelTab1, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false); - InspectorUI.treePanel.open(); + InspectorUI.toggleHTMLPanel(); } function inspectorOpenTreePanelTab1() { Services.obs.removeObserver(inspectorOpenTreePanelTab1, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY); ok(InspectorUI.inspecting, "Inspector is highlighting"); ok(InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is open"); is(InspectorUI.store.length, 2, "Inspector.store.length = 2"); is(InspectorUI.selection, div, "selection matches the div element"); Services.obs.addObserver(inspectorSidebarStyleView1, "StyleInspector-opened", false); executeSoon(function() { InspectorUI.showSidebar(); - InspectorUI.toolShow(InspectorUI.stylePanel.registrationObject); + InspectorUI.activateSidebarPanel("styleinspector"); }); } function inspectorSidebarStyleView1() { Services.obs.removeObserver(inspectorSidebarStyleView1, "StyleInspector-opened"); ok(InspectorUI.isSidebarOpen, "Inspector Sidebar is open"); ok(InspectorUI.stylePanel, "Inspector Has a Style Panel Instance");
new file mode 100644 --- /dev/null +++ b/browser/devtools/highlighter/test/browser_inspector_treePanel_menu.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + + +function test() { + + waitForExplicitFinish(); + + let doc; + let node1; + let div; + + function createDocument() { + div = doc.createElement("div"); + let h1 = doc.createElement("h1"); + let p1 = doc.createElement("p"); + let p2 = doc.createElement("p"); + doc.title = "Inspector Tree Menu Test"; + h1.textContent = "Inspector Tree Menu Test"; + p1.textContent = "This is some example text"; + div.appendChild(h1); + div.appendChild(p1); + doc.body.appendChild(div); + node1 = p1; + setupTest(); + } + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function onload() { + gBrowser.selectedBrowser.removeEventListener("load", onload, true); + doc = content.document; + waitForFocus(createDocument, content); + }, true); + + content.location = content.location = "data:text/html,basic tests for inspector";; + + function setupTest() { + Services.obs.addObserver(runTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false); + InspectorUI.toggleInspectorUI(); + } + + function runTests() { + Services.obs.removeObserver(runTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED); + Services.obs.addObserver(testCopyInnerMenu, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false); + InspectorUI.stopInspecting(); + InspectorUI.inspectNode(node1, true); + InspectorUI.treePanel.open(); + } + + function testCopyInnerMenu() { + let copyInner = document.getElementById("inspectorHTMLCopyInner"); + ok(copyInner, "the popup menu has a copy inner html menu item"); + + waitForClipboard("This is some example text", + function() { copyInner.doCommand(); }, + testCopyOuterMenu, testCopyOuterMenu); + } + + function testCopyOuterMenu() { + let copyOuter = document.getElementById("inspectorHTMLCopyOuter"); + ok(copyOuter, "the popup menu has a copy outer html menu item"); + + waitForClipboard("<p>This is some example text</p>", + function() { copyOuter.doCommand(); }, + testDeleteNode, testDeleteNode); + } + + function testDeleteNode() { + let deleteNode = document.getElementById("inspectorHTMLDelete"); + ok(deleteNode, "the popup menu has a delete menu item"); + + InspectorUI.highlighter.addListener("nodeselected", deleteTest); + + let commandEvent = document.createEvent("XULCommandEvent"); + commandEvent.initCommandEvent("command", true, true, window, 0, false, false, + false, false, null); + deleteNode.dispatchEvent(commandEvent); + } + + function deleteTest() { + InspectorUI.highlighter.removeListener("nodeSelected", deleteTest); + Services.obs.addObserver(finishUp, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false); + is(InspectorUI.selection, div, "parent node selected"); + let p = doc.querySelector("P"); + is(p, null, "node deleted"); + executeSoon(function() { + InspectorUI.closeInspectorUI(); + }); + } + + function finishUp() { + Services.obs.removeObserver(finishUp, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED); + doc = node1 = div = null; + gBrowser.removeCurrentTab(); + finish(); + } +}
--- a/browser/devtools/highlighter/test/head.js +++ b/browser/devtools/highlighter/test/head.js @@ -36,16 +36,26 @@ * * ***** END LICENSE BLOCK ***** */ const Cu = Components.utils; let tempScope = {}; Cu.import("resource:///modules/devtools/LayoutHelpers.jsm", tempScope); let LayoutHelpers = tempScope.LayoutHelpers; +// Clear preferences that may be set during the course of tests. +function clearUserPrefs() +{ + Services.prefs.clearUserPref("devtools.inspector.htmlPanelOpen"); + Services.prefs.clearUserPref("devtools.inspector.sidebarOpen"); + Services.prefs.clearUserPref("devtools.inspector.activeSidebar"); +} + +registerCleanupFunction(clearUserPrefs); + function isHighlighting() { let veil = InspectorUI.highlighter.veilTransparentBox; return !(veil.style.visibility == "hidden"); } function getHighlitNode() { @@ -73,8 +83,9 @@ function getHighlitNode() 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/scratchpad/scratchpad.js +++ b/browser/devtools/scratchpad/scratchpad.js @@ -436,28 +436,29 @@ var Scratchpad = { if (!error) { this.writeAsComment(result); } else { this.writeAsErrorComment(error); } }, /** - * Write out a value at the current insertion point as a block comment + * Write out a value at the next line from the current insertion point. + * The comment block will always be preceded by a newline character. * @param object aValue * The Object to write out as a string */ writeAsComment: function SP_writeAsComment(aValue) { let selection = this.getSelectionRange(); let insertionPoint = selection.start != selection.end ? selection.end : // after selected text this.editor.getCharCount(); // after text end - let newComment = "/*\n" + aValue + "\n*/"; + let newComment = "\n/*\n" + aValue + "\n*/"; this.setText(newComment, insertionPoint, insertionPoint); // Select the new comment. this.selectRange(insertionPoint, insertionPoint + newComment.length); }, /**
--- a/browser/devtools/scratchpad/test/browser_scratchpad_bug690552_display_outputs_errors.js +++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug690552_display_outputs_errors.js @@ -16,17 +16,17 @@ function test() "comments for 'display' and not sent to the console in Scratchpad"; } function runTests() { var scratchpad = gScratchpadWindow.Scratchpad; var message = "\"Hello World!\"" - var openComment = "/*\n"; + var openComment = "\n/*\n"; var closeComment = "\n*/"; var error = "throw new Error(\"Ouch!\")"; let messageArray = {}; let count = {}; scratchpad.setText(message); scratchpad.display(); is(scratchpad.getText(),
--- a/browser/devtools/scratchpad/test/browser_scratchpad_bug_679467_falsy.js +++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_679467_falsy.js @@ -25,30 +25,30 @@ function testFalsy() finish(); } function verifyFalsies(sp) { sp.setText("undefined"); sp.display(); - is(sp.selectedText, "/*\nundefined\n*/", "'undefined' is displayed"); + is(sp.selectedText, "\n/*\nundefined\n*/", "'undefined' is displayed"); sp.setText("false"); sp.display(); - is(sp.selectedText, "/*\nfalse\n*/", "'false' is displayed"); + is(sp.selectedText, "\n/*\nfalse\n*/", "'false' is displayed"); sp.setText("0"); sp.display(); - is(sp.selectedText, "/*\n0\n*/", "'0' is displayed"); + is(sp.selectedText, "\n/*\n0\n*/", "'0' is displayed"); sp.setText("null"); sp.display(); - is(sp.selectedText, "/*\nnull\n*/", "'null' is displayed"); + is(sp.selectedText, "\n/*\nnull\n*/", "'null' is displayed"); sp.setText("NaN"); sp.display(); - is(sp.selectedText, "/*\nNaN\n*/", "'NaN' is displayed"); + is(sp.selectedText, "\n/*\nNaN\n*/", "'NaN' is displayed"); sp.setText("''"); sp.display(); - is(sp.selectedText, "/*\n\n*/", "empty string is displayed"); + is(sp.selectedText, "\n/*\n\n*/", "empty string is displayed"); }
--- a/browser/devtools/scratchpad/test/browser_scratchpad_execute_print.js +++ b/browser/devtools/scratchpad/test/browser_scratchpad_execute_print.js @@ -35,23 +35,23 @@ function runTests() is(content.wrappedJSObject.foobarBug636725, 2, "run() updated window.foobarBug636725"); sp.display(); is(content.wrappedJSObject.foobarBug636725, 3, "display() updated window.foobarBug636725"); - is(sp.getText(), "++window.foobarBug636725/*\n3\n*/", + is(sp.getText(), "++window.foobarBug636725\n/*\n3\n*/", "display() shows evaluation result in the textbox"); - is(sp.selectedText, "/*\n3\n*/", "selectedText is correct"); + is(sp.selectedText, "\n/*\n3\n*/", "selectedText is correct"); let selection = sp.getSelectionRange(); is(selection.start, 24, "selection.start is correct"); - is(selection.end, 31, "selection.end is correct"); + is(selection.end, 32, "selection.end is correct"); // Test selection run() and display(). sp.setText("window.foobarBug636725 = 'a';\n" + "window.foobarBug636725 = 'b';"); sp.selectRange(1, 2); @@ -89,26 +89,26 @@ function runTests() sp.selectRange(0, 22); sp.display(); is(content.wrappedJSObject.foobarBug636725, "a", "display() worked for the selected range"); is(sp.getText(), "window.foobarBug636725" + - "/*\na\n*/" + + "\n/*\na\n*/" + " = 'c';\n" + "window.foobarBug636725 = 'b';", "display() shows evaluation result in the textbox"); - is(sp.selectedText, "/*\na\n*/", "selectedText is correct"); + is(sp.selectedText, "\n/*\na\n*/", "selectedText is correct"); selection = sp.getSelectionRange(); is(selection.start, 22, "selection.start is correct"); - is(selection.end, 29, "selection.end is correct"); + is(selection.end, 30, "selection.end is correct"); sp.deselect(); ok(!sp.selectedText, "selectedText is empty"); selection = sp.getSelectionRange(); is(selection.start, selection.end, "deselect() works");
--- a/browser/devtools/styleinspector/CssRuleView.jsm +++ b/browser/devtools/styleinspector/CssRuleView.jsm @@ -111,17 +111,17 @@ function ElementStyle(aElement, aStore) let doc = aElement.ownerDocument; // To figure out how shorthand properties are interpreted by the // engine, we will set properties on a dummy element and observe // how their .style attribute reflects them as computed values. this.dummyElement = doc.createElementNS(this.element.namespaceURI, this.element.tagName); - this._populate(); + this.populate(); } // We're exporting _ElementStyle for unit tests. var _ElementStyle = ElementStyle; ElementStyle.prototype = { // The element we're looking at. element: null, @@ -142,17 +142,17 @@ ElementStyle.prototype = { this.onChanged(); } }, /** * Refresh the list of rules to be displayed for the active element. * Upon completion, this.rules[] will hold a list of Rule objects. */ - _populate: function ElementStyle_populate() + populate: function ElementStyle_populate() { this.rules = []; let element = this.element; do { this._addElementRules(element); } while ((element = element.parentNode) && element.nodeType === Ci.nsIDOMNode.ELEMENT_NODE); @@ -708,25 +708,43 @@ CssRuleView.prototype = { this._elementStyle = new ElementStyle(aElement, this.store); this._elementStyle.onChanged = function() { this._changed(); }.bind(this); this._createEditors(); }, + + /** + * Update the rules for the currently highlighted element. + */ + nodeChanged: function CssRuleView_nodeChanged() + { + this._clearRules(); + this._elementStyle.populate(); + this._createEditors(); + }, + + /** + * Clear the rules. + */ + _clearRules: function CssRuleView_clearRules() + { + while (this.element.hasChildNodes()) { + this.element.removeChild(this.element.lastChild); + } + }, /** * Clear the rule view. */ clear: function CssRuleView_clear() { - while (this.element.hasChildNodes()) { - this.element.removeChild(this.element.lastChild); - } + this._clearRules(); this._viewedElement = null; this._elementStyle = null; }, /** * Called when the user has made changes to the ElementStyle. * Emits an event that clients can listen to. */
--- a/browser/devtools/styleinspector/StyleInspector.jsm +++ b/browser/devtools/styleinspector/StyleInspector.jsm @@ -115,16 +115,21 @@ StyleInspector.prototype = { this.iframe.getAttribute("src") == "chrome://browser/content/devtools/csshtmltree.xul") { let selectedNode = this.selectedNode || null; this.cssHtmlTree = new CssHtmlTree(this); this.cssLogic.highlight(selectedNode); this.cssHtmlTree.highlight(selectedNode); this.iframe.removeEventListener("load", boundIframeOnLoad, true); this.iframeReady = true; + + // Now that we've loaded, select any node we were previously asked + // to show. + this.selectNode(this.selectedNode); + Services.obs.notifyObservers(null, "StyleInspector-opened", null); } }.bind(this); this.iframe = this.IUI.getToolIframe(this.registrationObject); this.iframe.addEventListener("load", boundIframeOnLoad, true); }, @@ -210,21 +215,26 @@ StyleInspector.prototype = { }, /** * Check if the style inspector is open. * @returns boolean */ isOpen: function SI_isOpen() { - return this.openDocked ? this.iframeReady && this.IUI.isSidebarOpen && + return this.openDocked ? this.IUI.isSidebarOpen && (this.IUI.sidebarDeck.selectedPanel == this.iframe) : this.panel && this.panel.state && this.panel.state == "open"; }, + isLoaded: function SI_isLoaded() + { + return this.openDocked ? this.iframeReady : this.iframeReady && this.panelReady; + }, + /** * Select from Path (via CssHtmlTree_pathClick) * @param aNode The node to inspect. */ selectFromPath: function SI_selectFromPath(aNode) { if (this.IUI && this.IUI.selection) { if (aNode != this.IUI.selection) { @@ -237,28 +247,28 @@ StyleInspector.prototype = { /** * Select a node to inspect in the Style Inspector panel * @param aNode The node to inspect. */ selectNode: function SI_selectNode(aNode) { this.selectedNode = aNode; - if (this.isOpen() && !this.dimmed) { + if (this.isLoaded() && !this.dimmed) { this.cssLogic.highlight(aNode); this.cssHtmlTree.highlight(aNode); } }, /** * Update the display for the currently-selected node. */ updateNode: function SI_updateNode() { - if (this.isOpen() && !this.dimmed) { + if (this.isLoaded() && !this.dimmed) { this.cssLogic.highlight(this.selectedNode); this.cssHtmlTree.refreshPanel(); } }, /** * Dim or undim a panel by setting or removing a dimmed attribute. * @param aState
--- a/browser/devtools/tilt/Tilt.jsm +++ b/browser/devtools/tilt/Tilt.jsm @@ -122,76 +122,81 @@ Tilt.prototype = { return; } // create a visualizer instance for the current tab this.visualizers[id] = new TiltVisualizer({ chromeWindow: this.chromeWindow, contentWindow: this.chromeWindow.gBrowser.selectedBrowser.contentWindow, parentNode: this.chromeWindow.gBrowser.selectedBrowser.parentNode, - requestAnimationFrame: this.chromeWindow.mozRequestAnimationFrame, notifications: this.NOTIFICATIONS }); // make sure the visualizer object was initialized properly if (!this.visualizers[id].isInitialized()) { this.destroy(id); return; } Services.obs.notifyObservers(null, TILT_NOTIFICATIONS.INITIALIZING, null); }, /** - * Destroys a specific instance of the visualizer. + * Starts destroying a specific instance of the visualizer. * * @param {String} aId * the identifier of the instance in the visualizers array * @param {Boolean} aAnimateFlag * optional, set to true to display a destruction transition */ destroy: function T_destroy(aId, aAnimateFlag) { - // if the visualizer is already destroyed, don't do anything - if (!this.visualizers[aId]) { + // if the visualizer is destroyed or destroying, don't do anything + if (!this.visualizers[aId] || this._isDestroying) { + return; + } + this._isDestroying = true; + + let controller = this.visualizers[aId].controller; + let presenter = this.visualizers[aId].presenter; + + let content = presenter.contentWindow; + let pageXOffset = content.pageXOffset * presenter.transforms.zoom; + let pageYOffset = content.pageYOffset * presenter.transforms.zoom; + TiltUtils.setDocumentZoom(this.chromeWindow, presenter.transforms.zoom); + + // if we're not doing any outro animation, just finish destruction directly + if (!aAnimateFlag) { + this._finish(aId); return; } - if (!this.isDestroying) { - this.isDestroying = true; + // otherwise, trigger the outro animation and notify necessary observers + Services.obs.notifyObservers(null, TILT_NOTIFICATIONS.DESTROYING, null); - let finalize = function T_finalize(aId) { - this.visualizers[aId].removeOverlay(); - this.visualizers[aId].cleanup(); - this.visualizers[aId] = null; - - this.isDestroying = false; - this.chromeWindow.gBrowser.selectedBrowser.focus(); - Services.obs.notifyObservers(null, TILT_NOTIFICATIONS.DESTROYED, null); - }; + controller.removeEventListeners(); + controller.arcball.reset([-pageXOffset, -pageYOffset]); + presenter.executeDestruction(this._finish.bind(this, aId)); + }, - if (!aAnimateFlag) { - finalize.call(this, aId); - return; - } - - let controller = this.visualizers[aId].controller; - let presenter = this.visualizers[aId].presenter; + /** + * Finishes detroying a specific instance of the visualizer. + * + * @param {String} aId + * the identifier of the instance in the visualizers array + */ + _finish: function T__finish(aId) + { + this.visualizers[aId].removeOverlay(); + this.visualizers[aId].cleanup(); + this.visualizers[aId] = null; - let content = presenter.contentWindow; - let pageXOffset = content.pageXOffset * presenter.transforms.zoom; - let pageYOffset = content.pageYOffset * presenter.transforms.zoom; - - Services.obs.notifyObservers(null, TILT_NOTIFICATIONS.DESTROYING, null); - TiltUtils.setDocumentZoom(this.chromeWindow, presenter.transforms.zoom); - - controller.removeEventListeners(); - controller.arcball.reset([-pageXOffset, -pageYOffset]); - presenter.executeDestruction(finalize.bind(this, aId)); - } + this._isDestroying = false; + this.chromeWindow.gBrowser.selectedBrowser.focus(); + Services.obs.notifyObservers(null, TILT_NOTIFICATIONS.DESTROYED, null); }, /** * Handles any supplementary post-initialization work, done immediately * after a TILT_NOTIFICATIONS.INITIALIZING notification. */ _whenInitializing: function T__whenInitializing() { @@ -281,26 +286,28 @@ Tilt.prototype = { this.chromeWindow.InspectorUI.INSPECTOR_NOTIFICATIONS.DESTROYED, false); this.chromeWindow.gBrowser.tabContainer.addEventListener("TabSelect", this._onTabSelect.bind(this), false); // FIXME: this shouldn't be done here, see bug #705131 let onOpened = function() { - if (this.currentInstance) { - this.chromeWindow.InspectorUI.stopInspecting(); - this.inspectButton.disabled = true; - this.highlighterContainer.style.display = "none"; + if (this.inspector && this.highlighter && this.currentInstance) { + this.inspector.stopInspecting(); + this.inspector.inspectToolbutton.disabled = true; + this.highlighter.hide(); } }.bind(this); let onClosed = function() { - this.inspectButton.disabled = false; - this.highlighterContainer.style.display = ""; + if (this.inspector && this.highlighter) { + this.inspector.inspectToolbutton.disabled = false; + this.highlighter.show(); + } }.bind(this); Services.obs.addObserver(onOpened, this.chromeWindow.InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false); Services.obs.addObserver(onClosed, this.chromeWindow.InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false); Services.obs.addObserver(onOpened, TILT_NOTIFICATIONS.INITIALIZING, false); @@ -333,36 +340,31 @@ Tilt.prototype = { * Gets the visualizer instance for the current tab. */ get currentInstance() { return this.visualizers[this.currentWindowId]; }, /** + * Gets the current InspectorUI instance. + */ + get inspector() + { + return this.chromeWindow.InspectorUI; + }, + + /** + * Gets the current Highlighter instance from the InspectorUI. + */ + get highlighter() + { + return this.inspector.highlighter; + }, + + /** * Gets the Tilt button in the Inspector toolbar. */ get tiltButton() { - return this.chromeWindow.document.getElementById( - "inspector-3D-button"); - }, - - /** - * Gets the Inspect button in the Inspector toolbar. - * FIXME: this shouldn't be needed here, remove after bug #705131 - */ - get inspectButton() - { - return this.chromeWindow.document.getElementById( - "inspector-inspect-toolbutton"); - }, - - /** - * Gets the Highlighter contaniner stack. - * FIXME: this shouldn't be needed here, remove after bug #705131 - */ - get highlighterContainer() - { - return this.chromeWindow.document.getElementById( - "highlighter-container"); + return this.chromeWindow.document.getElementById("inspector-3D-button"); } };
--- a/browser/devtools/tilt/TiltGL.jsm +++ b/browser/devtools/tilt/TiltGL.jsm @@ -87,16 +87,18 @@ TiltGL.Renderer = function TGL_Renderer( this.context.clearColor(0, 0, 0, 0); this.context.clearDepth(1); /** * Variables representing the current framebuffer width and height. */ this.width = aCanvas.width; this.height = aCanvas.height; + this.initialWidth = this.width; + this.initialHeight = this.height; /** * The current model view matrix. */ this.mvMatrix = mat4.identity(mat4.create()); /** * The current projection matrix. @@ -859,32 +861,40 @@ TiltGL.Program.prototype = { let utils = TiltGL.ProgramUtils; // check if the program wasn't already active if (utils._activeProgram !== id) { utils._activeProgram = id; // use the the program if it wasn't already set this._context.useProgram(this._ref); - - // check if the required vertex attributes aren't already set - if (utils._enabledAttributes < this._attributes.length) { - utils._enabledAttributes = this._attributes.length; + this.cleanupVertexAttrib(); - // enable any necessary vertex attributes using the cache - for (let i in this._attributes) { - if (this._attributes.hasOwnProperty(i)) { - this._context.enableVertexAttribArray(this._attributes[i]); - } - } + // enable any necessary vertex attributes using the cache + for each (let attribute in this._attributes) { + this._context.enableVertexAttribArray(attribute); + utils._enabledAttributes.push(attribute); } } }, /** + * Disables all currently enabled vertex attribute arrays. + */ + cleanupVertexAttrib: function TGLP_cleanupVertexAttrib() + { + let utils = TiltGL.ProgramUtils; + + for each (let attribute in utils._enabledAttributes) { + this._context.disableVertexAttribArray(attribute); + } + utils._enabledAttributes = []; + }, + + /** * Binds a vertex buffer as an array buffer for a specific shader attribute. * * @param {String} aAtribute * the attribute name obtained from the shader * @param {Float32Array} aBuffer * the buffer to be bound */ bindVertexBuffer: function TGLP_bindVertexBuffer(aAtribute, aBuffer) @@ -944,19 +954,19 @@ TiltGL.Program.prototype = { * the sampler name to bind the texture to * @param {TiltGL.Texture} aTexture * the texture to be bound */ bindTexture: function TGLP_bindTexture(aSampler, aTexture) { let gl = this._context; - gl.uniform1i(this._uniforms[aSampler], 0); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, aTexture._ref); + gl.uniform1i(this._uniforms[aSampler], 0); }, /** * Function called when this object is destroyed. */ finalize: function TGLP_finalize() { if (this._context) { @@ -1172,17 +1182,17 @@ TiltGL.ProgramUtils = { /** * Represents the current active shader, identified by an id. */ _activeProgram: -1, /** * Represents the current enabled attributes. */ - _enabledAttributes: -1 + _enabledAttributes: [] }; /** * This constructor creates a texture from an Image. * * @param {Object} aContext * a WebGL context * @param {Object} aProperties @@ -1410,17 +1420,17 @@ TiltGL.TextureUtils = { // generate mipmap if necessary if (aProperties.mipmap) { gl.generateMipmap(gl.TEXTURE_2D); } }, /** * This shim renders a content window to a canvas element, but clamps the - * maximum width and height of the canvas to half the WebGL MAX_TEXTURE_SIZE. + * maximum width and height of the canvas to the WebGL MAX_TEXTURE_SIZE. * * @param {Window} aContentWindow * the content window to get a texture from * @param {Number} aMaxImageSize * the maximum image size to be used * * @return {Image} the new content window image */ @@ -1610,10 +1620,10 @@ TiltGL.create3DContext = function TGL_cr }; /** * Clears the cache and sets all the variables to default. */ TiltGL.clearCache = function TGL_clearCache() { TiltGL.ProgramUtils._activeProgram = -1; - TiltGL.ProgramUtils._enabledAttributes = -1; + TiltGL.ProgramUtils._enabledAttributes = []; };
--- a/browser/devtools/tilt/TiltUtils.jsm +++ b/browser/devtools/tilt/TiltUtils.jsm @@ -513,18 +513,18 @@ TiltUtils.bindObjectFunc = function TU_b */ TiltUtils.destroyObject = function TU_destroyObject(aScope) { if (!aScope) { return; } // objects in Tilt usually use a function to handle internal destruction - if ("function" === typeof aScope.finalize) { - aScope.finalize(); + if ("function" === typeof aScope._finalize) { + aScope._finalize(); } for (let i in aScope) { if (aScope.hasOwnProperty(i)) { delete aScope[i]; } } };
--- a/browser/devtools/tilt/TiltVisualizer.jsm +++ b/browser/devtools/tilt/TiltVisualizer.jsm @@ -50,33 +50,40 @@ const INVISIBLE_ELEMENTS = { "link": true, "meta": true, "option": true, "script": true, "style": true, "title": true }; +// a node is represented in the visualization mesh as a rectangular stack +// of 5 quads composed of 12 vertices; we draw these as triangles using an +// index buffer of 12 unsigned int elements, obviously one for each vertex; +// if a webpage has enough nodes to overflow the index buffer elements size, +// weird things may happen; thus, when necessary, we'll split into groups +const MAX_GROUP_NODES = Math.pow(2, Uint16Array.BYTES_PER_ELEMENT * 8) / 12 - 1; + const STACK_THICKNESS = 15; const WIREFRAME_COLOR = [0, 0, 0, 0.25]; -const INTRO_TRANSITION_DURATION = 50; -const OUTRO_TRANSITION_DURATION = 40; +const INTRO_TRANSITION_DURATION = 1000; +const OUTRO_TRANSITION_DURATION = 800; const INITIAL_Z_TRANSLATION = 400; const MOVE_INTO_VIEW_ACCURACY = 50; const MOUSE_CLICK_THRESHOLD = 10; -const MOUSE_INTRO_DELAY = 10; +const MOUSE_INTRO_DELAY = 200; const ARCBALL_SENSITIVITY = 0.5; const ARCBALL_ROTATION_STEP = 0.15; const ARCBALL_TRANSLATION_STEP = 35; const ARCBALL_ZOOM_STEP = 0.1; const ARCBALL_ZOOM_MIN = -3000; const ARCBALL_ZOOM_MAX = 500; -const ARCBALL_RESET_FACTOR = 0.9; -const ARCBALL_RESET_INTERVAL = 1000 / 60; +const ARCBALL_RESET_SPHERICAL_FACTOR = 0.1; +const ARCBALL_RESET_LINEAR_FACTOR = 0.01; const TILT_CRAFTER = "resource:///modules/devtools/TiltWorkerCrafter.js"; const TILT_PICKER = "resource:///modules/devtools/TiltWorkerPicker.js"; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource:///modules/devtools/TiltGL.jsm"); Cu.import("resource:///modules/devtools/TiltMath.jsm"); Cu.import("resource:///modules/devtools/TiltUtils.jsm"); @@ -87,17 +94,16 @@ let EXPORTED_SYMBOLS = ["TiltVisualizer" /** * Initializes the visualization presenter and controller. * * @param {Object} aProperties * an object containing the following properties: * {Window} chromeWindow: a reference to the top level window * {Window} contentWindow: the content window holding the visualized doc * {Element} parentNode: the parent node to hold the visualization - * {Function} requestAnimationFrame: responsible with scheduling loops * {Object} notifications: necessary notifications for Tilt * {Function} onError: optional, function called if initialization failed * {Function} onLoad: optional, function called if initialization worked */ function TiltVisualizer(aProperties) { // make sure the properties parameter is a valid object aProperties = aProperties || {}; @@ -116,17 +122,16 @@ function TiltVisualizer(aProperties) }); /** * Visualization logic and drawing loop. */ this.presenter = new TiltVisualizer.Presenter(this.canvas, aProperties.chromeWindow, aProperties.contentWindow, - aProperties.requestAnimationFrame, aProperties.notifications, aProperties.onError || null, aProperties.onLoad || null); /** * Visualization mouse and keyboard controller. */ this.controller = new TiltVisualizer.Controller(this.canvas, this.presenter); @@ -179,28 +184,25 @@ TiltVisualizer.prototype = { * This object manages the visualization logic and drawing loop. * * @param {HTMLCanvasElement} aCanvas * the canvas element used for rendering * @param {Window} aChromeWindow * a reference to the top-level window * @param {Window} aContentWindow * the content window holding the document to be visualized - * @param {Function} aRequestAnimationFrame - * function responsible with scheduling loop frames * @param {Object} aNotifications * necessary notifications for Tilt * @param {Function} onError * function called if initialization failed * @param {Function} onLoad * function called if initialization worked */ TiltVisualizer.Presenter = function TV_Presenter( - aCanvas, aChromeWindow, aContentWindow, aRequestAnimationFrame, aNotifications, - onError, onLoad) + aCanvas, aChromeWindow, aContentWindow, aNotifications, onError, onLoad) { /** * A canvas overlay used for drawing the visualization. */ this.canvas = aCanvas; /** * Save a reference to the top-level window, to access InspectorUI or Tilt. @@ -215,35 +217,36 @@ TiltVisualizer.Presenter = function TV_P /** * Shortcut for accessing notifications strings. */ this.NOTIFICATIONS = aNotifications; /** * Create the renderer, containing useful functions for easy drawing. */ - this.renderer = new TiltGL.Renderer(aCanvas, onError, onLoad); + this._renderer = new TiltGL.Renderer(aCanvas, onError, onLoad); /** * A custom shader used for drawing the visualization mesh. */ - this.visualizationProgram = null; + this._visualizationProgram = null; /** * The combined mesh representing the document visualization. */ - this.texture = null; - this.meshStacks = null; - this.meshWireframe = null; - this.traverseData = null; + this._texture = null; + this._meshData = null; + this._meshStacks = null; + this._meshWireframe = null; + this._traverseData = null; /** * A highlight quad drawn over a stacked dom node. */ - this.highlight = { + this._highlight = { disabled: true, v0: vec3.create(), v1: vec3.create(), v2: vec3.create(), v3: vec3.create() }; /** @@ -263,396 +266,435 @@ TiltVisualizer.Presenter = function TV_P this._currentSelection = -1; // the selected node index this._initialSelection = false; // true if an initial selection was made this._initialMeshConfiguration = false; // true if the 3D mesh was configured /** * Variable specifying if the scene should be redrawn. * This should happen usually when the visualization is translated/rotated. */ - this.redraw = true; + this._redraw = true; + + /** + * Total time passed since the rendering started. + * If the rendering is paused, this property won't get updated. + */ + this._time = 0; /** - * A frame counter, incremented each time the scene is redrawn. + * Frame delta time (the ammount of time passed for each frame). + * This is used to smoothly interpolate animation transfroms. */ - this.frames = 0; + this._delta = 0; + this._prevFrameTime = 0; + this._currFrameTime = 0; + + + this._setup(); + this._loop(); +}; + +TiltVisualizer.Presenter.prototype = { /** * The initialization logic. */ - let setup = function TVP_setup() + _setup: function TVP__setup() { - let renderer = this.renderer; + let renderer = this._renderer; let inspector = this.chromeWindow.InspectorUI; // if the renderer was destroyed, don't continue setup if (!renderer || !renderer.context) { return; } // create the visualization shaders and program to draw the stacks mesh - this.visualizationProgram = new renderer.Program({ + this._visualizationProgram = new renderer.Program({ vs: TiltVisualizer.MeshShader.vs, fs: TiltVisualizer.MeshShader.fs, attributes: ["vertexPosition", "vertexTexCoord", "vertexColor"], uniforms: ["mvMatrix", "projMatrix", "sampler"] }); // get the document zoom to properly scale the visualization if (inspector.highlighter) { this.transforms.zoom = inspector.highlighter.zoom; } - this.setupTexture(); - this.setupMeshData(); - this.setupEventListeners(); + // bind the owner object to the necessary functions + TiltUtils.bindObjectFunc(this, "^_on"); + TiltUtils.bindObjectFunc(this, "_loop"); + + this._setupTexture(); + this._setupMeshData(); + this._setupEventListeners(); this.canvas.focus(); - }.bind(this); + }, /** * The animation logic. */ - let loop = function TVP_loop() + _loop: function TVP__loop() { - let renderer = this.renderer; + let renderer = this._renderer; // if the renderer was destroyed, don't continue rendering if (!renderer || !renderer.context) { return; } // prepare for the next frame of the animation loop - aRequestAnimationFrame(loop); + this.chromeWindow.mozRequestAnimationFrame(this._loop); // only redraw if we really have to - if (this.redraw) { - this.redraw = false; - this.drawVisualization(); + if (this._redraw) { + this._redraw = false; + this._drawVisualization(); + } + + // update the current presenter transfroms from the controller + if ("function" === typeof this._controllerUpdate) { + this._controllerUpdate(this._time, this._delta); } - // call the attached ondraw function and handle all keyframe notifications - if ("function" === typeof this.ondraw) { - this.ondraw(this.frames); - } + this._handleFrameDelta(); + this._handleKeyframeNotifications(); + }, - this.handleKeyframeNotifications(); - }.bind(this); - - setup(); - loop(); -}; - -TiltVisualizer.Presenter.prototype = { + /** + * Calculates the current frame delta time. + */ + _handleFrameDelta: function TVP__handleFrameDelta() + { + this._prevFrameTime = this._currFrameTime; + this._currFrameTime = this.chromeWindow.mozAnimationStartTime; + this._delta = this._currFrameTime - this._prevFrameTime; + }, /** * Draws the visualization mesh and highlight quad. */ - drawVisualization: function TVP_drawVisualization() + _drawVisualization: function TVP__drawVisualization() { - let renderer = this.renderer; + let renderer = this._renderer; let transforms = this.transforms; let w = renderer.width; let h = renderer.height; + let ih = renderer.initialHeight; // if the mesh wasn't created yet, don't continue rendering - if (!this.meshStacks || !this.meshWireframe) { + if (!this._meshStacks || !this._meshWireframe) { return; } // clear the context to an opaque black background renderer.clear(); renderer.perspective(); // apply a transition transformation using an ortho and perspective matrix let ortho = mat4.ortho(0, w, h, 0, -1000, 1000); - if (!this.isExecutingDestruction) { - let f = this.frames / INTRO_TRANSITION_DURATION; + if (!this._isExecutingDestruction) { + let f = this._time / INTRO_TRANSITION_DURATION; renderer.lerp(renderer.projMatrix, ortho, f, 8); } else { - let f = this.frames / OUTRO_TRANSITION_DURATION; + let f = this._time / OUTRO_TRANSITION_DURATION; renderer.lerp(renderer.projMatrix, ortho, 1 - f, 8); } // apply the preliminary transformations to the model view - renderer.translate(w * 0.5, h * 0.5, -INITIAL_Z_TRANSLATION); + renderer.translate(w * 0.5, ih * 0.5, -INITIAL_Z_TRANSLATION); // calculate the camera matrix using the rotation and translation renderer.translate(transforms.translation[0], 0, transforms.translation[2]); renderer.transform(quat4.toMat4(transforms.rotation)); // offset the visualization mesh to center renderer.translate(transforms.offset[0], transforms.offset[1] + transforms.translation[1], 0); renderer.scale(transforms.zoom, transforms.zoom); // draw the visualization mesh renderer.strokeWeight(2); renderer.depthTest(true); - this.drawMeshStacks(); - this.drawMeshWireframe(); - this.drawHighlight(); + this._drawMeshStacks(); + this._drawMeshWireframe(); + this._drawHighlight(); // make sure the initial transition is drawn until finished - if (this.frames < INTRO_TRANSITION_DURATION || - this.frames < OUTRO_TRANSITION_DURATION) { - this.redraw = true; + if (this._time < INTRO_TRANSITION_DURATION || + this._time < OUTRO_TRANSITION_DURATION) { + this._redraw = true; } - this.frames++; + this._time += this._delta; }, /** * Draws the meshStacks object. */ - drawMeshStacks: function TVP_drawMeshStacks() + _drawMeshStacks: function TVP__drawMeshStacks() { - let renderer = this.renderer; - let mesh = this.meshStacks; + let renderer = this._renderer; + let mesh = this._meshStacks; - let visualizationProgram = this.visualizationProgram; - let texture = this.texture; + let visualizationProgram = this._visualizationProgram; + let texture = this._texture; let mvMatrix = renderer.mvMatrix; let projMatrix = renderer.projMatrix; // use the necessary shader visualizationProgram.use(); - // bind the attributes and uniforms as necessary - visualizationProgram.bindVertexBuffer("vertexPosition", mesh.vertices); - visualizationProgram.bindVertexBuffer("vertexTexCoord", mesh.texCoord); - visualizationProgram.bindVertexBuffer("vertexColor", mesh.color); + for (let i = 0, len = mesh.length; i < len; i++) { + let group = mesh[i]; + + // bind the attributes and uniforms as necessary + visualizationProgram.bindVertexBuffer("vertexPosition", group.vertices); + visualizationProgram.bindVertexBuffer("vertexTexCoord", group.texCoord); + visualizationProgram.bindVertexBuffer("vertexColor", group.color); - visualizationProgram.bindUniformMatrix("mvMatrix", mvMatrix); - visualizationProgram.bindUniformMatrix("projMatrix", projMatrix); - visualizationProgram.bindTexture("sampler", texture); + visualizationProgram.bindUniformMatrix("mvMatrix", mvMatrix); + visualizationProgram.bindUniformMatrix("projMatrix", projMatrix); + visualizationProgram.bindTexture("sampler", texture); - // draw the vertices as TRIANGLES indexed elements - renderer.drawIndexedVertices(renderer.context.TRIANGLES, mesh.indices); + // draw the vertices as TRIANGLES indexed elements + renderer.drawIndexedVertices(renderer.context.TRIANGLES, group.indices); + } // save the current model view and projection matrices mesh.mvMatrix = mat4.create(mvMatrix); mesh.projMatrix = mat4.create(projMatrix); }, /** * Draws the meshWireframe object. */ - drawMeshWireframe: function TVP_drawMeshWireframe() + _drawMeshWireframe: function TVP__drawMeshWireframe() { - let renderer = this.renderer; - let mesh = this.meshWireframe; + let renderer = this._renderer; + let mesh = this._meshWireframe; - // use the necessary shader - renderer.useColorShader(mesh.vertices, WIREFRAME_COLOR); + for (let i = 0, len = mesh.length; i < len; i++) { + let group = mesh[i]; - // draw the vertices as LINES indexed elements - renderer.drawIndexedVertices(renderer.context.LINES, mesh.indices); + // use the necessary shader + renderer.useColorShader(group.vertices, WIREFRAME_COLOR); + + // draw the vertices as LINES indexed elements + renderer.drawIndexedVertices(renderer.context.LINES, group.indices); + } }, /** * Draws a highlighted quad around a currently selected node. */ - drawHighlight: function TVP_drawHighlight() + _drawHighlight: function TVP__drawHighlight() { // check if there's anything to highlight (i.e any node is selected) - if (!this.highlight.disabled) { + if (!this._highlight.disabled) { // set the corresponding state to draw the highlight quad - let renderer = this.renderer; - let highlight = this.highlight; + let renderer = this._renderer; + let highlight = this._highlight; renderer.depthTest(false); renderer.fill(highlight.fill, 0.5); renderer.stroke(highlight.stroke); renderer.strokeWeight(highlight.strokeWeight); renderer.quad(highlight.v0, highlight.v1, highlight.v2, highlight.v3); } }, /** * Creates or refreshes the texture applied to the visualization mesh. */ - setupTexture: function TVP_setupTexture() + _setupTexture: function TVP__setupTexture() { - let renderer = this.renderer; + let renderer = this._renderer; // destroy any previously created texture - TiltUtils.destroyObject(this.texture); + TiltUtils.destroyObject(this._texture); this._texture = null; // if the renderer was destroyed, don't continue setup if (!renderer || !renderer.context) { return; } // get the maximum texture size - this.maxTextureSize = + this._maxTextureSize = renderer.context.getParameter(renderer.context.MAX_TEXTURE_SIZE); // use a simple shim to get the image representation of the document // this will be removed once the MOZ_window_region_texture bug #653656 // is finished; currently just converting the document image to a texture // applied to the mesh - this.texture = new renderer.Texture({ + this._texture = new renderer.Texture({ source: TiltGL.TextureUtils.createContentImage(this.contentWindow, - this.maxTextureSize), + this._maxTextureSize), format: "RGB" }); - if ("function" === typeof this.onSetupTexture) { - this.onSetupTexture(); - this.onSetupTexture = null; + if ("function" === typeof this._onSetupTexture) { + this._onSetupTexture(); + this._onSetupTexture = null; } }, /** * Create the combined mesh representing the document visualization by * traversing the document & adding a stack for each node that is drawable. * - * @param {Object} aData + * @param {Object} aMeshData * object containing the necessary mesh verts, texcoord etc. */ - setupMesh: function TVP_setupMesh(aData) + _setupMesh: function TVP__setupMesh(aMeshData) { - let renderer = this.renderer; + let renderer = this._renderer; // destroy any previously created mesh - TiltUtils.destroyObject(this.meshStacks); - TiltUtils.destroyObject(this.meshWireframe); + TiltUtils.destroyObject(this._meshStacks); this._meshStacks = []; + TiltUtils.destroyObject(this._meshWireframe); this._meshWireframe = []; // if the renderer was destroyed, don't continue setup if (!renderer || !renderer.context) { return; } // save the mesh data for future use - this.meshData = aData; + this._meshData = aMeshData; + + // create a sub-mesh for each group in the mesh data + for (let i = 0, len = aMeshData.groups.length; i < len; i++) { + let group = aMeshData.groups[i]; - // create the visualization mesh using the vertices, texture coordinates - // and indices computed when traversing the document object model - this.meshStacks = { - vertices: new renderer.VertexBuffer(aData.vertices, 3), - texCoord: new renderer.VertexBuffer(aData.texCoord, 2), - color: new renderer.VertexBuffer(aData.color, 3), - indices: new renderer.IndexBuffer(aData.stacksIndices) - }; + // create the visualization mesh using the vertices, texture coordinates + // and indices computed when traversing the document object model + this._meshStacks.push({ + vertices: new renderer.VertexBuffer(group.vertices, 3), + texCoord: new renderer.VertexBuffer(group.texCoord, 2), + color: new renderer.VertexBuffer(group.color, 3), + indices: new renderer.IndexBuffer(group.stacksIndices) + }); - // additionally, create a wireframe representation to make the - // visualization a bit more pretty - this.meshWireframe = { - vertices: this.meshStacks.vertices, - indices: new renderer.IndexBuffer(aData.wireframeIndices) - }; + // additionally, create a wireframe representation to make the + // visualization a bit more pretty + this._meshWireframe.push({ + vertices: this._meshStacks[i].vertices, + indices: new renderer.IndexBuffer(group.wireframeIndices) + }); + } // if there's no initial selection made, highlight the required node if (!this._initialSelection) { this._initialSelection = true; this.highlightNode(this.chromeWindow.InspectorUI.selection); + + if (this._currentSelection === 0) { // if the "html" node is selected + this._highlight.disabled = true; + } } + // configure the required mesh transformations and background only once if (!this._initialMeshConfiguration) { this._initialMeshConfiguration = true; - let width = renderer.width; - let height = renderer.height; - // set the necessary mesh offsets - this.transforms.offset[0] = -width * 0.5; - this.transforms.offset[1] = -height * 0.5; + this.transforms.offset[0] = -renderer.width * 0.5; + this.transforms.offset[1] = -renderer.height * 0.5; // make sure the canvas is opaque now that the initialization is finished this.canvas.style.background = TiltVisualizerStyle.canvas.background; - this.drawVisualization(); - this.redraw = true; + this._drawVisualization(); + this._redraw = true; } - if ("function" === typeof this.onSetupMesh) { - this.onSetupMesh(); - this.onSetupMesh = null; + if ("function" === typeof this._onSetupMesh) { + this._onSetupMesh(); + this._onSetupMesh = null; } }, /** - * Computes the mesh vertices, texture coordinates etc. + * Computes the mesh vertices, texture coordinates etc. by groups of nodes. */ - setupMeshData: function TVP_setupMeshData() + _setupMeshData: function TVP__setupMeshData() { - let renderer = this.renderer; + let renderer = this._renderer; // if the renderer was destroyed, don't continue setup if (!renderer || !renderer.context) { return; } // traverse the document and get the depths, coordinates and local names - this.traverseData = TiltUtils.DOM.traverse(this.contentWindow, { + this._traverseData = TiltUtils.DOM.traverse(this.contentWindow, { invisibleElements: INVISIBLE_ELEMENTS, minSize: ELEMENT_MIN_SIZE, - maxX: this.texture.width, - maxY: this.texture.height + maxX: this._texture.width, + maxY: this._texture.height }); let worker = new ChromeWorker(TILT_CRAFTER); worker.addEventListener("message", function TVP_onMessage(event) { - this.setupMesh(event.data); + this._setupMesh(event.data); }.bind(this), false); // calculate necessary information regarding vertices, texture coordinates // etc. in a separate thread, as this process may take a while worker.postMessage({ + maxGroupNodes: MAX_GROUP_NODES, thickness: STACK_THICKNESS, style: TiltVisualizerStyle.nodes, - texWidth: this.texture.width, - texHeight: this.texture.height, - nodesInfo: this.traverseData.info + texWidth: this._texture.width, + texHeight: this._texture.height, + nodesInfo: this._traverseData.info }); }, /** * Sets up event listeners necessary for the presenter. */ - setupEventListeners: function TVP_setupEventListeners() + _setupEventListeners: function TVP__setupEventListeners() { - // bind the owner object to the necessary functions - TiltUtils.bindObjectFunc(this, "^on"); - - this.contentWindow.addEventListener("resize", this.onResize, false); + this.contentWindow.addEventListener("resize", this._onResize, false); }, /** * Called when the content window of the current browser is resized. */ - onResize: function TVP_onResize(e) + _onResize: function TVP_onResize(e) { let zoom = this.chromeWindow.InspectorUI.highlighter.zoom; let width = e.target.innerWidth * zoom; let height = e.target.innerHeight * zoom; // handle aspect ratio changes to update the projection matrix - this.renderer.width = width; - this.renderer.height = height; + this._renderer.width = width; + this._renderer.height = height; - this.redraw = true; + this._redraw = true; }, /** * Highlights a specific node. * * @param {Element} aNode * the html node to be highlighted * @param {String} aFlags * flags specifying highlighting options */ highlightNode: function TVP_highlightNode(aNode, aFlags) { - this.highlightNodeFor(this.traverseData.nodes.indexOf(aNode), aFlags); + this.highlightNodeFor(this._traverseData.nodes.indexOf(aNode), aFlags); }, /** * Picks a stacked dom node at the x and y screen coordinates and highlights * the selected node in the mesh. * * @param {Number} x * the current horizontal coordinate of the mouse @@ -700,41 +742,41 @@ TiltVisualizer.Presenter.prototype = { }); }, /** * Sets the corresponding highlight coordinates and color based on the * information supplied. * * @param {Number} aNodeIndex - * the index of the node in the this.traverseData array + * the index of the node in the this._traverseData array * @param {String} aFlags * flags specifying highlighting options */ highlightNodeFor: function TVP_highlightNodeFor(aNodeIndex, aFlags) { - this.redraw = true; + this._redraw = true; // if the node was already selected, don't do anything if (this._currentSelection === aNodeIndex) { return; } // if an invalid or nonexisted node is specified, disable the highlight if (aNodeIndex < 0) { this._currentSelection = -1; - this.highlight.disabled = true; + this._highlight.disabled = true; Services.obs.notifyObservers(null, this.NOTIFICATIONS.UNHIGHLIGHTING, null); return; } - let highlight = this.highlight; - let info = this.traverseData.info[aNodeIndex]; - let node = this.traverseData.nodes[aNodeIndex]; + let highlight = this._highlight; + let info = this._traverseData.info[aNodeIndex]; + let node = this._traverseData.nodes[aNodeIndex]; let style = TiltVisualizerStyle.nodes; highlight.disabled = false; highlight.fill = style[info.name] || style.highlight.defaultFill; highlight.stroke = style.highlight.defaultStroke; highlight.strokeWeight = style.highlight.defaultStrokeWeight; let x = info.coord.left; @@ -756,50 +798,53 @@ TiltVisualizer.Presenter.prototype = { // if something is highlighted, make sure it's inside the current viewport; // the point which should be moved into view is considered the center [x, y] // position along the top edge of the currently selected node if (aFlags && aFlags.indexOf("moveIntoView") !== -1) { this.controller.arcball.moveIntoView(vec3.lerp( - vec3.scale(this.highlight.v0, this.transforms.zoom, []), - vec3.scale(this.highlight.v1, this.transforms.zoom, []), 0.5)); + vec3.scale(this._highlight.v0, this.transforms.zoom, []), + vec3.scale(this._highlight.v1, this.transforms.zoom, []), 0.5)); } Services.obs.notifyObservers(null, this.NOTIFICATIONS.HIGHLIGHTING, null); }, /** * Deletes a node from the visualization mesh. * * @param {Number} aNodeIndex - * the index of the node in the this.traverseData array; + * the index of the node in the this._traverseData array; * if not specified, it will default to the current selection */ deleteNode: function TVP_deleteNode(aNodeIndex) { // we probably don't want to delete the html or body node.. just sayin' if ((aNodeIndex = aNodeIndex || this._currentSelection) < 1) { return; } - let renderer = this.renderer; - let meshData = this.meshData; + let renderer = this._renderer; - for (let i = 0, k = 36 * aNodeIndex; i < 36; i++) { - meshData.vertices[i + k] = 0; + let groupIndex = parseInt(aNodeIndex / MAX_GROUP_NODES); + let nodeIndex = parseInt((aNodeIndex + (groupIndex ? 1 : 0)) % MAX_GROUP_NODES); + let group = this._meshStacks[groupIndex]; + let vertices = group.vertices.components; + + for (let i = 0, k = 36 * nodeIndex; i < 36; i++) { + vertices[i + k] = 0; } - this.meshStacks.vertices = new renderer.VertexBuffer(meshData.vertices, 3); - this.highlight.disabled = true; - this.redraw = true; + group.vertices = new renderer.VertexBuffer(vertices, 3); + this._highlight.disabled = true; + this._redraw = true; - Services.obs.notifyObservers(null, - this.NOTIFICATIONS.NODE_REMOVED, null); + Services.obs.notifyObservers(null, this.NOTIFICATIONS.NODE_REMOVED, null); }, /** * Picks a stacked dom node at the x and y screen coordinates and issues * a callback function with the found intersection. * * @param {Number} x * the current horizontal coordinate of the mouse @@ -811,17 +856,17 @@ TiltVisualizer.Presenter.prototype = { * {Function} onfail: function to be called if no intersections */ pickNode: function TVP_pickNode(x, y, aProperties) { // make sure the properties parameter is a valid object aProperties = aProperties || {}; // if the mesh wasn't created yet, don't continue picking - if (!this.meshStacks || !this.meshWireframe) { + if (!this._meshStacks || !this._meshWireframe) { return; } let worker = new ChromeWorker(TILT_PICKER); worker.addEventListener("message", function TVP_onMessage(event) { if (event.data) { if ("function" === typeof aProperties.onpick) { @@ -830,33 +875,32 @@ TiltVisualizer.Presenter.prototype = { } else { if ("function" === typeof aProperties.onfail) { aProperties.onfail(); } } }, false); let zoom = this.chromeWindow.InspectorUI.highlighter.zoom; - let width = this.renderer.width * zoom; - let height = this.renderer.height * zoom; - let mesh = this.meshStacks; + let width = this._renderer.width * zoom; + let height = this._renderer.height * zoom; x *= zoom; y *= zoom; // create a ray following the mouse direction from the near clipping plane // to the far clipping plane, to check for intersections with the mesh, // and do all the heavy lifting in a separate thread worker.postMessage({ thickness: STACK_THICKNESS, - vertices: mesh.vertices.components, + vertices: this._meshData.allVertices, // create the ray destined for 3D picking ray: vec3.createRay([x, y, 0], [x, y, 1], [0, 0, width, height], - mesh.mvMatrix, - mesh.projMatrix) + this._meshStacks.mvMatrix, + this._meshStacks.projMatrix) }); }, /** * Delegate translation method, used by the controller. * * @param {Array} aTranslation * the new translation on the [x, y, z] axis @@ -869,17 +913,17 @@ TiltVisualizer.Presenter.prototype = { let transforms = this.transforms; // only update the translation if it's not already set if (transforms.translation[0] !== x || transforms.translation[1] !== y || transforms.translation[2] !== z) { vec3.set(aTranslation, transforms.translation); - this.redraw = true; + this._redraw = true; } }, /** * Delegate rotation method, used by the controller. * * @param {Array} aQuaternion * the rotation quaternion, as [x, y, z, w] @@ -894,113 +938,118 @@ TiltVisualizer.Presenter.prototype = { // only update the rotation if it's not already set if (transforms.rotation[0] !== x || transforms.rotation[1] !== y || transforms.rotation[2] !== z || transforms.rotation[3] !== w) { quat4.set(aQuaternion, transforms.rotation); - this.redraw = true; + this._redraw = true; } }, /** * Handles notifications at specific frame counts. */ - handleKeyframeNotifications: function TV_handleKeyframeNotifications() + _handleKeyframeNotifications: function TV__handleKeyframeNotifications() { - if (!TiltVisualizer.Prefs.introTransition && !this.isExecutingDestruction) { - this.frames = INTRO_TRANSITION_DURATION; + if (!TiltVisualizer.Prefs.introTransition && !this._isExecutingDestruction) { + this._time = INTRO_TRANSITION_DURATION; } - if (!TiltVisualizer.Prefs.outroTransition && this.isExecutingDestruction) { - this.frames = OUTRO_TRANSITION_DURATION; + if (!TiltVisualizer.Prefs.outroTransition && this._isExecutingDestruction) { + this._time = OUTRO_TRANSITION_DURATION; } - if (this.frames === INTRO_TRANSITION_DURATION && - !this.isExecutingDestruction) { + if (this._time >= INTRO_TRANSITION_DURATION && + !this._isInitializationFinished && + !this._isExecutingDestruction) { + this._isInitializationFinished = true; Services.obs.notifyObservers(null, this.NOTIFICATIONS.INITIALIZED, null); - if ("function" === typeof this.onInitializationFinished) { - this.onInitializationFinished(); + if ("function" === typeof this._onInitializationFinished) { + this._onInitializationFinished(); } } - if (this.frames === OUTRO_TRANSITION_DURATION && - this.isExecutingDestruction) { + if (this._time >= OUTRO_TRANSITION_DURATION && + !this._isDestructionFinished && + this._isExecutingDestruction) { + this._isDestructionFinished = true; Services.obs.notifyObservers(null, this.NOTIFICATIONS.BEFORE_DESTROYED, null); - if ("function" === typeof this.onDestructionFinished) { - this.onDestructionFinished(); + if ("function" === typeof this._onDestructionFinished) { + this._onDestructionFinished(); } } }, /** * Starts executing the destruction sequence and issues a callback function * when finished. * * @param {Function} aCallback * the destruction finished callback */ executeDestruction: function TV_executeDestruction(aCallback) { - if (!this.isExecutingDestruction) { - this.isExecutingDestruction = true; - this.onDestructionFinished = aCallback; + if (!this._isExecutingDestruction) { + this._isExecutingDestruction = true; + this._onDestructionFinished = aCallback; // if we execute the destruction after the initialization finishes, // proceed normally; otherwise, skip everything and immediately issue // the callback - if (this.frames > OUTRO_TRANSITION_DURATION) { - this.frames = 0; - this.redraw = true; + if (this._time > OUTRO_TRANSITION_DURATION) { + this._time = 0; + this._redraw = true; } else { aCallback(); } } }, /** * Checks if this object was initialized properly. * * @return {Boolean} true if the object was initialized properly */ isInitialized: function TVP_isInitialized() { - return this.renderer && this.renderer.context; + return this._renderer && this._renderer.context; }, /** * Function called when this object is destroyed. */ - finalize: function TVP_finalize() + _finalize: function TVP__finalize() { - TiltUtils.destroyObject(this.visualizationProgram); - TiltUtils.destroyObject(this.texture); + TiltUtils.destroyObject(this._visualizationProgram); + TiltUtils.destroyObject(this._texture); - if (this.meshStacks) { - TiltUtils.destroyObject(this.meshStacks.vertices); - TiltUtils.destroyObject(this.meshStacks.texCoord); - TiltUtils.destroyObject(this.meshStacks.color); - TiltUtils.destroyObject(this.meshStacks.indices); + if (this._meshStacks) { + this._meshStacks.forEach(function(group) { + TiltUtils.destroyObject(group.vertices); + TiltUtils.destroyObject(group.texCoord); + TiltUtils.destroyObject(group.color); + TiltUtils.destroyObject(group.indices); + }); + } + if (this._meshWireframe) { + this._meshWireframe.forEach(function(group) { + TiltUtils.destroyObject(group.indices); + }); } - if (this.meshWireframe) { - TiltUtils.destroyObject(this.meshWireframe.indices); - } + TiltUtils.destroyObject(this._renderer); - TiltUtils.destroyObject(this.highlight); - TiltUtils.destroyObject(this.transforms); - TiltUtils.destroyObject(this.renderer); - - this.contentWindow.removeEventListener("resize", this.onResize, false); + this.contentWindow.removeEventListener("resize", this._onResize, false); } }; /** * A mouse and keyboard controller implementation. * * @param {HTMLCanvasElement} aCanvas * the visualization canvas element @@ -1018,141 +1067,143 @@ TiltVisualizer.Controller = function TV_ * Save a reference to the presenter to modify its model-view transforms. */ this.presenter = aPresenter; this.presenter.controller = this; /** * The initial controller dimensions and offset, in pixels. */ - this.zoom = aPresenter.transforms.zoom; - this.left = (aPresenter.contentWindow.pageXOffset || 0) * this.zoom; - this.top = (aPresenter.contentWindow.pageYOffset || 0) * this.zoom; - this.width = aCanvas.width; - this.height = aCanvas.height; + this._zoom = aPresenter.transforms.zoom; + this._left = (aPresenter.contentWindow.pageXOffset || 0) * this._zoom; + this._top = (aPresenter.contentWindow.pageYOffset || 0) * this._zoom; + this._width = aCanvas.width; + this._height = aCanvas.height; /** * Arcball used to control the visualization using the mouse. */ this.arcball = new TiltVisualizer.Arcball( - this.presenter.chromeWindow, this.width, this.height, 0, + this.presenter.chromeWindow, this._width, this._height, 0, [ - this.width + this.left < aPresenter.maxTextureSize ? -this.left : 0, - this.height + this.top < aPresenter.maxTextureSize ? -this.top : 0 + this._width + this._left < aPresenter._maxTextureSize ? -this._left : 0, + this._height + this._top < aPresenter._maxTextureSize ? -this._top : 0 ]); /** * Object containing the rotation quaternion and the translation amount. */ - this.coordinates = null; + this._coordinates = null; // bind the owner object to the necessary functions - TiltUtils.bindObjectFunc(this, "update"); - TiltUtils.bindObjectFunc(this, "^on"); + TiltUtils.bindObjectFunc(this, "_update"); + TiltUtils.bindObjectFunc(this, "^_on"); // add the necessary event listeners this.addEventListeners(); // attach this controller's update function to the presenter ondraw event - aPresenter.ondraw = this.update; + this.presenter._controllerUpdate = this._update; }; TiltVisualizer.Controller.prototype = { /** * Adds events listeners required by this controller. */ addEventListeners: function TVC_addEventListeners() { let canvas = this.canvas; let presenter = this.presenter; // bind commonly used mouse and keyboard events with the controller - canvas.addEventListener("mousedown", this.onMouseDown, false); - canvas.addEventListener("mouseup", this.onMouseUp, false); - canvas.addEventListener("mousemove", this.onMouseMove, false); - canvas.addEventListener("mouseover", this.onMouseOver, false); - canvas.addEventListener("mouseout", this.onMouseOut, false); - canvas.addEventListener("MozMousePixelScroll", this.onMozScroll, false); - canvas.addEventListener("keydown", this.onKeyDown, false); - canvas.addEventListener("keyup", this.onKeyUp, false); - canvas.addEventListener("keypress", this.onKeyPress, true); - canvas.addEventListener("blur", this.onBlur, false); + canvas.addEventListener("mousedown", this._onMouseDown, false); + canvas.addEventListener("mouseup", this._onMouseUp, false); + canvas.addEventListener("mousemove", this._onMouseMove, false); + canvas.addEventListener("mouseover", this._onMouseOver, false); + canvas.addEventListener("mouseout", this._onMouseOut, false); + canvas.addEventListener("MozMousePixelScroll", this._onMozScroll, false); + canvas.addEventListener("keydown", this._onKeyDown, false); + canvas.addEventListener("keyup", this._onKeyUp, false); + canvas.addEventListener("keypress", this._onKeyPress, true); + canvas.addEventListener("blur", this._onBlur, false); // handle resize events to change the arcball dimensions - presenter.contentWindow.addEventListener("resize", this.onResize, false); + presenter.contentWindow.addEventListener("resize", this._onResize, false); }, /** * Removes all added events listeners required by this controller. */ removeEventListeners: function TVC_removeEventListeners() { let canvas = this.canvas; let presenter = this.presenter; - canvas.removeEventListener("mousedown", this.onMouseDown, false); - canvas.removeEventListener("mouseup", this.onMouseUp, false); - canvas.removeEventListener("mousemove", this.onMouseMove, false); - canvas.removeEventListener("mouseover", this.onMouseOver, false); - canvas.removeEventListener("mouseout", this.onMouseOut, false); - canvas.removeEventListener("MozMousePixelScroll", this.onMozScroll, false); - canvas.removeEventListener("keydown", this.onKeyDown, false); - canvas.removeEventListener("keyup", this.onKeyUp, false); - canvas.removeEventListener("keypress", this.onKeyPress, true); - canvas.removeEventListener("blur", this.onBlur, false); + canvas.removeEventListener("mousedown", this._onMouseDown, false); + canvas.removeEventListener("mouseup", this._onMouseUp, false); + canvas.removeEventListener("mousemove", this._onMouseMove, false); + canvas.removeEventListener("mouseover", this._onMouseOver, false); + canvas.removeEventListener("mouseout", this._onMouseOut, false); + canvas.removeEventListener("MozMousePixelScroll", this._onMozScroll, false); + canvas.removeEventListener("keydown", this._onKeyDown, false); + canvas.removeEventListener("keyup", this._onKeyUp, false); + canvas.removeEventListener("keypress", this._onKeyPress, true); + canvas.removeEventListener("blur", this._onBlur, false); - presenter.contentWindow.removeEventListener("resize", this.onResize,false); + presenter.contentWindow.removeEventListener("resize", this._onResize, false); }, /** * Function called each frame, updating the visualization camera transforms. * - * @param {Number} aFrames - * the current animation frame count + * @param {Number} aTime + * total time passed since rendering started + * @param {Number} aDelta + * the current animation frame delta */ - update: function TVC_update(aFrames) + _update: function TVC__update(aTime, aDelta) { - this.frames = aFrames; - this.coordinates = this.arcball.update(); + this._time = aTime; + this._coordinates = this.arcball.update(aDelta); - this.presenter.setRotation(this.coordinates.rotation); - this.presenter.setTranslation(this.coordinates.translation); + this.presenter.setRotation(this._coordinates.rotation); + this.presenter.setTranslation(this._coordinates.translation); }, /** * Called once after every time a mouse button is pressed. */ - onMouseDown: function TVC_onMouseDown(e) + _onMouseDown: function TVC__onMouseDown(e) { e.target.focus(); e.preventDefault(); e.stopPropagation(); - if (this.frames < MOUSE_INTRO_DELAY) { + if (this._time < MOUSE_INTRO_DELAY) { return; } // calculate x and y coordinates using using the client and target offset let button = e.which; this._downX = e.clientX - e.target.offsetLeft; this._downY = e.clientY - e.target.offsetTop; this.arcball.mouseDown(this._downX, this._downY, button); }, /** * Called every time a mouse button is released. */ - onMouseUp: function TVC_onMouseUp(e) + _onMouseUp: function TVC__onMouseUp(e) { e.preventDefault(); e.stopPropagation(); - if (this.frames < MOUSE_INTRO_DELAY) { + if (this._time < MOUSE_INTRO_DELAY) { return; } // calculate x and y coordinates using using the client and target offset let button = e.which; let upX = e.clientX - e.target.offsetLeft; let upY = e.clientY - e.target.offsetTop; @@ -1165,123 +1216,131 @@ TiltVisualizer.Controller.prototype = { } this.arcball.mouseUp(upX, upY, button); }, /** * Called every time the mouse moves. */ - onMouseMove: function TVC_onMouseMove(e) + _onMouseMove: function TVC__onMouseMove(e) { e.preventDefault(); e.stopPropagation(); - if (this.frames < MOUSE_INTRO_DELAY) { + if (this._time < MOUSE_INTRO_DELAY) { return; } // calculate x and y coordinates using using the client and target offset let moveX = e.clientX - e.target.offsetLeft; let moveY = e.clientY - e.target.offsetTop; this.arcball.mouseMove(moveX, moveY); }, /** * Called when the mouse leaves the visualization bounds. */ - onMouseOver: function TVC_onMouseOver(e) + _onMouseOver: function TVC__onMouseOver(e) { e.preventDefault(); e.stopPropagation(); this.arcball.mouseOver(); }, /** * Called when the mouse leaves the visualization bounds. */ - onMouseOut: function TVC_onMouseOut(e) + _onMouseOut: function TVC__onMouseOut(e) { e.preventDefault(); e.stopPropagation(); this.arcball.mouseOut(); }, /** * Called when the mouse wheel is used. */ - onMozScroll: function TVC_onMozScroll(e) + _onMozScroll: function TVC__onMozScroll(e) { e.preventDefault(); e.stopPropagation(); this.arcball.zoom(e.detail); }, /** * Called when a key is pressed. */ - onKeyDown: function TVC_onKeyDown(e) + _onKeyDown: function TVC__onKeyDown(e) { let code = e.keyCode || e.which; if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) { e.preventDefault(); e.stopPropagation(); this.arcball.keyDown(code); } else { this.arcball.cancelKeyEvents(); } }, /** * Called when a key is released. */ - onKeyUp: function TVC_onKeyUp(e) + _onKeyUp: function TVC__onKeyUp(e) { let code = e.keyCode || e.which; if (code === e.DOM_VK_X) { this.presenter.deleteNode(); } + if (code === e.DOM_VK_F) { + let highlight = this.presenter._highlight; + let zoom = this.presenter.transforms.zoom; + + this.arcball.moveIntoView(vec3.lerp( + vec3.scale(highlight.v0, zoom, []), + vec3.scale(highlight.v1, zoom, []), 0.5)); + } if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) { e.preventDefault(); e.stopPropagation(); this.arcball.keyUp(code); } }, /** * Called when a key is pressed. */ - onKeyPress: function TVC_onKeyPress(e) + _onKeyPress: function TVC__onKeyPress(e) { let tilt = this.presenter.chromeWindow.Tilt; if (e.keyCode === e.DOM_VK_ESCAPE) { e.preventDefault(); e.stopPropagation(); tilt.destroy(tilt.currentWindowId, true); } }, /** * Called when the canvas looses focus. */ - onBlur: function TVC_onBlur(e) { + _onBlur: function TVC__onBlur(e) { this.arcball.cancelKeyEvents(); }, /** * Called when the content window of the current browser is resized. */ - onResize: function TVC_onResize(e) + _onResize: function TVC__onResize(e) { let zoom = this.presenter.chromeWindow.InspectorUI.highlighter.zoom; let width = e.target.innerWidth * zoom; let height = e.target.innerHeight * zoom; this.arcball.resize(width, height); }, @@ -1293,23 +1352,24 @@ TiltVisualizer.Controller.prototype = { isInitialized: function TVC_isInitialized() { return this.arcball ? true : false; }, /** * Function called when this object is destroyed. */ - finalize: function TVC_finalize() + _finalize: function TVC__finalize() { TiltUtils.destroyObject(this.arcball); - TiltUtils.destroyObject(this.coordinates); + TiltUtils.destroyObject(this._coordinates); this.removeEventListeners(); - this.presenter.ondraw = null; + this.presenter.controller = null; + this.presenter._controllerUpdate = null; } }; /** * This is a general purpose 3D rotation controller described by Ken Shoemake * in the Graphics Interface ’92 Proceedings. It features good behavior * easy implementation, cheap execution. * @@ -1389,19 +1449,22 @@ TiltVisualizer.Arcball = function TV_Arc TiltVisualizer.Arcball.prototype = { /** * Call this function whenever you need the updated rotation quaternion * and the zoom amount. These values will be returned as "rotation" and * "translation" properties inside an object. * + * @param {Number} aDelta + * the current animation frame delta + * * @return {Object} the rotation quaternion and the translation amount */ - update: function TVA_update() + update: function TVA_update(aDelta) { let mousePress = this._mousePress; let mouseRelease = this._mouseRelease; let mouseMove = this._mouseMove; let mouseLerp = this._mouseLerp; let mouseButton = this._mouseButton; // smoothly update the mouse coordinates @@ -1429,17 +1492,17 @@ TiltVisualizer.Arcball.prototype = { // left mouse button handles rotation if (mouseButton === 1 || this._rotating) { // the rotation doesn't stop immediately after the left mouse button is // released, so add a flag to smoothly continue it until it ends this._rotating = true; // find the sphere coordinates of the mouse positions - this.pointToSphere(x, y, this.width, this.height, this.radius, endVec); + this._pointToSphere(x, y, this.width, this.height, this.radius, endVec); // compute the vector perpendicular to the start & end vectors vec3.cross(startVec, endVec, pVec); // if the begin and end vectors don't coincide if (vec3.length(pVec) > 0) { deltaRot[0] = pVec[0]; deltaRot[1] = pVec[1]; @@ -1549,21 +1612,29 @@ TiltVisualizer.Arcball.prototype = { (additionalRot[2] - deltaAdditionalRot[2]) * ARCBALL_SENSITIVITY; deltaAdditionalTrans[0] += (additionalTrans[0] - deltaAdditionalTrans[0]) * ARCBALL_SENSITIVITY; deltaAdditionalTrans[1] += (additionalTrans[1] - deltaAdditionalTrans[1]) * ARCBALL_SENSITIVITY; // create an additional rotation based on the key events - quat4.fromEuler(deltaAdditionalRot[0], deltaAdditionalRot[1], 0, deltaRot); + quat4.fromEuler( + deltaAdditionalRot[0], + deltaAdditionalRot[1], + deltaAdditionalRot[2], deltaRot); // create an additional translation based on the key events vec3.set([deltaAdditionalTrans[0], deltaAdditionalTrans[1], 0], deltaTrans); + // handle the reset animation steps if necessary + if (this._resetInProgress) { + this._nextResetStep(aDelta || 1); + } + // return the current rotation and translation return { rotation: quat4.multiply(deltaRot, currentRot), translation: vec3.add(deltaTrans, currentTrans) }; }, /** @@ -1578,21 +1649,21 @@ TiltVisualizer.Arcball.prototype = { * which mouse button was pressed */ mouseDown: function TVA_mouseDown(x, y, aButton) { // save the mouse down state and prepare for rotations or translations this._mousePress[0] = x; this._mousePress[1] = y; this._mouseButton = aButton; - this._cancelResetInterval(); + this._cancelReset(); this._save(); // find the sphere coordinates of the mouse positions - this.pointToSphere( + this._pointToSphere( x, y, this.width, this.height, this.radius, this._startVec); quat4.set(this._currentRot, this._lastRot); }, /** * Function handling the mouseUp event. * Call this when a mouse button was released. @@ -1654,31 +1725,31 @@ TiltVisualizer.Arcball.prototype = { * Call this, for example, when the mouse wheel was scrolled or zoom keys * were pressed. * * @param {Number} aZoom * the zoom direction and speed */ zoom: function TVA_zoom(aZoom) { - this._cancelResetInterval(); + this._cancelReset(); this._zoomAmount = TiltMath.clamp(this._zoomAmount - aZoom, ARCBALL_ZOOM_MIN, ARCBALL_ZOOM_MAX); }, /** * Function handling the keyDown event. * Call this when a key was pressed. * * @param {Number} aCode * the code corresponding to the key pressed */ keyDown: function TVA_keyDown(aCode) { - this._cancelResetInterval(); + this._cancelReset(); this._keyCode[aCode] = true; }, /** * Function handling the keyUp event. * Call this when a key was released. * * @param {Number} aCode @@ -1700,17 +1771,17 @@ TiltVisualizer.Arcball.prototype = { * the width of canvas * @param {Number} aHeight * the height of canvas * @param {Number} aRadius * optional, the radius of the arcball * @param {Array} aSphereVec * a 3d vector to store the sphere coordinates */ - pointToSphere: function TVA_pointToSphere( + _pointToSphere: function TVA__pointToSphere( x, y, aWidth, aHeight, aRadius, aSphereVec) { // adjust point coords and scale down to range of [-1..1] x = (x - aWidth * 0.5) / aRadius; y = (y - aHeight * 0.5) / aRadius; // compute the square length of the vector to the point from the center let normal = 0; @@ -1746,16 +1817,42 @@ TiltVisualizer.Arcball.prototype = { */ cancelMouseEvents: function TVA_cancelMouseEvents() { this._rotating = false; this._mouseButton = -1; }, /** + * Incremental translation method. + * + * @param {Array} aTranslation + * the translation ammount on the [x, y] axis + */ + translate: function TVP_translate(aTranslation) + { + this._additionalTrans[0] += aTranslation[0]; + this._additionalTrans[1] += aTranslation[1]; + }, + + /** + * Incremental rotation method. + * + * @param {Array} aRotation + * the rotation ammount along the [x, y, z] axis + */ + rotate: function TVP_rotate(aRotation) + { + // explicitly rotate along y, x, z values because they're eulerian angles + this._additionalRot[0] += TiltMath.radians(aRotation[1]); + this._additionalRot[1] += TiltMath.radians(aRotation[0]); + this._additionalRot[2] += TiltMath.radians(aRotation[2]); + }, + + /** * Moves a target point into view only if it's outside the currently visible * area bounds (in which case it also resets any additional transforms). * * @param {Arary} aPoint * the [x, y] point which should be brought into view */ moveIntoView: function TVA_moveIntoView(aPoint) { let visiblePointX = -(this._currentTrans[0] + this._additionalTrans[0]); @@ -1798,84 +1895,93 @@ TiltVisualizer.Arcball.prototype = { * * @param {Array} aFinalTranslation * optional, final vector translation * @param {Array} aFinalRotation * optional, final quaternion rotation */ reset: function TVA_reset(aFinalTranslation, aFinalRotation) { - if ("function" === typeof this.onResetStart) { - this.onResetStart(); - this.onResetStart = null; + if ("function" === typeof this._onResetStart) { + this._onResetStart(); + this._onResetStart = null; } - let func = this._nextResetIntervalStep.bind(this); - this.cancelMouseEvents(); this.cancelKeyEvents(); - this._cancelResetInterval(); + this._cancelReset(); this._save(); this._resetFinalTranslation = vec3.create(aFinalTranslation); this._resetFinalRotation = quat4.create(aFinalRotation); - this._resetInterval = - this.chromeWindow.setInterval(func, ARCBALL_RESET_INTERVAL); + this._resetInProgress = true; }, /** * Cancels the current arcball reset animation if there is one. */ - _cancelResetInterval: function TVA__cancelResetInterval() + _cancelReset: function TVA__cancelReset() { - if (this._resetInterval) { - this.chromeWindow.clearInterval(this._resetInterval); - - this._resetInterval = null; + if (this._resetInProgress) { + this._resetInProgress = false; this._save(); - if ("function" === typeof this.onResetFinish) { - this.onResetFinish(); - this.onResetFinish = null; + if ("function" === typeof this._onResetFinish) { + this._onResetFinish(); + this._onResetFinish = null; + this._onResetStep = null; } } }, /** * Executes the next step in the arcball reset animation. + * + * @param {Number} aDelta + * the current animation frame delta */ - _nextResetIntervalStep: function TVA__nextResetIntervalStep() + _nextResetStep: function TVA__nextResetStep(aDelta) { - let fDelta = EPSILON * EPSILON; + // a very large animation frame delta (in case of seriously low framerate) + // would cause all the interpolations to become highly unstable + aDelta = TiltMath.clamp(aDelta, 1, 100); + + let fNearZero = EPSILON * EPSILON; + let fInterpLin = ARCBALL_RESET_LINEAR_FACTOR * aDelta; + let fInterpSph = ARCBALL_RESET_SPHERICAL_FACTOR; let fTran = this._resetFinalTranslation; let fRot = this._resetFinalRotation; let t = vec3.create(fTran); let r = quat4.multiply(quat4.inverse(quat4.create(this._currentRot)), fRot); // reset the rotation quaternion and translation vector - vec3.lerp(this._currentTrans, t, ARCBALL_RESET_FACTOR / 4); - quat4.slerp(this._currentRot, r, 1 - ARCBALL_RESET_FACTOR); + vec3.lerp(this._currentTrans, t, fInterpLin); + quat4.slerp(this._currentRot, r, fInterpSph); // also reset any additional transforms by the keyboard or mouse - vec3.scale(this._additionalTrans, ARCBALL_RESET_FACTOR); - vec3.scale(this._additionalRot, ARCBALL_RESET_FACTOR); - this._zoomAmount *= ARCBALL_RESET_FACTOR; + vec3.scale(this._additionalTrans, fInterpLin); + vec3.scale(this._additionalRot, fInterpLin); + this._zoomAmount *= fInterpLin; // clear the loop if the all values are very close to zero - if (vec3.length(vec3.subtract(this._lastRot, fRot, [])) < fDelta && - vec3.length(vec3.subtract(this._deltaRot, fRot, [])) < fDelta && - vec3.length(vec3.subtract(this._currentRot, fRot, [])) < fDelta && - vec3.length(vec3.subtract(this._lastTrans, fTran, [])) < fDelta && - vec3.length(vec3.subtract(this._deltaTrans, fTran, [])) < fDelta && - vec3.length(vec3.subtract(this._currentTrans, fTran, [])) < fDelta && - vec3.length(this._additionalRot) < fDelta && - vec3.length(this._additionalTrans) < fDelta) { + if (vec3.length(vec3.subtract(this._lastRot, fRot, [])) < fNearZero && + vec3.length(vec3.subtract(this._deltaRot, fRot, [])) < fNearZero && + vec3.length(vec3.subtract(this._currentRot, fRot, [])) < fNearZero && + vec3.length(vec3.subtract(this._lastTrans, fTran, [])) < fNearZero && + vec3.length(vec3.subtract(this._deltaTrans, fTran, [])) < fNearZero && + vec3.length(vec3.subtract(this._currentTrans, fTran, [])) < fNearZero && + vec3.length(this._additionalRot) < fNearZero && + vec3.length(this._additionalTrans) < fNearZero) { - this._cancelResetInterval(); + this._cancelReset(); + } + + if ("function" === typeof this._onResetStep) { + this._onResetStep(); } }, /** * Loads the keys to control this arcball. */ _loadKeys: function TVA__loadKeys() { @@ -1922,19 +2028,19 @@ TiltVisualizer.Arcball.prototype = { this._mouseLerp[0] = x; this._mouseLerp[1] = y; } }, /** * Function called when this object is destroyed. */ - finalize: function TVA_finalize() + _finalize: function TVA__finalize() { - this._cancelResetInterval(); + this._cancelReset(); } }; /** * Tilt configuration preferences. */ TiltVisualizer.Prefs = {
--- a/browser/devtools/tilt/TiltWorkerCrafter.js +++ b/browser/devtools/tilt/TiltWorkerCrafter.js @@ -33,51 +33,65 @@ * decision by deleting the provisions above and replace them with the notice * and other provisions required by the LGPL or the GPL. 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 SIXTEEN_OVER_255 = 16 / 255; -const ONE_OVER_255 = 1 / 255; - /** * Given the initialization data (thickness, sizes and information about * each DOM node) this worker sends back the arrays representing * vertices, texture coords, colors, indices and all the needed data for * rendering the DOM visualization mesh. * * Used in the TiltVisualization.Presenter object. */ self.onmessage = function TWC_onMessage(event) { let data = event.data; + let maxGroupNodes = parseInt(data.maxGroupNodes); let thickness = data.thickness; let style = data.style; let texWidth = data.texWidth; let texHeight = data.texHeight; let nodesInfo = data.nodesInfo; - // create the arrays used to construct the 3D mesh data - let vertices = []; - let texCoord = []; - let color = []; - let stacksIndices = []; - let wireframeIndices = []; - let meshWidth = 0; - let meshHeight = 0; + let mesh = { + allVertices: [], + groups: [], + width: 0, + height: 0 + }; + + let vertices; + let texCoord; + let color; + let stacksIndices; + let wireframeIndices; + let index; // seed the random function to get the same values each time // we're doing this to avoid ugly z-fighting with overlapping nodes self.random.seed(0); // go through all the dom nodes and compute the verts, texcoord etc. - for (let n = 0, i = 0, len = nodesInfo.length; n < len; n++) { + for (let n = 0, len = nodesInfo.length; n < len; n++) { + + // check if we need to start creating a new group + if (n % maxGroupNodes === 0) { + vertices = []; // recreate the arrays used to construct the 3D mesh data + texCoord = []; + color = []; + stacksIndices = []; + wireframeIndices = []; + index = 0; + } + let info = nodesInfo[n]; let depth = info.depth; let coord = info.coord; // calculate the stack x, y, z, width and height coordinates let z = depth * thickness; let y = coord.top; let x = coord.left; @@ -150,16 +164,17 @@ self.onmessage = function TWC_onMessage( g10, g11, g12, g10, g11, g12, g10, g11, g12, g20, g21, g22, g20, g21, g22, g20, g21, g22, g20, g21, g22); + let i = index; // number of vertex points, used to create the indices array let ip1 = i + 1; let ip2 = ip1 + 1; let ip3 = ip2 + 1; let ip4 = ip3 + 1; let ip5 = ip4 + 1; let ip6 = ip5 + 1; let ip7 = ip6 + 1; let ip8 = ip7 + 1; @@ -177,33 +192,38 @@ self.onmessage = function TWC_onMessage( // compute the wireframe indices if (depth !== 0) { wireframeIndices.unshift(i, ip1, ip1, ip2, ip2, ip3, ip3, i, ip8, i, ip9, ip1, ip11, ip3, ip10, ip2); } - // number of vertex points, used for creating the indices array - i += 12; // a vertex has 3 coords: x, y and z + // there are 12 vertices in a stack representing a node + index += 12; // set the maximum mesh width and height to calculate the center offset - meshWidth = Math.max(w, meshWidth); - meshHeight = Math.max(h, meshHeight); + mesh.width = Math.max(w, mesh.width); + mesh.height = Math.max(h, mesh.height); + + // check if we need to save the currently active group; this happens after + // we filled all the "slots" in a group or there aren't any remaining nodes + if (((n + 1) % maxGroupNodes === 0) || (n === len - 1)) { + mesh.groups.push({ + vertices: vertices, + texCoord: texCoord, + color: color, + stacksIndices: stacksIndices, + wireframeIndices: wireframeIndices + }); + mesh.allVertices = mesh.allVertices.concat(vertices); + } } - self.postMessage({ - vertices: vertices, - texCoord: texCoord, - color: color, - stacksIndices: stacksIndices, - wireframeIndices: wireframeIndices, - meshWidth: meshWidth, - meshHeight: meshHeight - }); + self.postMessage(mesh); close(); }; /** * Utility functions for generating random numbers using the Alea algorithm. */ self.random = {
--- a/browser/devtools/tilt/test/Makefile.in +++ b/browser/devtools/tilt/test/Makefile.in @@ -75,16 +75,17 @@ include $(topsrcdir)/config/rules.mk browser_tilt_math06.js \ browser_tilt_math07.js \ browser_tilt_picking.js \ browser_tilt_picking_delete.js \ browser_tilt_picking_highlight01-offs.js \ browser_tilt_picking_highlight01.js \ browser_tilt_picking_highlight02.js \ browser_tilt_picking_highlight03.js \ + browser_tilt_picking_miv.js \ browser_tilt_utils01.js \ browser_tilt_utils02.js \ browser_tilt_utils03.js \ browser_tilt_utils04.js \ browser_tilt_utils05.js \ browser_tilt_utils06.js \ browser_tilt_visualizer.js \ browser_tilt_zoom.js \
--- a/browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js +++ b/browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js @@ -57,21 +57,33 @@ function performTest(canvas, arcball, ca EventUtils.synthesizeKey("VK_S", { type: "keyup" }); EventUtils.synthesizeKey("VK_RIGHT", { type: "keyup" }); // ok, transformations finished, we can now try to reset the model view window.setTimeout(function() { info("Synthesizing arcball reset key press."); - arcball.onResetStart = function() { + arcball._onResetStart = function() { info("Starting arcball reset animation."); }; - arcball.onResetFinish = function() { + arcball._onResetStep = function() { + info("\nlastRot: " + quat4.str(arcball._lastRot) + + "\ndeltaRot: " + quat4.str(arcball._deltaRot) + + "\ncurrentRot: " + quat4.str(arcball._currentRot) + + "\nlastTrans: " + vec3.str(arcball._lastTrans) + + "\ndeltaTrans: " + vec3.str(arcball._deltaTrans) + + "\ncurrentTrans: " + vec3.str(arcball._currentTrans) + + "\nadditionalRot: " + vec3.str(arcball._additionalRot) + + "\nadditionalTrans: " + vec3.str(arcball._additionalTrans) + + "\nzoomAmount: " + arcball._zoomAmount); + }; + + arcball._onResetFinish = function() { ok(isApproxVec(arcball._lastRot, [0, 0, 0, 1]), "The arcball _lastRot field wasn't reset correctly."); ok(isApproxVec(arcball._deltaRot, [0, 0, 0, 1]), "The arcball _deltaRot field wasn't reset correctly."); ok(isApproxVec(arcball._currentRot, [0, 0, 0, 1]), "The arcball _currentRot field wasn't reset correctly."); ok(isApproxVec(arcball._lastTrans, [0, 0, 0]), @@ -84,18 +96,20 @@ function performTest(canvas, arcball, ca ok(isApproxVec(arcball._additionalRot, [0, 0, 0]), "The arcball _additionalRot field wasn't reset correctly."); ok(isApproxVec(arcball._additionalTrans, [0, 0, 0]), "The arcball _additionalTrans field wasn't reset correctly."); ok(isApproxVec([arcball._zoomAmount], [0]), "The arcball _zoomAmount field wasn't reset correctly."); - info("Finishing arcball reset test."); - callback(); + executeSoon(function() { + info("Finishing arcball reset test."); + callback(); + }); }; EventUtils.synthesizeKey("VK_R", { type: "keydown" }); }, Math.random() * 1000); // leave enough time for transforms to happen }, Math.random() * 1000); }, Math.random() * 1000); }
--- a/browser/devtools/tilt/test/browser_tilt_arcball-reset.js +++ b/browser/devtools/tilt/test/browser_tilt_arcball-reset.js @@ -55,21 +55,33 @@ function performTest(canvas, arcball, ca EventUtils.synthesizeKey("VK_W", { type: "keyup" }); EventUtils.synthesizeKey("VK_LEFT", { type: "keyup" }); // ok, transformations finished, we can now try to reset the model view window.setTimeout(function() { info("Synthesizing arcball reset key press."); - arcball.onResetStart = function() { + arcball._onResetStart = function() { info("Starting arcball reset animation."); }; - arcball.onResetFinish = function() { + arcball._onResetStep = function() { + info("\nlastRot: " + quat4.str(arcball._lastRot) + + "\ndeltaRot: " + quat4.str(arcball._deltaRot) + + "\ncurrentRot: " + quat4.str(arcball._currentRot) + + "\nlastTrans: " + vec3.str(arcball._lastTrans) + + "\ndeltaTrans: " + vec3.str(arcball._deltaTrans) + + "\ncurrentTrans: " + vec3.str(arcball._currentTrans) + + "\nadditionalRot: " + vec3.str(arcball._additionalRot) + + "\nadditionalTrans: " + vec3.str(arcball._additionalTrans) + + "\nzoomAmount: " + arcball._zoomAmount); + }; + + arcball._onResetFinish = function() { ok(isApproxVec(arcball._lastRot, [0, 0, 0, 1]), "The arcball _lastRot field wasn't reset correctly."); ok(isApproxVec(arcball._deltaRot, [0, 0, 0, 1]), "The arcball _deltaRot field wasn't reset correctly."); ok(isApproxVec(arcball._currentRot, [0, 0, 0, 1]), "The arcball _currentRot field wasn't reset correctly."); ok(isApproxVec(arcball._lastTrans, [0, 0, 0]), @@ -82,18 +94,20 @@ function performTest(canvas, arcball, ca ok(isApproxVec(arcball._additionalRot, [0, 0, 0]), "The arcball _additionalRot field wasn't reset correctly."); ok(isApproxVec(arcball._additionalTrans, [0, 0, 0]), "The arcball _additionalTrans field wasn't reset correctly."); ok(isApproxVec([arcball._zoomAmount], [0]), "The arcball _zoomAmount field wasn't reset correctly."); - info("Finishing arcball reset test."); - callback(); + executeSoon(function() { + info("Finishing arcball reset test."); + callback(); + }); }; EventUtils.synthesizeKey("VK_R", { type: "keydown" }); }, Math.random() * 1000); // leave enough time for transforms to happen }, Math.random() * 1000); }, Math.random() * 1000); }
--- a/browser/devtools/tilt/test/browser_tilt_arcball.js +++ b/browser/devtools/tilt/test/browser_tilt_arcball.js @@ -43,20 +43,20 @@ function test() { "The second arcball height wasn't set correctly."); is(arcball2.radius, 654, "The second arcball radius wasn't implicitly set correctly."); let arcball3 = new TiltVisualizer.Arcball(window, 512, 512); let sphereVec = vec3.create(); - arcball3.pointToSphere(123, 456, 256, 512, 512, sphereVec); + arcball3._pointToSphere(123, 456, 256, 512, 512, sphereVec); ok(isApproxVec(sphereVec, [-0.009765625, 0.390625, 0.9204980731010437]), - "The pointToSphere() function didn't map the coordinates correctly."); + "The _pointToSphere() function didn't map the coordinates correctly."); let stack1 = []; let expect1 = [ { rotation: [ -0.08877250552177429, 0.0242881178855896, -0.04222869873046875, -0.9948599338531494], translation: [0, 0, 0] }, { rotation: [
--- a/browser/devtools/tilt/test/browser_tilt_controller.js +++ b/browser/devtools/tilt/test/browser_tilt_controller.js @@ -42,40 +42,40 @@ function test() { function testEventCancel(cancellingEvent) { is(document.activeElement, canvas, "The visualizer canvas should be focused when performing this test."); EventUtils.synthesizeKey("VK_A", { type: "keydown" }); EventUtils.synthesizeKey("VK_LEFT", { type: "keydown" }); - instance.controller.update(); + instance.controller._update(); ok(!isEqualVec(tran(), prev_tran), "After a translation key is pressed, the vector should change."); ok(!isEqualVec(rot(), prev_rot), "After a rotation key is pressed, the quaternion should change."); save(); cancellingEvent(); - instance.controller.update(); + instance.controller._update(); ok(!isEqualVec(tran(), prev_tran), "Even if the canvas lost focus, the vector has some inertia."); ok(!isEqualVec(rot(), prev_rot), "Even if the canvas lost focus, the quaternion has some inertia."); save(); while (!isEqualVec(tran(), prev_tran) || !isEqualVec(rot(), prev_rot)) { - instance.controller.update(); + instance.controller._update(); save(); } ok(isEqualVec(tran(), prev_tran) && isEqualVec(rot(), prev_rot), "After focus lost, the transforms inertia eventually stops."); } info("Setting typeaheadfind to true.");
--- a/browser/devtools/tilt/test/browser_tilt_picking.js +++ b/browser/devtools/tilt/test/browser_tilt_picking.js @@ -16,25 +16,23 @@ function test() { createTab(function() { createTilt({ onTiltOpen: function(instance) { let presenter = instance.presenter; let canvas = presenter.canvas; - presenter.onSetupMesh = function() { + presenter._onSetupMesh = function() { presenter.pickNode(canvas.width / 2, 10, { onpick: function(data) { ok(data.index > 0, "Simply picking a node didn't work properly."); - ok(!presenter.highlight.disabled, - "After only picking a node, it shouldn't be highlighted."); Services.obs.addObserver(cleanup, DESTROYED, false); InspectorUI.closeInspectorUI(); } }); }; } });
--- a/browser/devtools/tilt/test/browser_tilt_picking_delete.js +++ b/browser/devtools/tilt/test/browser_tilt_picking_delete.js @@ -18,45 +18,45 @@ function test() { createTab(function() { createTilt({ onTiltOpen: function(instance) { presenter = instance.presenter; Services.obs.addObserver(whenNodeRemoved, NODE_REMOVED, false); - presenter.onSetupMesh = function() { + presenter._onSetupMesh = function() { presenter.highlightNodeAt(presenter.canvas.width / 2, 10, { onpick: function() { ok(presenter._currentSelection > 0, "Highlighting a node didn't work properly."); - ok(!presenter.highlight.disabled, + ok(!presenter._highlight.disabled, "After highlighting a node, it should be highlighted. D'oh."); presenter.deleteNode(); } }); }; } }); }); } function whenNodeRemoved() { ok(presenter._currentSelection > 0, "Deleting a node shouldn't change the current selection."); - ok(presenter.highlight.disabled, + ok(presenter._highlight.disabled, "After deleting a node, it shouldn't be highlighted."); let nodeIndex = presenter._currentSelection; - let meshData = presenter.meshData; + let vertices = presenter._meshStacks[0].vertices.components; for (let i = 0, k = 36 * nodeIndex; i < 36; i++) { - is(meshData.vertices[i + k], 0, + is(vertices[i + k], 0, "The stack vertices weren't degenerated properly."); } executeSoon(function() { Services.obs.addObserver(cleanup, DESTROYED, false); InspectorUI.closeInspectorUI(); }); }
--- a/browser/devtools/tilt/test/browser_tilt_picking_highlight01-offs.js +++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight01-offs.js @@ -19,45 +19,45 @@ function test() { createTab(function() { createTilt({ onTiltOpen: function(instance) { presenter = instance.presenter; Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false); - presenter.onInitializationFinished = function() { + presenter._onInitializationFinished = function() { let contentDocument = presenter.contentWindow.document; let div = contentDocument.getElementById("far-far-away"); presenter.highlightNode(div, "moveIntoView"); }; } }); }); } function whenHighlighting() { ok(presenter._currentSelection > 0, "Highlighting a node didn't work properly."); - ok(!presenter.highlight.disabled, + ok(!presenter._highlight.disabled, "After highlighting a node, it should be highlighted. D'oh."); - ok(presenter.controller.arcball._resetInterval, + ok(presenter.controller.arcball._resetInProgress, "Highlighting a node that's not already visible should trigger a reset!"); executeSoon(function() { Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false); presenter.highlightNode(null); }); } function whenUnhighlighting() { ok(presenter._currentSelection < 0, "Unhighlighting a should remove the current selection."); - ok(presenter.highlight.disabled, + ok(presenter._highlight.disabled, "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh."); executeSoon(function() { Services.obs.addObserver(cleanup, DESTROYED, false); InspectorUI.closeInspectorUI(); }); }
--- a/browser/devtools/tilt/test/browser_tilt_picking_highlight01.js +++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight01.js @@ -18,45 +18,45 @@ function test() { createTab(function() { createTilt({ onTiltOpen: function(instance) { presenter = instance.presenter; Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false); - presenter.onSetupMesh = function() { + presenter._onSetupMesh = function() { let contentDocument = presenter.contentWindow.document; let div = contentDocument.getElementById("first-law"); presenter.highlightNode(div, "moveIntoView"); }; } }); }); } function whenHighlighting() { ok(presenter._currentSelection > 0, "Highlighting a node didn't work properly."); - ok(!presenter.highlight.disabled, + ok(!presenter._highlight.disabled, "After highlighting a node, it should be highlighted. D'oh."); - ok(!presenter.controller.arcball._resetInterval, + ok(!presenter.controller.arcball._resetInProgress, "Highlighting a node that's already visible shouldn't trigger a reset."); executeSoon(function() { Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false); presenter.highlightNode(null); }); } function whenUnhighlighting() { ok(presenter._currentSelection < 0, "Unhighlighting a should remove the current selection."); - ok(presenter.highlight.disabled, + ok(presenter._highlight.disabled, "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh."); executeSoon(function() { Services.obs.addObserver(cleanup, DESTROYED, false); InspectorUI.closeInspectorUI(); }); }
--- a/browser/devtools/tilt/test/browser_tilt_picking_highlight02.js +++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight02.js @@ -18,40 +18,40 @@ function test() { createTab(function() { createTilt({ onTiltOpen: function(instance) { presenter = instance.presenter; Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false); - presenter.onSetupMesh = function() { + presenter._onSetupMesh = function() { presenter.highlightNodeAt(presenter.canvas.width / 2, 10); }; } }); }); } function whenHighlighting() { ok(presenter._currentSelection > 0, "Highlighting a node didn't work properly."); - ok(!presenter.highlight.disabled, + ok(!presenter._highlight.disabled, "After highlighting a node, it should be highlighted. D'oh."); executeSoon(function() { Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false); presenter.highlightNodeAt(-1, -1); }); } function whenUnhighlighting() { ok(presenter._currentSelection < 0, "Unhighlighting a should remove the current selection."); - ok(presenter.highlight.disabled, + ok(presenter._highlight.disabled, "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh."); executeSoon(function() { Services.obs.addObserver(cleanup, DESTROYED, false); InspectorUI.closeInspectorUI(); }); }
--- a/browser/devtools/tilt/test/browser_tilt_picking_highlight03.js +++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight03.js @@ -18,40 +18,40 @@ function test() { createTab(function() { createTilt({ onTiltOpen: function(instance) { presenter = instance.presenter; Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false); - presenter.onSetupMesh = function() { + presenter._onSetupMesh = function() { presenter.highlightNodeFor(5); // 1 = html, 2 = body, 3 = first div }; } }); }); } function whenHighlighting() { ok(presenter._currentSelection > 0, "Highlighting a node didn't work properly."); - ok(!presenter.highlight.disabled, + ok(!presenter._highlight.disabled, "After highlighting a node, it should be highlighted. D'oh."); executeSoon(function() { Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false); presenter.highlightNodeFor(-1); }); } function whenUnhighlighting() { ok(presenter._currentSelection < 0, "Unhighlighting a should remove the current selection."); - ok(presenter.highlight.disabled, + ok(presenter._highlight.disabled, "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh."); executeSoon(function() { Services.obs.addObserver(cleanup, DESTROYED, false); InspectorUI.closeInspectorUI(); }); }
new file mode 100644 --- /dev/null +++ b/browser/devtools/tilt/test/browser_tilt_picking_miv.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +let presenter; + +function test() { + if (!isTiltEnabled()) { + info("Skipping highlight test because Tilt isn't enabled."); + return; + } + if (!isWebGLSupported()) { + info("Skipping highlight test because WebGL isn't supported."); + return; + } + + requestLongerTimeout(10); + waitForExplicitFinish(); + + createTab(function() { + createTilt({ + onTiltOpen: function(instance) + { + presenter = instance.presenter; + Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false); + + presenter._onInitializationFinished = function() { + let contentDocument = presenter.contentWindow.document; + let div = contentDocument.getElementById("far-far-away"); + + presenter.highlightNode(div); + }; + } + }); + }); +} + +function whenHighlighting() { + ok(presenter._currentSelection > 0, + "Highlighting a node didn't work properly."); + ok(!presenter._highlight.disabled, + "After highlighting a node, it should be highlighted. D'oh."); + ok(!presenter.controller.arcball._resetInProgress, + "Highlighting a node that's not already visible shouldn't trigger a reset " + + "without this being explicitly requested!"); + + EventUtils.sendKey("F"); + executeSoon(whenBringingIntoView); +} + +function whenBringingIntoView() { + ok(presenter._currentSelection > 0, + "The node should still be selected."); + ok(!presenter._highlight.disabled, + "The node should still be highlighted"); + ok(presenter.controller.arcball._resetInProgress, + "Highlighting a node that's not already visible should trigger a reset " + + "when this is being explicitly requested!"); + + executeSoon(function() { + Services.obs.addObserver(cleanup, DESTROYED, false); + InspectorUI.closeInspectorUI(); + }); +} + +function cleanup() { + Services.obs.removeObserver(whenHighlighting, HIGHLIGHTING); + Services.obs.removeObserver(cleanup, DESTROYED); + gBrowser.removeCurrentTab(); + finish(); +}
--- a/browser/devtools/tilt/test/browser_tilt_utils05.js +++ b/browser/devtools/tilt/test/browser_tilt_utils05.js @@ -1,16 +1,12 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; -let tmp = {}; -Cu.import("resource:///modules/devtools/LayoutHelpers.jsm", tmp); -let LayoutHelpers = tmp.LayoutHelpers; - function init(callback) { let iframe = gBrowser.ownerDocument.createElement("iframe"); iframe.addEventListener("load", function onLoad() { iframe.removeEventListener("load", onLoad, true); callback(iframe); gBrowser.parentNode.removeChild(iframe);
--- a/browser/devtools/tilt/test/browser_tilt_utils06.js +++ b/browser/devtools/tilt/test/browser_tilt_utils06.js @@ -6,17 +6,17 @@ let someObject = { a: 1, func: function() { this.b = 2; } }; let anotherObject = { - finalize: function() + _finalize: function() { someObject.c = 3; } }; function test() { ok(TiltUtils, "The TiltUtils object doesn't exist.");
--- a/browser/devtools/tilt/test/browser_tilt_visualizer.js +++ b/browser/devtools/tilt/test/browser_tilt_visualizer.js @@ -14,17 +14,16 @@ function test() { let webGLError = false; let webGLLoad = false; let visualizer = new TiltVisualizer({ chromeWindow: window, contentWindow: gBrowser.selectedBrowser.contentWindow, parentNode: gBrowser.selectedBrowser.parentNode, - requestAnimationFrame: window.mozRequestAnimationFrame, inspectorUI: window.InspectorUI, onError: function onWebGLError() { webGLError = true; }, onLoad: function onWebGLLoad() @@ -66,59 +65,59 @@ function test() { "The visualizer presenter wasn't destroyed."); is(visualizer.controller, undefined, "The visualizer controller wasn't destroyed."); is(visualizer.canvas, undefined, "The visualizer canvas wasn't destroyed."); } function testPresenter(presenter) { - ok(presenter.renderer, + ok(presenter._renderer, "The presenter renderer wasn't initialized properly."); - ok(presenter.visualizationProgram, + ok(presenter._visualizationProgram, "The presenter visualizationProgram wasn't initialized properly."); - ok(presenter.texture, + ok(presenter._texture, "The presenter texture wasn't initialized properly."); - ok(!presenter.meshStacks, + ok(!presenter._meshStacks, "The presenter meshStacks shouldn't be initialized yet."); - ok(!presenter.meshWireframe, + ok(!presenter._meshWireframe, "The presenter meshWireframe shouldn't be initialized yet."); - ok(presenter.traverseData, + ok(presenter._traverseData, "The presenter nodesInformation wasn't initialized properly."); - ok(presenter.highlight, + ok(presenter._highlight, "The presenter highlight wasn't initialized properly."); - ok(presenter.highlight.disabled, - "The presenter highlight should be initially disabled"); - ok(isApproxVec(presenter.highlight.v0, [0, 0, 0]), + ok(presenter._highlight.disabled, + "The presenter highlight should be initially disabled."); + ok(isApproxVec(presenter._highlight.v0, [0, 0, 0]), "The presenter highlight first vertex should be initially zeroed."); - ok(isApproxVec(presenter.highlight.v1, [0, 0, 0]), + ok(isApproxVec(presenter._highlight.v1, [0, 0, 0]), "The presenter highlight second vertex should be initially zeroed."); - ok(isApproxVec(presenter.highlight.v2, [0, 0, 0]), + ok(isApproxVec(presenter._highlight.v2, [0, 0, 0]), "The presenter highlight third vertex should be initially zeroed."); - ok(isApproxVec(presenter.highlight.v3, [0, 0, 0]), + ok(isApproxVec(presenter._highlight.v3, [0, 0, 0]), "The presenter highlight fourth vertex should be initially zeroed."); ok(presenter.transforms, "The presenter transforms wasn't initialized properly."); - ok(isApproxVec(presenter.transforms.zoom, 1), + is(presenter.transforms.zoom, 1, "The presenter transforms zoom should be initially 1."); ok(isApproxVec(presenter.transforms.offset, [0, 0, 0]), "The presenter transforms offset should be initially zeroed."); ok(isApproxVec(presenter.transforms.translation, [0, 0, 0]), "The presenter transforms translation should be initially zeroed."); ok(isApproxVec(presenter.transforms.rotation, [0, 0, 0, 1]), "The presenter transforms rotation should be initially set to identity."); presenter.setTranslation([1, 2, 3]); presenter.setRotation([5, 6, 7, 8]); ok(isApproxVec(presenter.transforms.translation, [1, 2, 3]), "The presenter transforms translation wasn't modified as it should"); ok(isApproxVec(presenter.transforms.rotation, [5, 6, 7, 8]), "The presenter transforms rotation wasn't modified as it should"); - ok(presenter.redraw, + ok(presenter._redraw, "The new transforms should have issued a redraw request."); } function testController(controller) { ok(controller.arcball, "The controller arcball wasn't initialized properly."); ok(!controller.coordinates, "The presenter meshWireframe shouldn't be initialized yet.");
--- a/browser/devtools/tilt/test/browser_tilt_zoom.js +++ b/browser/devtools/tilt/test/browser_tilt_zoom.js @@ -30,17 +30,17 @@ function test() { ok(isApprox(instance.presenter.transforms.zoom, ZOOM), "The presenter transforms zoom wasn't initially set correctly."); let contentWindow = gBrowser.selectedBrowser.contentWindow; let initialWidth = contentWindow.innerWidth; let initialHeight = contentWindow.innerHeight; - let renderer = instance.presenter.renderer; + let renderer = instance.presenter._renderer; let arcball = instance.controller.arcball; ok(isApprox(contentWindow.innerWidth * ZOOM, renderer.width, 1), "The renderer width wasn't set correctly before the resize."); ok(isApprox(contentWindow.innerHeight * ZOOM, renderer.height, 1), "The renderer height wasn't set correctly before the resize."); ok(isApprox(contentWindow.innerWidth * ZOOM, arcball.width, 1),
--- a/browser/devtools/tilt/test/head.js +++ b/browser/devtools/tilt/test/head.js @@ -2,25 +2,27 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; let tempScope = {}; Components.utils.import("resource:///modules/devtools/TiltGL.jsm", tempScope); Components.utils.import("resource:///modules/devtools/TiltMath.jsm", tempScope); Components.utils.import("resource:///modules/devtools/TiltUtils.jsm", tempScope); Components.utils.import("resource:///modules/devtools/TiltVisualizer.jsm", tempScope); +Components.utils.import("resource:///modules/devtools/LayoutHelpers.jsm", tempScope); let TiltGL = tempScope.TiltGL; let EPSILON = tempScope.EPSILON; let TiltMath = tempScope.TiltMath; let vec3 = tempScope.vec3; let mat3 = tempScope.mat3; let mat4 = tempScope.mat4; let quat4 = tempScope.quat4; let TiltUtils = tempScope.TiltUtils; let TiltVisualizer = tempScope.TiltVisualizer; +let LayoutHelpers = tempScope.LayoutHelpers; const DEFAULT_HTML = "data:text/html," + "<DOCTYPE html>" + "<html>" + "<head>" + "<title>Three Laws</title>" + "</head>" +
new file mode 100644 --- /dev/null +++ b/browser/devtools/webconsole/GcliTiltCommands.jsm @@ -0,0 +1,224 @@ +/* ***** 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 GCLI Commands. + * + * 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): + * Victor Porof <vporof@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 ***** */ + + +let EXPORTED_SYMBOLS = [ "GcliCommands" ]; + +Components.utils.import("resource:///modules/gcli.jsm"); +Components.utils.import("resource:///modules/HUDService.jsm"); + + +/** + * 'tilt' command + */ +gcli.addCommand({ + name: 'tilt', + description: gcli.lookup("tiltDesc"), + manual: gcli.lookup("tiltManual") +}); + + +/** + * 'tilt open' command + */ +gcli.addCommand({ + name: 'tilt open', + description: gcli.lookup("tiltOpenDesc"), + manual: gcli.lookup("tiltOpenManual"), + params: [ + { + name: "node", + type: "node", + defaultValue: null, + description: gcli.lookup("inspectNodeDesc"), + manual: gcli.lookup("inspectNodeManual") + } + ], + exec: function(args, context) { + let chromeWindow = context.environment.chromeDocument.defaultView; + let InspectorUI = chromeWindow.InspectorUI; + let Tilt = chromeWindow.Tilt; + + if (Tilt.currentInstance) { + Tilt.update(args.node); + } else { + let hudId = chromeWindow.HUDConsoleUI.getOpenHUD(); + let hud = HUDService.getHudReferenceById(hudId); + + if (hud && !hud.consolePanel) { + HUDService.deactivateHUDForContext(chromeWindow.gBrowser.selectedTab); + } + InspectorUI.openInspectorUI(args.node); + Tilt.initialize(); + } + } +}); + + +/** + * 'tilt translate' command + */ +gcli.addCommand({ + name: 'tilt translate', + description: gcli.lookup("tiltTranslateDesc"), + manual: gcli.lookup("tiltTranslateManual"), + params: [ + { + name: "x", + type: "number", + defaultValue: 0, + description: gcli.lookup("tiltTranslateXDesc"), + manual: gcli.lookup("tiltTranslateXManual") + }, + { + name: "y", + type: "number", + defaultValue: 0, + description: gcli.lookup("tiltTranslateYDesc"), + manual: gcli.lookup("tiltTranslateYManual") + } + ], + exec: function(args, context) { + let chromeWindow = context.environment.chromeDocument.defaultView; + let Tilt = chromeWindow.Tilt; + + if (Tilt.currentInstance) { + Tilt.currentInstance.controller.arcball.translate([args.x, args.y]); + } + } +}); + + +/** + * 'tilt rotate' command + */ +gcli.addCommand({ + name: 'tilt rotate', + description: gcli.lookup("tiltRotateDesc"), + manual: gcli.lookup("tiltRotateManual"), + params: [ + { + name: "x", + type: { name: 'number', min: -360, max: 360, step: 10 }, + defaultValue: 0, + description: gcli.lookup("tiltRotateXDesc"), + manual: gcli.lookup("tiltRotateXManual") + }, + { + name: "y", + type: { name: 'number', min: -360, max: 360, step: 10 }, + defaultValue: 0, + description: gcli.lookup("tiltRotateYDesc"), + manual: gcli.lookup("tiltRotateYManual") + }, + { + name: "z", + type: { name: 'number', min: -360, max: 360, step: 10 }, + defaultValue: 0, + description: gcli.lookup("tiltRotateZDesc"), + manual: gcli.lookup("tiltRotateZManual") + } + ], + exec: function(args, context) { + let chromeWindow = context.environment.chromeDocument.defaultView; + let Tilt = chromeWindow.Tilt; + + if (Tilt.currentInstance) { + Tilt.currentInstance.controller.arcball.rotate([args.x, args.y, args.z]); + } + } +}); + + +/** + * 'tilt zoom' command + */ +gcli.addCommand({ + name: 'tilt zoom', + description: gcli.lookup("tiltZoomDesc"), + manual: gcli.lookup("tiltZoomManual"), + params: [ + { + name: "zoom", + type: { name: 'number' }, + description: gcli.lookup("tiltZoomAmountDesc"), + manual: gcli.lookup("tiltZoomAmountManual") + } + ], + exec: function(args, context) { + let chromeWindow = context.environment.chromeDocument.defaultView; + let Tilt = chromeWindow.Tilt; + + if (Tilt.currentInstance) { + Tilt.currentInstance.controller.arcball.zoom(-args.zoom); + } + } +}); + + +/** + * 'tilt reset' command + */ +gcli.addCommand({ + name: 'tilt reset', + description: gcli.lookup("tiltResetDesc"), + manual: gcli.lookup("tiltResetManual"), + exec: function(args, context) { + let chromeWindow = context.environment.chromeDocument.defaultView; + let Tilt = chromeWindow.Tilt; + + if (Tilt.currentInstance) { + Tilt.currentInstance.controller.arcball.reset(); + } + } +}); + + +/** + * 'tilt close' command + */ +gcli.addCommand({ + name: 'tilt close', + description: gcli.lookup("tiltCloseDesc"), + manual: gcli.lookup("tiltCloseManual"), + exec: function(args, context) { + let chromeWindow = context.environment.chromeDocument.defaultView; + let Tilt = chromeWindow.Tilt; + + Tilt.destroy(Tilt.currentWindowId); + } +});
--- a/browser/devtools/webconsole/HUDService.jsm +++ b/browser/devtools/webconsole/HUDService.jsm @@ -159,16 +159,17 @@ function LogFactory(aMessagePrefix) * modules. In general there is no reason when JSMs need to export symbols * except when they need the host environment to inform them of things like the * current window/document/etc. */ function loadCommands() { let commandExports = {}; Cu.import("resource:///modules/GcliCommands.jsm", commandExports); + Cu.import("resource:///modules/GcliTiltCommands.jsm", commandExports); return commandExports; } let log = LogFactory("*** HUDService:"); const HUD_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
--- a/browser/devtools/webconsole/Makefile.in +++ b/browser/devtools/webconsole/Makefile.in @@ -45,16 +45,17 @@ VPATH = @srcdir@ include $(DEPTH)/config/autoconf.mk EXTRA_JS_MODULES = \ PropertyPanel.jsm \ NetworkHelper.jsm \ AutocompletePopup.jsm \ gcli.jsm \ GcliCommands.jsm \ + GcliTiltCommands.jsm \ $(NULL) EXTRA_PP_JS_MODULES = \ HUDService.jsm \ $(NULL) TEST_DIRS = test
--- a/browser/devtools/webconsole/test/browser_gcli_web.js +++ b/browser/devtools/webconsole/test/browser_gcli_web.js @@ -1098,17 +1098,16 @@ exports.testBlank = function() { }; exports.testIncompleteMultiMatch = function() { update({ typed: 't', cursor: { start: 1, end: 1 } }); test.is( 'I', statuses); test.is(Status.ERROR, status); test.is(-1, assignC.paramIndex); test.ok(assignC.getPredictions().length > 0); - test.ok(assignC.getPredictions().length < 20); // could break ... verifyPredictionsContains('tsv', assignC.getPredictions()); verifyPredictionsContains('tsr', assignC.getPredictions()); test.is(null, requ.commandAssignment.getValue()); }; exports.testIncompleteSingleMatch = function() { update({ typed: 'tselar', cursor: { start: 6, end: 6 } }); test.is( 'IIIIII', statuses);
--- a/browser/locales/en-US/chrome/browser/browser.dtd +++ b/browser/locales/en-US/chrome/browser/browser.dtd @@ -208,21 +208,29 @@ can reach it easily. --> - "Scratchpad" in your locale. You should feel free to find a close - approximation to it or choose a word (or words) that means - "simple discardable text editor". --> <!ENTITY scratchpad.label "Scratchpad"> <!ENTITY scratchpad.accesskey "s"> <!ENTITY scratchpad.keycode "VK_F4"> <!ENTITY scratchpad.keytext "F4"> -<!ENTITY inspectPanelTitle.label "HTML"> <!ENTITY inspectButton.label "Inspect"> <!ENTITY inspectButton.accesskey "I"> <!ENTITY inspectCloseButton.tooltiptext "Close Inspector"> +<!ENTITY inspectorHTMLCopyInner.label "Copy Inner HTML"> +<!ENTITY inspectorHTMLCopyInner.accesskey "I"> + +<!ENTITY inspectorHTMLCopyOuter.label "Copy Outer HTML"> +<!ENTITY inspectorHTMLCopyOuter.accesskey "O"> + +<!ENTITY inspectorHTMLDelete.label "Delete Node"> +<!ENTITY inspectorHTMLDelete.accesskey "D"> + <!-- LOCALIZATION NOTE (inspect3DViewButton.label): This button shows an - alternate view for the Inspector, creating a 3D visualization of the - webpage. --> <!ENTITY inspect3DViewButton.label "3D View"> <!ENTITY inspect3DViewButton.accesskey "W"> <!ENTITY inspectStyleButton.label "Style"> <!ENTITY inspectStyleButton.accesskey "S"> @@ -594,8 +602,20 @@ just addresses the organization to follo <!ENTITY syncSetup.label "Set Up &syncBrand.shortName.label;…"> <!ENTITY syncSetup.accesskey "Y"> <!ENTITY syncSyncNowItem.label "Sync Now"> <!ENTITY syncSyncNowItem.accesskey "S"> <!ENTITY syncToolbarButton.label "Sync"> <!ENTITY addonBarCloseButton.tooltip "Close Add-on Bar"> <!ENTITY toggleAddonBarCmd.key "/"> + +<!-- LOCALIZATION NOTE (htmlPanel.label): This is a label for a button that +activates the Web Developer->Inspect UI's HTML Tree Panel. --> +<!ENTITY htmlPanel.label "HTML"> + +<!-- LOCALIZATION NOTE (htmlPanel.tooltiptext): The text that appears when a user +hovers over the HTML panel's toolbar button. --> +<!ENTITY htmlPanel.tooltiptext "HTML panel"> + +<!-- LOCALIZATION NOTE (htmlPanel.accesskey): The key bound to the HTML panel's +toolbar button --> +<!ENTITY htmlPanel.accesskey "H">
--- a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties +++ b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties @@ -51,16 +51,146 @@ inspectManual=Investigate the dimensions # when the user is using this command. inspectNodeDesc=CSS selector # LOCALIZATION NOTE (inspectNodeManual) A fuller description of the 'node' # parameter to the 'inspect' command, displayed when the user asks for help # on what it does. inspectNodeManual=A CSS selector for use with Document.querySelector which identifies a single element +# LOCALIZATION NOTE (tiltDesc) A very short description of the 'tilt' +# command. See tiltManual for a fuller description of what it does. This +# string is designed to be shown in a menu alongside the command name, which +# is why it should be as short as possible. +tiltDesc=Visualize the webpage in 3D + +# LOCALIZATION NOTE (tiltManual) A fuller description of the 'tilt' +# command, displayed when the user asks for help on what it does. +tiltManual=Investigate the relationship between various parts of a webpage and their ancestors in a 3D environment + +# LOCALIZATION NOTE (tiltOpenDesc) A very short description of the 'tilt inspect' +# command. See tiltOpenManual for a fuller description of what it does. This +# string is designed to be shown in a menu alongside the command name, which +# is why it should be as short as possible. +tiltOpenDesc=Open the Inspector 3D view + +# LOCALIZATION NOTE (tiltOpenManual) A fuller description of the 'tilt translate' +# command, displayed when the user asks for help on what it does. +tiltOpenManual=Initialize the 3D page inspector and optionally highlight a node using a CSS selector + +# LOCALIZATION NOTE (tiltTranslateDesc) A very short description of the 'tilt translate' +# command. See tiltTranslateManual for a fuller description of what it does. This +# string is designed to be shown in a menu alongside the command name, which +# is why it should be as short as possible. +tiltTranslateDesc=Move the webpage mesh + +# LOCALIZATION NOTE (tiltTranslateManual) A fuller description of the 'tilt translate' +# command, displayed when the user asks for help on what it does. +tiltTranslateManual=Incrementally translate the webpage mesh in a certain direction + +# LOCALIZATION NOTE (tiltTranslateXDesc) A very short string to describe the +# 'x' parameter to the 'tilt translate' command, which is displayed in a dialog +# when the user is using this command. +tiltTranslateXDesc=X (pixels) + +# LOCALIZATION NOTE (tiltTranslateXManual) A fuller description of the 'x' +# parameter to the 'translate' command, displayed when the user asks for help +# on what it does. +tiltTranslateXManual=The ammount in pixels to translate the webpage mesh on the X axis + +# LOCALIZATION NOTE (tiltTranslateYDesc) A very short string to describe the +# 'y' parameter to the 'tilt translate' command, which is displayed in a dialog +# when the user is using this command. +tiltTranslateYDesc=Y (pixels) + +# LOCALIZATION NOTE (tiltTranslateYManual) A fuller description of the 'y' +# parameter to the 'translate' command, displayed when the user asks for help +# on what it does. +tiltTranslateYManual=The ammount in pixels to translate the webpage mesh on the Y axis + +# LOCALIZATION NOTE (tiltRotateDesc) A very short description of the 'tilt rotate' +# command. See tiltRotateManual for a fuller description of what it does. This +# string is designed to be shown in a menu alongside the command name, which +# is why it should be as short as possible. +tiltRotateDesc=Spin the webpage mesh + +# LOCALIZATION NOTE (tiltRotateManual) A fuller description of the 'tilt rotate' +# command, displayed when the user asks for help on what it does. +tiltRotateManual=Incrementally rotate the webpage mesh in a certain direction + +# LOCALIZATION NOTE (tiltRotateXDesc) A very short string to describe the +# 'x' parameter to the 'tilt rotate' command, which is displayed in a dialog +# when the user is using this command. +tiltRotateXDesc=X (degrees) + +# LOCALIZATION NOTE (tiltRotateXManual) A fuller description of the 'x' +# parameter to the 'rotate' command, displayed when the user asks for help +# on what it does. +tiltRotateXManual=The ammount in degrees to rotate the webpage mesh along the X axis + +# LOCALIZATION NOTE (tiltRotateYDesc) A very short string to describe the +# 'y' parameter to the 'tilt rotate' command, which is displayed in a dialog +# when the user is using this command. +tiltRotateYDesc=Y (degrees) + +# LOCALIZATION NOTE (tiltRotateYManual) A fuller description of the 'y' +# parameter to the 'rotate' command, displayed when the user asks for help +# on what it does. +tiltRotateYManual=The ammount in degrees to rotate the webpage mesh along the Y axis + +# LOCALIZATION NOTE (tiltRotateZDesc) A very short string to describe the +# 'z' parameter to the 'tilt rotate' command, which is displayed in a dialog +# when the user is using this command. +tiltRotateZDesc=Z (degrees) + +# LOCALIZATION NOTE (tiltRotateZManual) A fuller description of the 'z' +# parameter to the 'rotate' command, displayed when the user asks for help +# on what it does. +tiltRotateZManual=The ammount in degrees to rotate the webpage mesh along the Z axis + +# LOCALIZATION NOTE (tiltZoomDesc) A very short description of the 'tilt zoom' +# command. See tiltZoomManual for a fuller description of what it does. This +# string is designed to be shown in a menu alongside the command name, which +# is why it should be as short as possible. +tiltZoomDesc=Move away or towards the webpage mesh + +# LOCALIZATION NOTE (tiltZoomManual) A fuller description of the 'tilt zoom' +# command, displayed when the user asks for help on what it does. +tiltZoomManual=Incrementally move the webpage mesh in a certain direction along the Z axis + +# LOCALIZATION NOTE (tiltZoomAmountDesc) A very short string to describe the +# 'zoom' parameter to the 'tilt zoom' command, which is displayed in a dialog +# when the user is using this command. +tiltZoomAmountDesc=Zoom (pixels) + +# LOCALIZATION NOTE (tiltZoomAmmuntManual) A fuller description of the 'zoom' +# parameter to the 'zoom' command, displayed when the user asks for help +# on what it does. +tiltZoomAmountManual=The amount in pixels to translate the webpage mesh along the Z axis + +# LOCALIZATION NOTE (tiltResetDesc) A very short description of the 'tilt reset' +# command. See tiltResetManual for a fuller description of what it does. This +# string is designed to be shown in a menu alongside the command name, which +# is why it should be as short as possible. +tiltResetDesc=Reset the translation, rotation and zoom + +# LOCALIZATION NOTE (tiltResetManual) A fuller description of the 'tilt reset' +# command, displayed when the user asks for help on what it does. +tiltResetManual=Resets any transformations applied to the webpage mesh modelview matrix + +# LOCALIZATION NOTE (tiltCloseDesc) A very short description of the 'tilt close' +# command. See tiltCloseManual for a fuller description of what it does. This +# string is designed to be shown in a menu alongside the command name, which +# is why it should be as short as possible. +tiltCloseDesc=Close the visualization if open + +# LOCALIZATION NOTE (tiltCloseManual) A fuller description of the 'tilt close' +# command, displayed when the user asks for help on what it does. +tiltCloseManual=Close the visualization and switch back to the Inspector default highlighter + # LOCALIZATION NOTE (breakDesc) A very short string used to describe the # function of the break command. breakDesc=Manage breakpoints # LOCALIZATION NOTE (breakManual) A longer description describing the # set of commands that control breakpoints. breakManual=Commands to list, add and remove breakpoints
--- a/browser/locales/en-US/chrome/browser/devtools/inspector.properties +++ b/browser/locales/en-US/chrome/browser/devtools/inspector.properties @@ -15,25 +15,13 @@ confirmNavigationAway.buttonLeave=Leave confirmNavigationAway.buttonLeaveAccesskey=L confirmNavigationAway.buttonStay=Stay on Page confirmNavigationAway.buttonStayAccesskey=S breadcrumbs.siblings=Siblings # LOCALIZATION NOTE (htmlPanel): Used in the Inspector tool's openInspectorUI # method when registering the HTML panel. -# LOCALIZATION NOTE (htmlPanel.label): This is a lable for a button that -# activates the Web Developer->Inspect UI's HTML Tree Panel. -htmlPanel.label=HTML - -# LOCALIZATION NOTE (htmlPanel.tooltiptext): The text that appears when a user -# hovers over the HTML panel's toolbar button. -htmlPanel.tooltiptext=HTML panel - -# LOCALIZATION NOTE (htmlPanel.accesskey): The key bound to the HTML panel's -# toolbar button. -htmlPanel.accesskey=H - # LOCALIZATION NOTE (ruleView.*): Button label, accesskey and tooltip text # associated with the Highlighter's CSS Rule View in the Style Sidebar. ruleView.label=Rules ruleView.accesskey=R -ruleView.tooltiptext=View and Edit CSS \ No newline at end of file +ruleView.tooltiptext=View and Edit CSS
--- a/browser/themes/gnomestripe/browser.css +++ b/browser/themes/gnomestripe/browser.css @@ -1987,56 +1987,47 @@ panel[dimmed="true"] { } /* Highlighter toolbar */ #inspector-toolbar { border-top: 1px solid hsla(210, 8%, 5%, .65); } -#inspector-toolbar[treepanel-open] { - padding-top: 0; -} - #devtools-side-splitter { -moz-appearance: none; border: 0; -moz-border-start: 1px solid #242b33; min-width: 0; width: 3px; background-color: transparent; -moz-margin-end: -3px; position: relative; } #devtools-sidebar-box { background-color: -moz-Field; } -/* Highlighter - toolbar resizer */ - -#inspector-top-resizer { - -moz-appearance: none; - cursor: n-resize; - background: none; - height: 4px; -} - /* Highlighter - Node Infobar */ /* Highlighter - Node Infobar - text */ html|*#highlighter-nodeinfobar-tagname { color: white; } html|*#highlighter-nodeinfobar-id { color: hsl(90, 79%, 52%); } +html|*#highlighter-nodeinfobar-pseudo-classes { + color: hsl(20, 100%, 70%); +} + /* Highlighter - Node Infobar - box & arrow */ #highlighter-nodeinfobar { color: hsl(200, 100%, 65%); border: 1px solid hsla(210, 19%, 63%, .5); border-radius: 3px; padding: 8px 16px; background: -moz-linear-gradient(hsl(209, 18%, 30%), hsl(210, 24%, 16%)) no-repeat padding-box; @@ -2131,21 +2122,29 @@ html|*#highlighter-nodeinfobar-id { .inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-tag { color: hsl(208,100%,60%); } .inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-id { color: hsl(205,100%,70%); } +.inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-pseudo-classes { + color: hsl(20, 100%, 70%); +} + .inspector-breadcrumbs-id, .inspector-breadcrumbs-classes { color: #8d99a6; } +.inspector-breadcrumbs-pseudo-classes { + color: hsl(20, 100%, 85%); +} + /* Highlighter toolbar - breadcrumbs - LTR */ .inspector-breadcrumbs-button:-moz-locale-dir(ltr):first-of-type { margin-left: 0; } .inspector-breadcrumbs-button { -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/ltr-middle.png") 1 13 2 13 fill stretch; @@ -2255,8 +2254,20 @@ html|*#highlighter-nodeinfobar-id { .inspector-breadcrumbs-button:last-of-type[checked]:-moz-locale-dir(rtl) { -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/rtl-end-selected.png") 1 13 2 13 fill stretch; } .inspector-breadcrumbs-button[siblings-menu-open]:last-of-type[checked]:-moz-locale-dir(rtl), .inspector-breadcrumbs-button:last-of-type[checked]:hover:active:-moz-locale-dir(rtl) { -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/rtl-end-selected-pressed.png") 1 13 2 13 fill stretch; } + +/* Highlighter toolbar - HTML Tree */ + +#inspector-tree-splitter { + -moz-appearance: none; + border-top: 1px solid black; + border-bottom-width: 0; + min-height: 3px; + height: 3px; + margin-bottom: -3px; + position: relative; +}
--- a/browser/themes/gnomestripe/devtools/debugger.css +++ b/browser/themes/gnomestripe/devtools/debugger.css @@ -243,16 +243,39 @@ a { -moz-margin-end: 5px; } .arrow[open] { -moz-appearance: treetwistyopen; } /** + * Animations + */ + +.details[open] { + -moz-animation-duration: 0.25s; + -moz-animation-name: showblock; +} + +@-moz-keyframes showblock { + from { + opacity: 0; + -moz-transform-origin: top; + -moz-transform: scaleY(0); + } + + to { + opacity: 1; + -moz-transform-origin: top; + -moz-transform: scaleY(1); + } +} + +/** * Display helpers */ .unselectable { padding-top: 2px; padding-bottom: 2px; }
--- a/browser/themes/gnomestripe/inspector.css +++ b/browser/themes/gnomestripe/inspector.css @@ -54,17 +54,17 @@ html { background-color: -moz-dialog; } body { margin: 0; overflow: auto; font-family: Lucida Grande, sans-serif; font-size: 11px; - border-top: 1px solid #BBB9BA; + padding-top: 5px; } h1 { font-size: 17px; border-bottom: 1px solid threedlightshadow; } a {
--- a/browser/themes/pinstripe/browser.css +++ b/browser/themes/pinstripe/browser.css @@ -2734,58 +2734,47 @@ panel[dimmed="true"] { padding-right: 16px; /* use -moz-padding-end when/if bug 631729 gets fixed */ } #inspector-toolbar:-moz-locale-dir(rtl) { padding-left: 4px; padding-right: 18px; /* use -moz-padding-end when/if bug 631729 gets fixed */ } -#inspector-toolbar[treepanel-open] { - padding-top: 0; - padding-right: 0; - -moz-padding-end: 4px; -} - #devtools-side-splitter { background-image: none !important; border: 0; -moz-border-start: 1px solid #242b33; min-width: 0; width: 3px; background-color: transparent; -moz-margin-end: -3px; position: relative; } #devtools-sidebar-box { background-color: -moz-Field; } -/* Highlighter - toolbar resizer */ - -#inspector-top-resizer { - -moz-appearance: none; - cursor: n-resize; - background: none; - height: 4px; -} - /* Highlighter - Node Infobar */ /* Highlighter - Node Infobar - text */ html|*#highlighter-nodeinfobar-tagname { color: white; } html|*#highlighter-nodeinfobar-id { color: hsl(90, 79%, 52%); } +html|*#highlighter-nodeinfobar-pseudo-classes { + color: hsl(20, 100%, 70%); +} + /* Highlighter - Node Infobar - box & arrow */ #highlighter-nodeinfobar { color: hsl(200, 100%, 65%); border: 1px solid hsla(210, 19%, 63%, .5); border-radius: 3px; padding: 8px 16px; background: -moz-linear-gradient(hsl(209, 18%, 30%), hsl(210, 24%, 16%)) no-repeat padding-box; @@ -2874,21 +2863,29 @@ html|*#highlighter-nodeinfobar-id { .inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-tag { color: hsl(208,100%,60%); } .inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-id { color: hsl(205,100%,70%); } +.inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-pseudo-classes { + color: hsl(20, 100%, 70%); +} + .inspector-breadcrumbs-id, .inspector-breadcrumbs-classes { color: #8d99a6; } +.inspector-breadcrumbs-pseudo-classes { + color: hsl(20, 100%, 85%); +} + /* Highlighter toolbar - breadcrumbs - LTR */ .inspector-breadcrumbs-button:-moz-locale-dir(ltr):first-of-type { margin-left: 0; } .inspector-breadcrumbs-button { -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/ltr-middle.png") 1 13 2 13 fill stretch; @@ -2998,8 +2995,20 @@ html|*#highlighter-nodeinfobar-id { .inspector-breadcrumbs-button:last-of-type[checked]:-moz-locale-dir(rtl) { -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/rtl-end-selected.png") 1 13 2 13 fill stretch; } .inspector-breadcrumbs-button[siblings-menu-open]:last-of-type[checked]:-moz-locale-dir(rtl), .inspector-breadcrumbs-button:last-of-type[checked]:hover:active:-moz-locale-dir(rtl) { -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/rtl-end-selected-pressed.png") 1 13 2 13 fill stretch; } + +/* Highlighter toolbar - HTML Tree */ + +#inspector-tree-splitter { + -moz-appearance: none; + border-top: 1px solid black; + border-bottom-width: 0; + min-height: 3px; + height: 3px; + margin-bottom: -3px; + position: relative; +}
--- a/browser/themes/pinstripe/devtools/debugger.css +++ b/browser/themes/pinstripe/devtools/debugger.css @@ -241,16 +241,39 @@ a { -moz-appearance: treetwisty; } .arrow[open] { -moz-appearance: treetwistyopen; } /** + * Animations + */ + +.details[open] { + -moz-animation-duration: 0.25s; + -moz-animation-name: showblock; +} + +@-moz-keyframes showblock { + from { + opacity: 0; + -moz-transform-origin: top; + -moz-transform: scaleY(0); + } + + to { + opacity: 1; + -moz-transform-origin: top; + -moz-transform: scaleY(1); + } +} + +/** * Display helpers */ .unselectable { padding-top: 4px; } .info {
--- a/browser/themes/pinstripe/inspector.css +++ b/browser/themes/pinstripe/inspector.css @@ -54,17 +54,17 @@ html { background-color: -moz-dialog; } body { margin: 0; overflow: auto; font-family: Lucida Grande, sans-serif; font-size: 11px; - border-top: 1px solid #BBB9BA; + padding-top: 5px; } h1 { font-size: 17px; border-bottom: 1px solid threedlightshadow; } a {
--- a/browser/themes/winstripe/browser.css +++ b/browser/themes/winstripe/browser.css @@ -2682,55 +2682,46 @@ panel[dimmed="true"] { } /* Highlighter toolbar */ #inspector-toolbar { border-top: 1px solid hsla(211,68%,6%,.65) !important; } -#inspector-toolbar[treepanel-open] { - padding-top: 0; -} - #devtools-side-splitter { border: 0; -moz-border-start: 1px solid #242b33; min-width: 0; width: 3px; background-color: transparent; -moz-margin-end: -3px; position: relative; } #devtools-sidebar-box { background-color: -moz-Field; } -/* Highlighter - toolbar resizer */ - -#inspector-top-resizer { - -moz-appearance: none; - cursor: n-resize; - background: none; - height: 4px; -} - /* Highlighter - Node Infobar */ /* Highlighter - Node Infobar - text */ html|*#highlighter-nodeinfobar-tagname { color: white; } html|*#highlighter-nodeinfobar-id { color: hsl(90, 79%, 52%); } +html|*#highlighter-nodeinfobar-pseudo-classes { + color: hsl(20, 100%, 70%); +} + /* Highlighter - Node Infobar - box & arrow */ #highlighter-nodeinfobar { color: hsl(200, 100%, 65%); border: 1px solid hsla(210, 19%, 63%, .5); border-radius: 3px; padding: 8px 16px; background: -moz-linear-gradient(hsl(209, 18%, 30%), hsl(210, 24%, 16%)) no-repeat padding-box; @@ -2825,21 +2816,29 @@ html|*#highlighter-nodeinfobar-id { .inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-tag { color: hsl(200,100%,60%); } .inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-id { color: hsl(200,100%,70%); } +.inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-pseudo-classes { + color: hsl(20, 100%, 70%); +} + .inspector-breadcrumbs-id, .inspector-breadcrumbs-classes { color: #8d99a6; } +.inspector-breadcrumbs-pseudo-classes { + color: hsl(20, 100%, 85%); +} + /* Highlighter toolbar - breadcrumbs - LTR */ .inspector-breadcrumbs-button:-moz-locale-dir(ltr):first-of-type { margin-left: 0; } .inspector-breadcrumbs-button { -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/ltr-middle.png") 2 13 2 13 fill stretch; @@ -2949,8 +2948,20 @@ html|*#highlighter-nodeinfobar-id { .inspector-breadcrumbs-button:last-of-type[checked]:-moz-locale-dir(rtl) { -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/rtl-end-selected.png") 2 13 2 13 fill stretch; } .inspector-breadcrumbs-button[siblings-menu-open]:last-of-type[checked]:-moz-locale-dir(rtl), .inspector-breadcrumbs-button:last-of-type[checked]:hover:active:-moz-locale-dir(rtl) { -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/rtl-end-selected-pressed.png") 2 13 2 13 fill stretch; } + +/* Highlighter toolbar - HTML Tree */ + +#inspector-tree-splitter { + -moz-appearance: none; + border-top: 1px solid black; + border-bottom-width: 0; + min-height: 3px; + height: 3px; + margin-bottom: -3px; + position: relative; +}
--- a/browser/themes/winstripe/devtools/debugger.css +++ b/browser/themes/winstripe/devtools/debugger.css @@ -245,16 +245,39 @@ a { background: url("chrome://global/skin/tree/twisty-clsd.png") center center no-repeat; } .arrow[open] { background-image: url("chrome://global/skin/tree/twisty-open.png"); } /** + * Animations + */ + +.details[open] { + -moz-animation-duration: 0.25s; + -moz-animation-name: showblock; +} + +@-moz-keyframes showblock { + from { + opacity: 0; + -moz-transform-origin: top; + -moz-transform: scaleY(0); + } + + to { + opacity: 1; + -moz-transform-origin: top; + -moz-transform: scaleY(1); + } +} + +/** * Display helpers */ .unselectable { padding-top: 2px; padding-bottom: 2px; }
--- a/browser/themes/winstripe/inspector.css +++ b/browser/themes/winstripe/inspector.css @@ -54,16 +54,17 @@ html { background-color: -moz-dialog; } body { margin: 0; overflow: auto; font-family: Lucida Grande, sans-serif; font-size: 11px; + padding-top: 5px; } h1 { font-size: 17px; border-bottom: 1px solid threedlightshadow; } a {
--- a/build/automation.py.in +++ b/build/automation.py.in @@ -749,17 +749,17 @@ user_pref("camino.use_system_proxy_setti # We should have a "crashinject" program in our utility path crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe")) if os.path.exists(crashinject) and subprocess.Popen([crashinject, str(proc.pid)]).wait() == 0: return #TODO: kill the process such that it triggers Breakpad on OS X (bug 525296) self.log.info("Can't trigger Breakpad, just killing process") proc.kill() - def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath): + def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath, logger): """ Look for timeout or crashes and return the status after the process terminates """ stackFixerProcess = None stackFixerFunction = None didTimeout = False hitMaxTime = False if proc.stdout is None: self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection") else: @@ -783,16 +783,18 @@ user_pref("camino.use_system_proxy_setti # This method is preferred for developer machines, so we don't have to run "make buildsymbols". stackFixerProcess = self.Process([self.PERL, os.path.join(utilityPath, "fix-linux-stack.pl")], stdin=logsource, stdout=subprocess.PIPE) logsource = stackFixerProcess.stdout (line, didTimeout) = self.readWithTimeout(logsource, timeout) while line != "" and not didTimeout: + if logger: + logger.log(line) if "TEST-START" in line and "|" in line: self.lastTestSeen = line.split("|")[1].strip() if stackFixerFunction: line = stackFixerFunction(line) self.log.info(line.rstrip().decode("UTF-8", "ignore")) if not debuggerInfo and not self.haveDumpedScreen and "TEST-UNEXPECTED-FAIL" in line and "Test timed out" in line: self.dumpScreen(utilityPath) @@ -872,17 +874,17 @@ user_pref("camino.use_system_proxy_setti self.log.info("TEST-UNEXPECTED-FAIL | automation.py | child process %d still alive after shutdown", processPID) self.killPid(processPID) def checkForCrashes(self, profileDir, symbolsPath): automationutils.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath, self.lastTestSeen) def runApp(self, testURL, env, app, profileDir, extraArgs, runSSLTunnel = False, utilityPath = None, - xrePath = None, certPath = None, + xrePath = None, certPath = None, logger = None, debuggerInfo = None, symbolsPath = None, timeout = -1, maxTime = None): """ Run the app, log the duration it took to execute, return the status code. Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds. """ if utilityPath == None: @@ -931,17 +933,17 @@ user_pref("camino.use_system_proxy_setti self.lastTestSeen = "automation.py" proc = self.Process([cmd] + args, env = self.environment(env, xrePath = xrePath, crashreporter = not debuggerInfo), stdout = outputPipe, stderr = subprocess.STDOUT) self.log.info("INFO | automation.py | Application pid: %d", proc.pid) - status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath) + status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath, logger) self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime)) # Do a final check for zombie child processes. self.checkForZombies(processLog) self.checkForCrashes(profileDir, symbolsPath) if os.path.exists(processLog): os.unlink(processLog)
--- a/build/automationutils.py +++ b/build/automationutils.py @@ -35,28 +35,30 @@ # the terms of any one of the MPL, the GPL or the LGPL. # # ***** END LICENSE BLOCK ***** */ from __future__ import with_statement import glob, logging, os, platform, shutil, subprocess, sys, tempfile, urllib2, zipfile import re from urlparse import urlparse +from operator import itemgetter __all__ = [ "ZipFileReader", "addCommonOptions", "checkForCrashes", "dumpLeakLog", "isURL", "processLeakLog", "getDebuggerInfo", "DEBUGGER_INFO", "replaceBackSlashes", "wrapCommand", + "ShutdownLeakLogger" ] # Map of debugging programs to information about them, like default arguments # and whether or not they are interactive. DEBUGGER_INFO = { # gdb requires that you supply the '--args' flag in order to pass arguments # after the executable name to the executable. "gdb": { @@ -445,8 +447,111 @@ def wrapCommand(cmd): binary. """ if platform.system() == "Darwin" and \ hasattr(platform, 'mac_ver') and \ platform.mac_ver()[0][:4] < '10.6': return ["arch", "-arch", "i386"] + cmd # otherwise just execute the command normally return cmd + +class ShutdownLeakLogger(object): + """ + Parses the mochitest run log when running a debug build, assigns all leaked + DOM windows (that are still around after test suite shutdown, despite running + the GC) to the tests that created them and prints leak statistics. + """ + MAX_LEAK_COUNT = 120 + + def __init__(self, logger): + self.logger = logger + self.tests = [] + self.leakedWindows = {} + self.leakedDocShells = set() + self.currentTest = None + self.seenShutdown = False + + def log(self, line): + if line[2:11] == "DOMWINDOW": + self._logWindow(line) + elif line[2:10] == "DOCSHELL": + self._logDocShell(line) + elif line.startswith("TEST-START"): + fileName = line.split(" ")[-1].strip().replace("chrome://mochitests/content/browser/", "") + self.currentTest = {"fileName": fileName, "windows": set(), "docShells": set()} + elif line.startswith("INFO TEST-END"): + # don't track a test if no windows or docShells leaked + if self.currentTest["windows"] and self.currentTest["docShells"]: + self.tests.append(self.currentTest) + self.currentTest = None + elif line.startswith("INFO TEST-START | Shutdown"): + self.seenShutdown = True + + def parse(self): + leakingTests = self._parseLeakingTests() + + if leakingTests: + totalWindows = sum(len(test["leakedWindows"]) for test in leakingTests) + totalDocShells = sum(len(test["leakedDocShells"]) for test in leakingTests) + msgType = "INFO" if totalWindows + totalDocShells < self.MAX_LEAK_COUNT else "UNEXPECTED-FAIL" + self.logger.info("TEST-%s | ShutdownLeaks | leaked %d DOMWindow(s) and %d DocShell(s) until shutdown", msgType, totalWindows, totalDocShells) + + for test in leakingTests: + self.logger.info("\n[%s]", test["fileName"]) + + for url, count in self._zipLeakedWindows(test["leakedWindows"]): + self.logger.info(" %d window(s) [url = %s]", count, url) + + if test["leakedDocShells"]: + self.logger.info(" %d docShell(s)", len(test["leakedDocShells"])) + + def _logWindow(self, line): + created = line[:2] == "++" + id = self._parseValue(line, "serial") + + if self.currentTest: + windows = self.currentTest["windows"] + if created: + windows.add(id) + else: + windows.discard(id) + elif self.seenShutdown and not created: + self.leakedWindows[id] = self._parseValue(line, "url") + + def _logDocShell(self, line): + created = line[:2] == "++" + id = self._parseValue(line, "id") + + if self.currentTest: + docShells = self.currentTest["docShells"] + if created: + docShells.add(id) + else: + docShells.discard(id) + elif self.seenShutdown and not created: + self.leakedDocShells.add(id) + + def _parseValue(self, line, name): + return re.search("\[%s = (.+?)\]" % name, line).group(1) + + def _parseLeakingTests(self): + leakingTests = [] + + for test in self.tests: + test["leakedWindows"] = [self.leakedWindows[id] for id in test["windows"] if id in self.leakedWindows] + test["leakedDocShells"] = [id for id in test["docShells"] if id in self.leakedDocShells] + test["leakCount"] = len(test["leakedWindows"]) + len(test["leakedDocShells"]) + + if test["leakCount"]: + leakingTests.append(test) + + return sorted(leakingTests, key=itemgetter("leakCount"), reverse=True) + + def _zipLeakedWindows(self, leakedWindows): + counts = [] + counted = set() + + for url in leakedWindows: + if not url in counted: + counts.append((url, leakedWindows.count(url))) + counted.add(url) + + return sorted(counts, key=itemgetter(1), reverse=True)
--- a/build/mobile/remoteautomation.py +++ b/build/mobile/remoteautomation.py @@ -91,17 +91,17 @@ class RemoteAutomation(Automation): if crashreporter: env['MOZ_CRASHREPORTER_NO_REPORT'] = '1' env['MOZ_CRASHREPORTER'] = '1' else: env['MOZ_CRASHREPORTER_DISABLE'] = '1' return env - def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsDir): + def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsDir, logger): # maxTime is used to override the default timeout, we should honor that status = proc.wait(timeout = maxTime) print proc.stdout if (status == 1 and self._devicemanager.processExist(proc.procName)): # Then we timed out, make sure Fennec is dead proc.kill()
--- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -780,17 +780,18 @@ nsDocShell::nsDocShell(): if (gDocShellLeakLog) PR_LOG(gDocShellLeakLog, PR_LOG_DEBUG, ("DOCSHELL %p created\n", this)); #endif #ifdef DEBUG // We're counting the number of |nsDocShells| to help find leaks ++gNumberOfDocShells; if (!PR_GetEnv("MOZ_QUIET")) { - printf("++DOCSHELL %p == %ld\n", (void*) this, gNumberOfDocShells); + printf("++DOCSHELL %p == %ld [id = %ld]\n", (void*) this, + gNumberOfDocShells, mHistoryID); } #endif } nsDocShell::~nsDocShell() { Destroy(); @@ -808,17 +809,18 @@ nsDocShell::~nsDocShell() if (gDocShellLeakLog) PR_LOG(gDocShellLeakLog, PR_LOG_DEBUG, ("DOCSHELL %p destroyed\n", this)); #endif #ifdef DEBUG // We're counting the number of |nsDocShells| to help find leaks --gNumberOfDocShells; if (!PR_GetEnv("MOZ_QUIET")) { - printf("--DOCSHELL %p == %ld\n", (void*) this, gNumberOfDocShells); + printf("--DOCSHELL %p == %ld [id = %ld]\n", (void*) this, + gNumberOfDocShells, mHistoryID); } #endif } nsresult nsDocShell::Init() { nsresult rv = nsDocLoader::Init();
--- a/testing/mochitest/runtests.py +++ b/testing/mochitest/runtests.py @@ -670,43 +670,54 @@ class Mochitest(object): # then again to actually run mochitest if options.timeout: timeout = options.timeout + 30 elif not options.autorun: timeout = None else: timeout = 330.0 # default JS harness timeout is 300 seconds + # it's a debug build, we can parse leaked DOMWindows and docShells + if Automation.IS_DEBUG_BUILD: + logger = ShutdownLeakLogger(self.automation.log) + else: + logger = None + if options.vmwareRecording: self.startVMwareRecording(options); self.automation.log.info("INFO | runtests.py | Running tests: start.\n") try: status = self.automation.runApp(testURL, browserEnv, options.app, options.profilePath, options.browserArgs, runSSLTunnel = self.runSSLTunnel, utilityPath = options.utilityPath, xrePath = options.xrePath, certPath=options.certPath, debuggerInfo=debuggerInfo, symbolsPath=options.symbolsPath, + logger = logger, timeout = timeout) except KeyboardInterrupt: self.automation.log.info("INFO | runtests.py | Received keyboard interrupt.\n"); status = -1 except: self.automation.log.info("INFO | runtests.py | Received unexpected exception while running application '%s'\n" % (sys.exc_info()[1])) status = 1 if options.vmwareRecording: self.stopVMwareRecording(); self.stopWebServer(options) self.stopWebSocketServer(options) processLeakLog(self.leak_report_file, options.leakThreshold) + + if logger: + logger.parse() + self.automation.log.info("\nINFO | runtests.py | Running tests: end.") if manifest is not None: self.cleanup(manifest, options) return status def makeTestConfig(self, options): "Creates a test configuration file for customizing test execution."
--- a/toolkit/content/widgets/videocontrols.xml +++ b/toolkit/content/widgets/videocontrols.xml @@ -629,20 +629,25 @@ terminateEventListeners : function () { if (this.statsInterval) { clearInterval(this.statsInterval); this.statsInterval = null; } for each (let event in this.videoEvents) this.video.removeEventListener(event, this, false); + + for each(let element in this.controlListeners) + element.item.removeEventListener(element.event, element.func, false); + + delete this.controlListeners; + this.video.removeEventListener("media-showStatistics", this._handleCustomEventsBound, false); delete this._handleCustomEventsBound; - this.video.ownerDocument.removeEventListener("mozfullscreenchange", this._setFullscreenButtonStateBound, false); - delete this._setFullscreenButtonStateBound; + this.log("--- videocontrols terminated ---"); }, hasError : function () { return (this.video.error != null || this.video.networkState == this.video.NETWORK_NO_SOURCE); }, updateErrorText : function () { @@ -926,16 +931,17 @@ element.hidden = true; }, _triggeredByControls: false, togglePause : function () { if (this.video.paused || this.video.ended) { this._triggeredByControls = true; + this.hideClickToPlay(); this.video.play(); } else { this.video.pause(); } // We'll handle style changes in the event listener for // the "play" and "pause" events, same as if content // script was controlling video playback. @@ -970,32 +976,43 @@ this.fullscreenButton.setAttribute("aria-label", value); if (this.isVideoInFullScreen()) this.fullscreenButton.setAttribute("fullscreened", "true"); else this.fullscreenButton.removeAttribute("fullscreened"); }, - handleClickToPlay : function () { + clickToPlayClickHandler : function(e) { + if (e.button != 0 || this.hasError()) + return; + // Read defaultPrevented asynchronously, since Web content + // may want to consume the "click" event but will only + // receive it after us. + let self = this; + setTimeout(function clickToPlayCallback() { + if (!e.defaultPrevented) + self.togglePause(); + }, 0); + }, + hideClickToPlay : function () { let videoHeight = this.video.clientHeight; let videoWidth = this.video.clientWidth; // The play button will animate to 3x its size. This // shows the animation unless the video is too small // to show 2/3 of the animation. let animationScale = 2; if (this._overlayPlayButtonHeight * animationScale > (videoHeight - this._controlBarHeight)|| this._overlayPlayButtonWidth * animationScale > videoWidth) { this.clickToPlay.setAttribute("immediate", "true"); } else { this.clickToPlay.removeAttribute("immediate"); } this.clickToPlay.setAttribute("fadeout", "true"); - this.togglePause(); }, setPlayButtonState : function(aPaused) { if (aPaused) this.playButton.setAttribute("paused", "true"); else this.playButton.removeAttribute("paused"); @@ -1325,57 +1342,46 @@ // Use the handleEvent() callback for all media events. // The "error" event listener must capture, so that it can trap error events // from the <source> children, which don't bubble. for each (let event in this.videoEvents) this.video.addEventListener(event, this, (event == "error") ? true : false); var self = this; - this.muteButton.addEventListener("command", function() { self.toggleMute(); }, false); - this.playButton.addEventListener("command", function() { self.togglePause(); }, false); - this.fullscreenButton.addEventListener("command", function() { self.toggleFullscreen(); }, false ); - this.clickToPlay.addEventListener("click", function clickToPlayClickHandler(e) { - if (e.button != 0 || self.hasError()) - return; - // Read defaultPrevented asynchronously, since Web content - // may want to consume the "click" event but will only - // receive it after us. - setTimeout(function clickToPlayCallback() { - if (!e.defaultPrevented) - self.handleClickToPlay(); - }, 0); - }, false); + + this.controlListeners = []; - this.controlsSpacer.addEventListener("click", function spacerClickHandler(e) { - if (e.button != 0 || self.hasError()) - return; - // Read defaultPrevented asynchronously, since Web content - // may want to consume the "click" event but will only - // receive it after us (bug 693014). - setTimeout(function togglePauseCallback() { - if (!e.defaultPrevented) - self.togglePause(); - }, 0); - }, false); + // Helper function to add an event listener to the given element + function addListener(elem, eventName, func) { + let boundFunc = func.bind(self); + self.controlListeners.push({ item: elem, event: eventName, func: boundFunc }); + elem.addEventListener(eventName, boundFunc, false); + } + + addListener(this.muteButton, "command", this.toggleMute); + addListener(this.playButton, "command", this.togglePause); + addListener(this.fullscreenButton, "command", this.toggleFullscreen); + addListener(this.clickToPlay, "click", this.clickToPlayClickHandler); + addListener(this.controlsSpacer, "click", this.clickToPlayClickHandler); if (!this.isAudioOnly) { - this.muteButton.addEventListener("mouseover", function(e) { self.onVolumeMouseInOut(e); }, false); - this.muteButton.addEventListener("mouseout", function(e) { self.onVolumeMouseInOut(e); }, false); - this.volumeStack.addEventListener("mouseover", function(e) { self.onVolumeMouseInOut(e); }, false); - this.volumeStack.addEventListener("mouseout", function(e) { self.onVolumeMouseInOut(e); }, false); + addListener(this.muteButton, "mouseover", this.onVolumeMouseInOut); + addListener(this.muteButton, "mouseout", this.onVolumeMouseInOut); + addListener(this.volumeStack, "mouseover", this.onVolumeMouseInOut); + addListener(this.volumeStack, "mouseout", this.onVolumeMouseInOut); } - this.videocontrols.addEventListener("transitionend", function(e) { self.onTransitionEnd(e); }, false); - this._setFullscreenButtonStateBound = this.setFullscreenButtonState.bind(this); - this.video.ownerDocument.addEventListener("mozfullscreenchange", this._setFullscreenButtonStateBound, false); + addListener(this.videocontrols, "transitionend", this.onTransitionEnd); + addListener(this.video.ownerDocument, "mozfullscreenchange", this.setFullscreenButtonState); // Make the <video> element keyboard accessible. this.video.setAttribute("tabindex", 0); - this.video.addEventListener("keypress", function (e) { self.keyHandler(e) }, false); + + addListener(this.video, "keypress", this.keyHandler); this.log("--- videocontrols initialized ---"); } }) ]]> </field> <field readonly="true" name="isTouchControl">false</field>
--- a/toolkit/devtools/debugger/server/dbg-browser-actors.js +++ b/toolkit/devtools/debugger/server/dbg-browser-actors.js @@ -62,24 +62,29 @@ function createRootActor(aConnection) function BrowserRootActor(aConnection) { this.conn = aConnection; this._tabActors = new WeakMap(); this._tabActorPool = null; this._actorFactories = null; this.onTabClosed = this.onTabClosed.bind(this); + this._onWindowCreated = this.onWindowCreated.bind(this); windowMediator.addListener(this); } BrowserRootActor.prototype = { /** * Return a 'hello' packet as specified by the Remote Debugging Protocol. */ sayHello: function BRA_sayHello() { + // Create the tab actor for the selected tab right away so that it gets a + // chance to listen to onNewScript notifications. + this._preInitTabActor(); + return { from: "root", applicationType: "browser", traits: [] }; }, /** * Disconnects the actor from the browser window. */ @@ -109,20 +114,16 @@ BrowserRootActor.prototype = { let actorList = []; // Walk over open browser windows. let e = windowMediator.getEnumerator("navigator:browser"); let selected; while (e.hasMoreElements()) { let win = e.getNext(); - // Watch the window for tab closes so we can invalidate - // actors as needed. - this.watchWindow(win); - // List the tabs in this browser. let selectedBrowser = win.getBrowser().selectedBrowser; let browsers = win.getBrowser().browsers; for each (let browser in browsers) { if (browser == selectedBrowser) { selected = actorList.length; } let actor = this._tabActors.get(browser); @@ -174,33 +175,78 @@ BrowserRootActor.prototype = { * When a tab is closed, exit its tab actor. The actor * will be dropped at the next listTabs request. */ onTabClosed: function BRA_onTabClosed(aEvent) { this.exitTabActor(aEvent.target.linkedBrowser); }, /** + * Handle location changes, by preinitializing a tab actor. + */ + onWindowCreated: function BRA_onWindowCreated(evt) { + if (evt.target === this.browser.contentDocument) { + this._preInitTabActor(); + } + }, + + /** * Exit the tab actor of the specified tab. */ exitTabActor: function BRA_exitTabActor(aWindow) { + this.browser.removeEventListener("DOMWindowCreated", this._onWindowCreated, true); let actor = this._tabActors.get(aWindow); if (actor) { actor.exit(); } }, + /** + * Create the tab actor in the selected tab right away so that it gets a + * chance to listen to onNewScript notifications. + */ + _preInitTabActor: function BRA__preInitTabActor() { + let actorPool = new ActorPool(this.conn); + + // Walk over open browser windows. + let e = windowMediator.getEnumerator("navigator:browser"); + while (e.hasMoreElements()) { + let win = e.getNext(); + + // Watch the window for tab closes so we can invalidate + // actors as needed. + this.watchWindow(win); + + this.browser = win.getBrowser().selectedBrowser; + let actor = this._tabActors.get(this.browser); + if (actor) { + actor._detach(); + } + actor = new BrowserTabActor(this.conn, this.browser); + actor.parentID = this.actorID; + this._tabActors.set(this.browser, actor); + + actorPool.addActor(actor); + } + + this._tabActorPool = actorPool; + this.conn.addActorPool(this._tabActorPool); + + // Watch for globals being created in this tab. + this.browser.addEventListener("DOMWindowCreated", this._onWindowCreated, true); + }, + // nsIWindowMediatorListener onWindowTitleChange: function BRA_onWindowTitleChange(aWindow, aTitle) { }, onOpenWindow: function BRA_onOpenWindow(aWindow) { }, onCloseWindow: function BRA_onCloseWindow(aWindow) { if (aWindow.getBrowser) { this.unwatchWindow(aWindow); } - }, + } } /** * The request types this actor can handle. */ BrowserRootActor.prototype.requestTypes = { "listTabs": BrowserRootActor.prototype.onListTabs }; @@ -215,16 +261,17 @@ BrowserRootActor.prototype.requestTypes * The browser instance that contains this tab. */ function BrowserTabActor(aConnection, aBrowser) { this.conn = aConnection; this._browser = aBrowser; this._onWindowCreated = this.onWindowCreated.bind(this); + this._attach(); } // XXX (bug 710213): BrowserTabActor attach/detach/exit/disconnect is a // *complete* mess, needs to be rethought asap. BrowserTabActor.prototype = { get browser() { return this._browser; }, @@ -285,17 +332,17 @@ BrowserTabActor.prototype = { dbg_assert(!this._tabPool, "Shouldn't have a tab pool if we weren't attached."); this._tabPool = new ActorPool(this.conn); this.conn.addActorPool(this._tabPool); // ... and a pool for context-lifetime actors. this._pushContext(); // Watch for globals being created in this tab. - this.browser.addEventListener("DOMWindowCreated", this._onWindowCreated, true); + this.browser.addEventListener("DOMWindowCreated", this._onWindowCreated, false); this._attached = true; }, /** * Creates a thread actor and a pool for context-lifetime actors. It then sets * up the content window for debugging. */ @@ -326,17 +373,17 @@ BrowserTabActor.prototype = { /** * Does the actual work of detaching from a tab. */ _detach: function BTA_detach() { if (!this.attached) { return; } - this.browser.removeEventListener("DOMWindowCreated", this._onWindowCreated, true); + this.browser.removeEventListener("DOMWindowCreated", this._onWindowCreated, false); this._popContext(); // Shut down actors that belong to this tab's pool. this.conn.removeActorPool(this._tabPool); this._tabPool = null; this._attached = false; @@ -392,18 +439,17 @@ BrowserTabActor.prototype = { */ onWindowCreated: function BTA_onWindowCreated(evt) { if (evt.target === this.browser.contentDocument) { if (this._attached) { this.conn.send({ from: this.actorID, type: "tabNavigated", url: this.browser.contentDocument.URL }); } } - }, - + } }; /** * The request types this actor can handle. */ BrowserTabActor.prototype.requestTypes = { "attach": BrowserTabActor.prototype.onAttach, "detach": BrowserTabActor.prototype.onDetach
--- a/toolkit/devtools/debugger/server/dbg-script-actors.js +++ b/toolkit/devtools/debugger/server/dbg-script-actors.js @@ -94,22 +94,29 @@ ThreadActor.prototype = { // for aGlobal's compartment. Ideally this won't be necessary // medium- to long-term, and will be managed by the engine // instead. if (!this._dbg) { this._dbg = new Debugger(); } + // TODO: Remove this horrible hack when bug 723563 is fixed. + // Make sure that a chrome window is not added as a debuggee when opening + // the debugger in an empty tab or during tests. + if (aGlobal.location && + (aGlobal.location.protocol == "about:" || + aGlobal.location.protocol == "chrome:")) { + return; + } + this.dbg.addDebuggee(aGlobal); this.dbg.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this); this.dbg.onDebuggerStatement = this.onDebuggerStatement.bind(this); this.dbg.onNewScript = this.onNewScript.bind(this); - // Keep the debugger disabled until a client attaches. - this.dbg.enabled = false; }, /** * Remove a debuggee global from the JSInspector. */ removeDebugee: function TA_removeDebuggee(aGlobal) { try { this.dbg.removeDebuggee(aGlobal);
--- a/toolkit/mozapps/extensions/test/browser/browser_bug591465.js +++ b/toolkit/mozapps/extensions/test/browser/browser_bug591465.js @@ -141,16 +141,17 @@ add_test(function() { check_contextmenu(false, true, false, false, false); gContextMenu.hidePopup(); run_next_test(); }, false); info("Opening context menu on enabled extension item"); + el.parentNode.ensureElementIsVisible(el); EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow); EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow); }); add_test(function() { var el = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org"); isnot(el, null, "Should have found addon element"); el.mAddon.userDisabled = true; @@ -160,16 +161,17 @@ add_test(function() { check_contextmenu(false, false, false, false, false); gContextMenu.hidePopup(); run_next_test(); }, false); info("Opening context menu on newly disabled extension item"); + el.parentNode.ensureElementIsVisible(el); EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow); EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow); }); add_test(function() { var el = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org"); isnot(el, null, "Should have found addon element"); el.mAddon.userDisabled = false; @@ -179,16 +181,17 @@ add_test(function() { check_contextmenu(false, true, false, false, false); gContextMenu.hidePopup(); run_next_test(); }, false); info("Opening context menu on newly enabled extension item"); + el.parentNode.ensureElementIsVisible(el); EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow); EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow); }); add_test(function() { var el = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org"); gContextMenu.addEventListener("popupshown", function() { @@ -196,36 +199,38 @@ add_test(function() { check_contextmenu(false, false, false, false, false); gContextMenu.hidePopup(); run_next_test(); }, false); info("Opening context menu on disabled extension item"); + el.parentNode.ensureElementIsVisible(el); EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow); EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow); }); add_test(function() { gManagerWindow.loadView("addons://list/theme"); wait_for_view_load(gManagerWindow, function() { var el = get_addon_element(gManagerWindow, "theme1@tests.mozilla.org"); gContextMenu.addEventListener("popupshown", function() { gContextMenu.removeEventListener("popupshown", arguments.callee, false); - check_contextmenu(true, true, false, false, false); + check_contextmenu(true, true, false, false, false); gContextMenu.hidePopup(); run_next_test(); }, false); info("Opening context menu on enabled theme item"); + el.parentNode.ensureElementIsVisible(el); EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow); EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow); }); }); add_test(function() { var el = get_addon_element(gManagerWindow, "theme2@tests.mozilla.org"); @@ -235,16 +240,17 @@ add_test(function() { check_contextmenu(true, false, false, false, false); gContextMenu.hidePopup(); run_next_test(); }, false); info("Opening context menu on disabled theme item"); + el.parentNode.ensureElementIsVisible(el); EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow); EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow); }); add_test(function() { LightweightThemeManager.currentTheme = gLWTheme; @@ -255,16 +261,17 @@ add_test(function() { check_contextmenu(true, true, false, false, false); gContextMenu.hidePopup(); run_next_test(); }, false); info("Opening context menu on enabled LW theme item"); + el.parentNode.ensureElementIsVisible(el); EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow); EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow); }); add_test(function() { LightweightThemeManager.currentTheme = null; @@ -275,16 +282,17 @@ add_test(function() { check_contextmenu(true, false, false, false, false); gContextMenu.hidePopup(); run_next_test(); }, false); info("Opening context menu on disabled LW theme item"); + el.parentNode.ensureElementIsVisible(el); EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow); EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow); }); add_test(function() { LightweightThemeManager.currentTheme = gLWTheme; @@ -462,16 +470,17 @@ add_test(function() { check_contextmenu(false, false, true, false, false); gContextMenu.hidePopup(); run_next_test(); }, false); info("Opening context menu on remote extension item"); + el.parentNode.ensureElementIsVisible(el); EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow); EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow); }); }); });
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_bug638292.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug638292.js @@ -8,16 +8,22 @@ function test() { gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); waitForFocus(page_loaded, gBrowser.contentWindow); }, true); gBrowser.loadURI(TESTROOT + "bug638292.html"); } function check_load(aCallback) { gBrowser.addEventListener("load", function(aEvent) { + // SeaMonkey needs to deal with intermediate "about:blank" document(s). + if (!aEvent.target.location) { + info("Ignoring about:blank load. (Expected (a few times) on SeaMonkey only.)"); + return; + } + gBrowser.removeEventListener("load", arguments.callee, true); // Let the load handler complete executeSoon(function() { var doc = gBrowser.browsers[2].contentDocument; is(doc.getElementById("enabled").textContent, "true", "installTrigger should have been enabled"); // Focus the old tab
--- a/toolkit/themes/pinstripe/global/media/videocontrols.css +++ b/toolkit/themes/pinstripe/global/media/videocontrols.css @@ -72,17 +72,17 @@ min-width: 16px; min-height: 11px; } .volumeBackgroundBar { /* margin left/right: make bar 8px wide (control width = 28, minus 2 * 10 margin) */ margin: 0 10px; background-color: rgba(255,255,255,.5); - border-radius: 4px 4px; + border-radius: 2.5px; } .durationBox { -moz-box-pack: center; } .durationLabel { margin-left: -22px; /* 1/2 of scrubber thumb width, for overhang. */ @@ -97,17 +97,17 @@ display: none; } .backgroundBar { /* margin top/bottom: make bar 8px tall (control height = 28, minus 2 * 10 margin) */ /* margin left/right: 1/2 of scrubber thumb width, for overhang. */ margin: 10px 22px; background-color: rgba(255,255,255,.5); - border-radius: 4px; + border-radius: 2.5px; } .bufferBar, .progressBar { /* margin top/bottom: make bar 8px tall (control height = 28, minus 2 * 10 margin) */ /* margin left/right: 1/2 of scrubber thumb width, for overhang. */ margin: 10px 22px; -moz-appearance: none; @@ -117,22 +117,22 @@ /* .progress-bar is an element inside the <progressmeter> implementation. */ .bufferBar .progress-bar { /* * Note that this is drawn on top of the .backgroundBar. So although this * has the same background-color specified, the semitransparent * compositing gives it a different visual appearance. */ background-color: rgba(255,255,255,.5); - border-radius: 4px; + border-radius: 2.5px; } .progressBar .progress-bar { background-color: white; - border-radius: 4px 0 0 4px; + border-radius: 2.5px; } /* .scale-slider is an element inside the <scale> implementation. */ .scrubber .scale-slider, .volumeControl .scale-slider { /* Hide the default horizontal bar. */ -moz-appearance: none; background: none;
--- a/toolkit/themes/winstripe/global/media/videocontrols.css +++ b/toolkit/themes/winstripe/global/media/videocontrols.css @@ -74,17 +74,17 @@ min-width: 16px; min-height: 11px; } .volumeBackgroundBar { /* margin left/right: make bar 8px wide (control width = 28, minus 2 * 10 margin) */ margin: 0 10px; background-color: rgba(255,255,255,.5); - border-radius: 4px 4px; + border-radius: 2.5px; } .durationBox { -moz-box-pack: center; } .durationLabel { margin-left: -22px; /* 1/2 of scrubber thumb width, for overhang. */ @@ -99,17 +99,17 @@ display: none; } .backgroundBar { /* margin top/bottom: make bar 8px tall (control height = 28, minus 2 * 10 margin) */ /* margin left/right: 1/2 of scrubber thumb width, for overhang. */ margin: 10px 22px; background-color: rgba(255,255,255,.5); - border-radius: 4px; + border-radius: 2.5px; } .bufferBar, .progressBar { /* margin top/bottom: make bar 8px tall (control height = 28, minus 2 * 10 margin) */ /* margin left/right: 1/2 of scrubber thumb width, for overhang. */ margin: 10px 22px; -moz-appearance: none; @@ -122,23 +122,23 @@ /* .progress-bar is an element inside the <progressmeter> implementation. */ .bufferBar .progress-bar { /* * Note that this is drawn on top of the .backgroundBar. So although this * has the same background-color specified, the semitransparent * compositing gives it a different visual appearance. */ background-color: rgba(255,255,255,.5); - border-radius: 4px; + border-radius: 2.5px; -moz-appearance: none; } .progressBar .progress-bar { background-color: white; - border-radius: 4px 0 0 4px; + border-radius: 2.5px; -moz-appearance: none; } /* .scale-slider is an element inside the <scale> implementation. */ .scrubber .scale-slider, .volumeControl .scale-slider { /* Hide the default horizontal bar. */ -moz-appearance: none;
--- a/widget/android/nsWindow.cpp +++ b/widget/android/nsWindow.cpp @@ -967,18 +967,18 @@ nsWindow::OnGlobalAndroidEvent(AndroidGe case AndroidGeckoEvent::MOTION_EVENT: { win->UserActivity(); if (!gTopLevelWindows.IsEmpty()) { nsIntPoint pt(0,0); nsTArray<nsIntPoint> points = ae->Points(); if (points.Length() > 0) { pt = points[0]; } - pt.x = clamped(pt.x, 0, gAndroidBounds.width - 1); - pt.y = clamped(pt.y, 0, gAndroidBounds.height - 1); + pt.x = clamped(pt.x, 0, PR_MAX(gAndroidBounds.width - 1, 0)); + pt.y = clamped(pt.y, 0, PR_MAX(gAndroidBounds.height - 1, 0)); nsWindow *target = win->FindWindowForPoint(pt); #if 0 ALOG("MOTION_EVENT %f,%f -> %p (visible: %d children: %d)", pt.x, pt.y, (void*)target, target ? target->mIsVisible : 0, target ? target->mChildren.Length() : 0); DumpWindows(); #endif