Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | // This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. |
michael@0 | 4 | |
michael@0 | 5 | "use strict"; |
michael@0 | 6 | |
michael@0 | 7 | this.EXPORTED_SYMBOLS = ["BrowserUITelemetry"]; |
michael@0 | 8 | |
michael@0 | 9 | const {interfaces: Ci, utils: Cu} = Components; |
michael@0 | 10 | |
michael@0 | 11 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 12 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 13 | |
michael@0 | 14 | XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", |
michael@0 | 15 | "resource://gre/modules/UITelemetry.jsm"); |
michael@0 | 16 | XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", |
michael@0 | 17 | "resource:///modules/RecentWindow.jsm"); |
michael@0 | 18 | XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", |
michael@0 | 19 | "resource:///modules/CustomizableUI.jsm"); |
michael@0 | 20 | XPCOMUtils.defineLazyModuleGetter(this, "UITour", |
michael@0 | 21 | "resource:///modules/UITour.jsm"); |
michael@0 | 22 | XPCOMUtils.defineLazyGetter(this, "Timer", function() { |
michael@0 | 23 | let timer = {}; |
michael@0 | 24 | Cu.import("resource://gre/modules/Timer.jsm", timer); |
michael@0 | 25 | return timer; |
michael@0 | 26 | }); |
michael@0 | 27 | |
michael@0 | 28 | const MS_SECOND = 1000; |
michael@0 | 29 | const MS_MINUTE = MS_SECOND * 60; |
michael@0 | 30 | const MS_HOUR = MS_MINUTE * 60; |
michael@0 | 31 | |
michael@0 | 32 | XPCOMUtils.defineLazyGetter(this, "DEFAULT_AREA_PLACEMENTS", function() { |
michael@0 | 33 | let result = { |
michael@0 | 34 | "PanelUI-contents": [ |
michael@0 | 35 | "edit-controls", |
michael@0 | 36 | "zoom-controls", |
michael@0 | 37 | "new-window-button", |
michael@0 | 38 | "privatebrowsing-button", |
michael@0 | 39 | "save-page-button", |
michael@0 | 40 | "print-button", |
michael@0 | 41 | "history-panelmenu", |
michael@0 | 42 | "fullscreen-button", |
michael@0 | 43 | "find-button", |
michael@0 | 44 | "preferences-button", |
michael@0 | 45 | "add-ons-button", |
michael@0 | 46 | "developer-button", |
michael@0 | 47 | ], |
michael@0 | 48 | "nav-bar": [ |
michael@0 | 49 | "urlbar-container", |
michael@0 | 50 | "search-container", |
michael@0 | 51 | "webrtc-status-button", |
michael@0 | 52 | "bookmarks-menu-button", |
michael@0 | 53 | "downloads-button", |
michael@0 | 54 | "home-button", |
michael@0 | 55 | "social-share-button", |
michael@0 | 56 | ], |
michael@0 | 57 | // It's true that toolbar-menubar is not visible |
michael@0 | 58 | // on OS X, but the XUL node is definitely present |
michael@0 | 59 | // in the document. |
michael@0 | 60 | "toolbar-menubar": [ |
michael@0 | 61 | "menubar-items", |
michael@0 | 62 | ], |
michael@0 | 63 | "TabsToolbar": [ |
michael@0 | 64 | "tabbrowser-tabs", |
michael@0 | 65 | "new-tab-button", |
michael@0 | 66 | "alltabs-button", |
michael@0 | 67 | ], |
michael@0 | 68 | "PersonalToolbar": [ |
michael@0 | 69 | "personal-bookmarks", |
michael@0 | 70 | ], |
michael@0 | 71 | }; |
michael@0 | 72 | |
michael@0 | 73 | let showCharacterEncoding = Services.prefs.getComplexValue( |
michael@0 | 74 | "browser.menu.showCharacterEncoding", |
michael@0 | 75 | Ci.nsIPrefLocalizedString |
michael@0 | 76 | ).data; |
michael@0 | 77 | if (showCharacterEncoding == "true") { |
michael@0 | 78 | result["PanelUI-contents"].push("characterencoding-button"); |
michael@0 | 79 | } |
michael@0 | 80 | |
michael@0 | 81 | if (Services.metro && Services.metro.supported) { |
michael@0 | 82 | result["PanelUI-contents"].push("switch-to-metro-button"); |
michael@0 | 83 | } |
michael@0 | 84 | |
michael@0 | 85 | return result; |
michael@0 | 86 | }); |
michael@0 | 87 | |
michael@0 | 88 | XPCOMUtils.defineLazyGetter(this, "DEFAULT_AREAS", function() { |
michael@0 | 89 | return Object.keys(DEFAULT_AREA_PLACEMENTS); |
michael@0 | 90 | }); |
michael@0 | 91 | |
michael@0 | 92 | XPCOMUtils.defineLazyGetter(this, "PALETTE_ITEMS", function() { |
michael@0 | 93 | let result = [ |
michael@0 | 94 | "open-file-button", |
michael@0 | 95 | "developer-button", |
michael@0 | 96 | "feed-button", |
michael@0 | 97 | "email-link-button", |
michael@0 | 98 | "sync-button", |
michael@0 | 99 | "tabview-button", |
michael@0 | 100 | ]; |
michael@0 | 101 | |
michael@0 | 102 | let panelPlacements = DEFAULT_AREA_PLACEMENTS["PanelUI-contents"]; |
michael@0 | 103 | if (panelPlacements.indexOf("characterencoding-button") == -1) { |
michael@0 | 104 | result.push("characterencoding-button"); |
michael@0 | 105 | } |
michael@0 | 106 | |
michael@0 | 107 | return result; |
michael@0 | 108 | }); |
michael@0 | 109 | |
michael@0 | 110 | XPCOMUtils.defineLazyGetter(this, "DEFAULT_ITEMS", function() { |
michael@0 | 111 | let result = []; |
michael@0 | 112 | for (let [, buttons] of Iterator(DEFAULT_AREA_PLACEMENTS)) { |
michael@0 | 113 | result = result.concat(buttons); |
michael@0 | 114 | } |
michael@0 | 115 | return result; |
michael@0 | 116 | }); |
michael@0 | 117 | |
michael@0 | 118 | XPCOMUtils.defineLazyGetter(this, "ALL_BUILTIN_ITEMS", function() { |
michael@0 | 119 | // These special cases are for click events on built-in items that are |
michael@0 | 120 | // contained within customizable items (like the navigation widget). |
michael@0 | 121 | const SPECIAL_CASES = [ |
michael@0 | 122 | "back-button", |
michael@0 | 123 | "forward-button", |
michael@0 | 124 | "urlbar-stop-button", |
michael@0 | 125 | "urlbar-go-button", |
michael@0 | 126 | "urlbar-reload-button", |
michael@0 | 127 | "searchbar", |
michael@0 | 128 | "cut-button", |
michael@0 | 129 | "copy-button", |
michael@0 | 130 | "paste-button", |
michael@0 | 131 | "zoom-out-button", |
michael@0 | 132 | "zoom-reset-button", |
michael@0 | 133 | "zoom-in-button", |
michael@0 | 134 | "BMB_bookmarksPopup", |
michael@0 | 135 | "BMB_unsortedBookmarksPopup", |
michael@0 | 136 | "BMB_bookmarksToolbarPopup", |
michael@0 | 137 | ] |
michael@0 | 138 | return DEFAULT_ITEMS.concat(PALETTE_ITEMS) |
michael@0 | 139 | .concat(SPECIAL_CASES); |
michael@0 | 140 | }); |
michael@0 | 141 | |
michael@0 | 142 | const OTHER_MOUSEUP_MONITORED_ITEMS = [ |
michael@0 | 143 | "PlacesChevron", |
michael@0 | 144 | "PlacesToolbarItems", |
michael@0 | 145 | "menubar-items", |
michael@0 | 146 | ]; |
michael@0 | 147 | |
michael@0 | 148 | // Items that open arrow panels will often be overlapped by |
michael@0 | 149 | // the panel that they're opening by the time the mouseup |
michael@0 | 150 | // event is fired, so for these items, we monitor mousedown. |
michael@0 | 151 | const MOUSEDOWN_MONITORED_ITEMS = [ |
michael@0 | 152 | "PanelUI-menu-button", |
michael@0 | 153 | ]; |
michael@0 | 154 | |
michael@0 | 155 | // Weakly maps browser windows to objects whose keys are relative |
michael@0 | 156 | // timestamps for when some kind of session started. For example, |
michael@0 | 157 | // when a customization session started. That way, when the window |
michael@0 | 158 | // exits customization mode, we can determine how long the session |
michael@0 | 159 | // lasted. |
michael@0 | 160 | const WINDOW_DURATION_MAP = new WeakMap(); |
michael@0 | 161 | |
michael@0 | 162 | // Default bucket name, when no other bucket is active. |
michael@0 | 163 | const BUCKET_DEFAULT = "__DEFAULT__"; |
michael@0 | 164 | // Bucket prefix, for named buckets. |
michael@0 | 165 | const BUCKET_PREFIX = "bucket_"; |
michael@0 | 166 | // Standard separator to use between different parts of a bucket name, such |
michael@0 | 167 | // as primary name and the time step string. |
michael@0 | 168 | const BUCKET_SEPARATOR = "|"; |
michael@0 | 169 | |
michael@0 | 170 | this.BrowserUITelemetry = { |
michael@0 | 171 | init: function() { |
michael@0 | 172 | UITelemetry.addSimpleMeasureFunction("toolbars", |
michael@0 | 173 | this.getToolbarMeasures.bind(this)); |
michael@0 | 174 | // Ensure that UITour.jsm remains lazy-loaded, yet always registers its |
michael@0 | 175 | // simple measure function with UITelemetry. |
michael@0 | 176 | UITelemetry.addSimpleMeasureFunction("UITour", |
michael@0 | 177 | () => UITour.getTelemetry()); |
michael@0 | 178 | |
michael@0 | 179 | Services.obs.addObserver(this, "sessionstore-windows-restored", false); |
michael@0 | 180 | Services.obs.addObserver(this, "browser-delayed-startup-finished", false); |
michael@0 | 181 | CustomizableUI.addListener(this); |
michael@0 | 182 | }, |
michael@0 | 183 | |
michael@0 | 184 | observe: function(aSubject, aTopic, aData) { |
michael@0 | 185 | switch(aTopic) { |
michael@0 | 186 | case "sessionstore-windows-restored": |
michael@0 | 187 | this._gatherFirstWindowMeasurements(); |
michael@0 | 188 | break; |
michael@0 | 189 | case "browser-delayed-startup-finished": |
michael@0 | 190 | this._registerWindow(aSubject); |
michael@0 | 191 | break; |
michael@0 | 192 | } |
michael@0 | 193 | }, |
michael@0 | 194 | |
michael@0 | 195 | /** |
michael@0 | 196 | * For the _countableEvents object, constructs a chain of |
michael@0 | 197 | * Javascript Objects with the keys in aKeys, with the final |
michael@0 | 198 | * key getting the value in aEndWith. If the final key already |
michael@0 | 199 | * exists in the final object, its value is not set. In either |
michael@0 | 200 | * case, a reference to the second last object in the chain is |
michael@0 | 201 | * returned. |
michael@0 | 202 | * |
michael@0 | 203 | * Example - suppose I want to store: |
michael@0 | 204 | * _countableEvents: { |
michael@0 | 205 | * a: { |
michael@0 | 206 | * b: { |
michael@0 | 207 | * c: 0 |
michael@0 | 208 | * } |
michael@0 | 209 | * } |
michael@0 | 210 | * } |
michael@0 | 211 | * |
michael@0 | 212 | * And then increment the "c" value by 1, you could call this |
michael@0 | 213 | * function like this: |
michael@0 | 214 | * |
michael@0 | 215 | * let example = this._ensureObjectChain([a, b, c], 0); |
michael@0 | 216 | * example["c"]++; |
michael@0 | 217 | * |
michael@0 | 218 | * Subsequent repetitions of these last two lines would |
michael@0 | 219 | * simply result in the c value being incremented again |
michael@0 | 220 | * and again. |
michael@0 | 221 | * |
michael@0 | 222 | * @param aKeys the Array of keys to chain Objects together with. |
michael@0 | 223 | * @param aEndWith the value to assign to the last key. |
michael@0 | 224 | * @returns a reference to the second last object in the chain - |
michael@0 | 225 | * so in our example, that'd be "b". |
michael@0 | 226 | */ |
michael@0 | 227 | _ensureObjectChain: function(aKeys, aEndWith) { |
michael@0 | 228 | let current = this._countableEvents; |
michael@0 | 229 | let parent = null; |
michael@0 | 230 | aKeys.unshift(this._bucket); |
michael@0 | 231 | for (let [i, key] of Iterator(aKeys)) { |
michael@0 | 232 | if (!(key in current)) { |
michael@0 | 233 | if (i == aKeys.length - 1) { |
michael@0 | 234 | current[key] = aEndWith; |
michael@0 | 235 | } else { |
michael@0 | 236 | current[key] = {}; |
michael@0 | 237 | } |
michael@0 | 238 | } |
michael@0 | 239 | parent = current; |
michael@0 | 240 | current = current[key]; |
michael@0 | 241 | } |
michael@0 | 242 | return parent; |
michael@0 | 243 | }, |
michael@0 | 244 | |
michael@0 | 245 | _countableEvents: {}, |
michael@0 | 246 | _countEvent: function(aKeyArray) { |
michael@0 | 247 | let countObject = this._ensureObjectChain(aKeyArray, 0); |
michael@0 | 248 | let lastItemKey = aKeyArray[aKeyArray.length - 1]; |
michael@0 | 249 | countObject[lastItemKey]++; |
michael@0 | 250 | }, |
michael@0 | 251 | |
michael@0 | 252 | _countMouseUpEvent: function(aCategory, aAction, aButton) { |
michael@0 | 253 | const BUTTONS = ["left", "middle", "right"]; |
michael@0 | 254 | let buttonKey = BUTTONS[aButton]; |
michael@0 | 255 | if (buttonKey) { |
michael@0 | 256 | this._countEvent([aCategory, aAction, buttonKey]); |
michael@0 | 257 | } |
michael@0 | 258 | }, |
michael@0 | 259 | |
michael@0 | 260 | _firstWindowMeasurements: null, |
michael@0 | 261 | _gatherFirstWindowMeasurements: function() { |
michael@0 | 262 | // We'll gather measurements as soon as the session has restored. |
michael@0 | 263 | // We do this here instead of waiting for UITelemetry to ask for |
michael@0 | 264 | // our measurements because at that point all browser windows have |
michael@0 | 265 | // probably been closed, since the vast majority of saved-session |
michael@0 | 266 | // pings are gathered during shutdown. |
michael@0 | 267 | let win = RecentWindow.getMostRecentBrowserWindow({ |
michael@0 | 268 | private: false, |
michael@0 | 269 | allowPopups: false, |
michael@0 | 270 | }); |
michael@0 | 271 | |
michael@0 | 272 | // If there are no such windows, we're out of luck. :( |
michael@0 | 273 | this._firstWindowMeasurements = win ? this._getWindowMeasurements(win) |
michael@0 | 274 | : {}; |
michael@0 | 275 | }, |
michael@0 | 276 | |
michael@0 | 277 | _registerWindow: function(aWindow) { |
michael@0 | 278 | aWindow.addEventListener("unload", this); |
michael@0 | 279 | let document = aWindow.document; |
michael@0 | 280 | |
michael@0 | 281 | for (let areaID of CustomizableUI.areas) { |
michael@0 | 282 | let areaNode = document.getElementById(areaID); |
michael@0 | 283 | if (areaNode) { |
michael@0 | 284 | (areaNode.customizationTarget || areaNode).addEventListener("mouseup", this); |
michael@0 | 285 | } |
michael@0 | 286 | } |
michael@0 | 287 | |
michael@0 | 288 | for (let itemID of OTHER_MOUSEUP_MONITORED_ITEMS) { |
michael@0 | 289 | let item = document.getElementById(itemID); |
michael@0 | 290 | if (item) { |
michael@0 | 291 | item.addEventListener("mouseup", this); |
michael@0 | 292 | } |
michael@0 | 293 | } |
michael@0 | 294 | |
michael@0 | 295 | for (let itemID of MOUSEDOWN_MONITORED_ITEMS) { |
michael@0 | 296 | let item = document.getElementById(itemID); |
michael@0 | 297 | if (item) { |
michael@0 | 298 | item.addEventListener("mousedown", this); |
michael@0 | 299 | } |
michael@0 | 300 | } |
michael@0 | 301 | |
michael@0 | 302 | WINDOW_DURATION_MAP.set(aWindow, {}); |
michael@0 | 303 | }, |
michael@0 | 304 | |
michael@0 | 305 | _unregisterWindow: function(aWindow) { |
michael@0 | 306 | aWindow.removeEventListener("unload", this); |
michael@0 | 307 | let document = aWindow.document; |
michael@0 | 308 | |
michael@0 | 309 | for (let areaID of CustomizableUI.areas) { |
michael@0 | 310 | let areaNode = document.getElementById(areaID); |
michael@0 | 311 | if (areaNode) { |
michael@0 | 312 | (areaNode.customizationTarget || areaNode).removeEventListener("mouseup", this); |
michael@0 | 313 | } |
michael@0 | 314 | } |
michael@0 | 315 | |
michael@0 | 316 | for (let itemID of OTHER_MOUSEUP_MONITORED_ITEMS) { |
michael@0 | 317 | let item = document.getElementById(itemID); |
michael@0 | 318 | if (item) { |
michael@0 | 319 | item.removeEventListener("mouseup", this); |
michael@0 | 320 | } |
michael@0 | 321 | } |
michael@0 | 322 | |
michael@0 | 323 | for (let itemID of MOUSEDOWN_MONITORED_ITEMS) { |
michael@0 | 324 | let item = document.getElementById(itemID); |
michael@0 | 325 | if (item) { |
michael@0 | 326 | item.removeEventListener("mousedown", this); |
michael@0 | 327 | } |
michael@0 | 328 | } |
michael@0 | 329 | }, |
michael@0 | 330 | |
michael@0 | 331 | handleEvent: function(aEvent) { |
michael@0 | 332 | switch(aEvent.type) { |
michael@0 | 333 | case "unload": |
michael@0 | 334 | this._unregisterWindow(aEvent.currentTarget); |
michael@0 | 335 | break; |
michael@0 | 336 | case "mouseup": |
michael@0 | 337 | this._handleMouseUp(aEvent); |
michael@0 | 338 | break; |
michael@0 | 339 | case "mousedown": |
michael@0 | 340 | this._handleMouseDown(aEvent); |
michael@0 | 341 | break; |
michael@0 | 342 | } |
michael@0 | 343 | }, |
michael@0 | 344 | |
michael@0 | 345 | _handleMouseUp: function(aEvent) { |
michael@0 | 346 | let targetID = aEvent.currentTarget.id; |
michael@0 | 347 | |
michael@0 | 348 | switch (targetID) { |
michael@0 | 349 | case "PlacesToolbarItems": |
michael@0 | 350 | this._PlacesToolbarItemsMouseUp(aEvent); |
michael@0 | 351 | break; |
michael@0 | 352 | case "PlacesChevron": |
michael@0 | 353 | this._PlacesChevronMouseUp(aEvent); |
michael@0 | 354 | break; |
michael@0 | 355 | case "menubar-items": |
michael@0 | 356 | this._menubarMouseUp(aEvent); |
michael@0 | 357 | break; |
michael@0 | 358 | default: |
michael@0 | 359 | this._checkForBuiltinItem(aEvent); |
michael@0 | 360 | } |
michael@0 | 361 | }, |
michael@0 | 362 | |
michael@0 | 363 | _handleMouseDown: function(aEvent) { |
michael@0 | 364 | if (aEvent.currentTarget.id == "PanelUI-menu-button") { |
michael@0 | 365 | // _countMouseUpEvent expects a detail for the second argument, |
michael@0 | 366 | // but we don't really have any details to give. Just passing in |
michael@0 | 367 | // "button" is probably simpler than trying to modify |
michael@0 | 368 | // _countMouseUpEvent for this particular case. |
michael@0 | 369 | this._countMouseUpEvent("click-menu-button", "button", aEvent.button); |
michael@0 | 370 | } |
michael@0 | 371 | }, |
michael@0 | 372 | |
michael@0 | 373 | _PlacesChevronMouseUp: function(aEvent) { |
michael@0 | 374 | let target = aEvent.originalTarget; |
michael@0 | 375 | let result = target.id == "PlacesChevron" ? "chevron" : "overflowed-item"; |
michael@0 | 376 | this._countMouseUpEvent("click-bookmarks-bar", result, aEvent.button); |
michael@0 | 377 | }, |
michael@0 | 378 | |
michael@0 | 379 | _PlacesToolbarItemsMouseUp: function(aEvent) { |
michael@0 | 380 | let target = aEvent.originalTarget; |
michael@0 | 381 | // If this isn't a bookmark-item, we don't care about it. |
michael@0 | 382 | if (!target.classList.contains("bookmark-item")) { |
michael@0 | 383 | return; |
michael@0 | 384 | } |
michael@0 | 385 | |
michael@0 | 386 | let result = target.hasAttribute("container") ? "container" : "item"; |
michael@0 | 387 | this._countMouseUpEvent("click-bookmarks-bar", result, aEvent.button); |
michael@0 | 388 | }, |
michael@0 | 389 | |
michael@0 | 390 | _menubarMouseUp: function(aEvent) { |
michael@0 | 391 | let target = aEvent.originalTarget; |
michael@0 | 392 | let tag = target.localName |
michael@0 | 393 | let result = (tag == "menu" || tag == "menuitem") ? tag : "other"; |
michael@0 | 394 | this._countMouseUpEvent("click-menubar", result, aEvent.button); |
michael@0 | 395 | }, |
michael@0 | 396 | |
michael@0 | 397 | _bookmarksMenuButtonMouseUp: function(aEvent) { |
michael@0 | 398 | let bookmarksWidget = CustomizableUI.getWidget("bookmarks-menu-button"); |
michael@0 | 399 | if (bookmarksWidget.areaType == CustomizableUI.TYPE_MENU_PANEL) { |
michael@0 | 400 | // In the menu panel, only the star is visible, and that opens up the |
michael@0 | 401 | // bookmarks subview. |
michael@0 | 402 | this._countMouseUpEvent("click-bookmarks-menu-button", "in-panel", |
michael@0 | 403 | aEvent.button); |
michael@0 | 404 | } else { |
michael@0 | 405 | let clickedItem = aEvent.originalTarget; |
michael@0 | 406 | // Did we click on the star, or the dropmarker? The star |
michael@0 | 407 | // has an anonid of "button". If we don't find that, we'll |
michael@0 | 408 | // assume we clicked on the dropmarker. |
michael@0 | 409 | let action = "menu"; |
michael@0 | 410 | if (clickedItem.getAttribute("anonid") == "button") { |
michael@0 | 411 | // We clicked on the star - now we just need to record |
michael@0 | 412 | // whether or not we're adding a bookmark or editing an |
michael@0 | 413 | // existing one. |
michael@0 | 414 | let bookmarksMenuNode = |
michael@0 | 415 | bookmarksWidget.forWindow(aEvent.target.ownerGlobal).node; |
michael@0 | 416 | action = bookmarksMenuNode.hasAttribute("starred") ? "edit" : "add"; |
michael@0 | 417 | } |
michael@0 | 418 | this._countMouseUpEvent("click-bookmarks-menu-button", action, |
michael@0 | 419 | aEvent.button); |
michael@0 | 420 | } |
michael@0 | 421 | }, |
michael@0 | 422 | |
michael@0 | 423 | _checkForBuiltinItem: function(aEvent) { |
michael@0 | 424 | let item = aEvent.originalTarget; |
michael@0 | 425 | |
michael@0 | 426 | // We special-case the bookmarks-menu-button, since we want to |
michael@0 | 427 | // monitor more than just clicks on it. |
michael@0 | 428 | if (item.id == "bookmarks-menu-button" || |
michael@0 | 429 | getIDBasedOnFirstIDedAncestor(item) == "bookmarks-menu-button") { |
michael@0 | 430 | this._bookmarksMenuButtonMouseUp(aEvent); |
michael@0 | 431 | return; |
michael@0 | 432 | } |
michael@0 | 433 | |
michael@0 | 434 | // Perhaps we're seeing one of the default toolbar items |
michael@0 | 435 | // being clicked. |
michael@0 | 436 | if (ALL_BUILTIN_ITEMS.indexOf(item.id) != -1) { |
michael@0 | 437 | // Base case - we clicked directly on one of our built-in items, |
michael@0 | 438 | // and we can go ahead and register that click. |
michael@0 | 439 | this._countMouseUpEvent("click-builtin-item", item.id, aEvent.button); |
michael@0 | 440 | return; |
michael@0 | 441 | } |
michael@0 | 442 | |
michael@0 | 443 | // If not, we need to check if one of the ancestors of the clicked |
michael@0 | 444 | // item is in our list of built-in items to check. |
michael@0 | 445 | let candidate = getIDBasedOnFirstIDedAncestor(item); |
michael@0 | 446 | if (ALL_BUILTIN_ITEMS.indexOf(candidate) != -1) { |
michael@0 | 447 | this._countMouseUpEvent("click-builtin-item", candidate, aEvent.button); |
michael@0 | 448 | } |
michael@0 | 449 | }, |
michael@0 | 450 | |
michael@0 | 451 | _getWindowMeasurements: function(aWindow) { |
michael@0 | 452 | let document = aWindow.document; |
michael@0 | 453 | let result = {}; |
michael@0 | 454 | |
michael@0 | 455 | // Determine if the window is in the maximized, normal or |
michael@0 | 456 | // fullscreen state. |
michael@0 | 457 | result.sizemode = document.documentElement.getAttribute("sizemode"); |
michael@0 | 458 | |
michael@0 | 459 | // Determine if the Bookmarks bar is currently visible |
michael@0 | 460 | let bookmarksBar = document.getElementById("PersonalToolbar"); |
michael@0 | 461 | result.bookmarksBarEnabled = bookmarksBar && !bookmarksBar.collapsed; |
michael@0 | 462 | |
michael@0 | 463 | // Determine if the menubar is currently visible. On OS X, the menubar |
michael@0 | 464 | // is never shown, despite not having the collapsed attribute set. |
michael@0 | 465 | let menuBar = document.getElementById("toolbar-menubar"); |
michael@0 | 466 | result.menuBarEnabled = |
michael@0 | 467 | menuBar && Services.appinfo.OS != "Darwin" |
michael@0 | 468 | && menuBar.getAttribute("autohide") != "true"; |
michael@0 | 469 | |
michael@0 | 470 | // Determine if the titlebar is currently visible. |
michael@0 | 471 | result.titleBarEnabled = !Services.prefs.getBoolPref("browser.tabs.drawInTitlebar"); |
michael@0 | 472 | |
michael@0 | 473 | // Examine all customizable areas and see what default items |
michael@0 | 474 | // are present and missing. |
michael@0 | 475 | let defaultKept = []; |
michael@0 | 476 | let defaultMoved = []; |
michael@0 | 477 | let nondefaultAdded = []; |
michael@0 | 478 | |
michael@0 | 479 | for (let areaID of CustomizableUI.areas) { |
michael@0 | 480 | let items = CustomizableUI.getWidgetIdsInArea(areaID); |
michael@0 | 481 | for (let item of items) { |
michael@0 | 482 | // Is this a default item? |
michael@0 | 483 | if (DEFAULT_ITEMS.indexOf(item) != -1) { |
michael@0 | 484 | // Ok, it's a default item - but is it in its default |
michael@0 | 485 | // toolbar? We use Array.isArray instead of checking for |
michael@0 | 486 | // toolbarID in DEFAULT_AREA_PLACEMENTS because an add-on might |
michael@0 | 487 | // be clever and give itself the id of "toString" or something. |
michael@0 | 488 | if (Array.isArray(DEFAULT_AREA_PLACEMENTS[areaID]) && |
michael@0 | 489 | DEFAULT_AREA_PLACEMENTS[areaID].indexOf(item) != -1) { |
michael@0 | 490 | // The item is in its default toolbar |
michael@0 | 491 | defaultKept.push(item); |
michael@0 | 492 | } else { |
michael@0 | 493 | defaultMoved.push(item); |
michael@0 | 494 | } |
michael@0 | 495 | } else if (PALETTE_ITEMS.indexOf(item) != -1) { |
michael@0 | 496 | // It's a palette item that's been moved into a toolbar |
michael@0 | 497 | nondefaultAdded.push(item); |
michael@0 | 498 | } |
michael@0 | 499 | // else, it's provided by an add-on, and we won't record it. |
michael@0 | 500 | } |
michael@0 | 501 | } |
michael@0 | 502 | |
michael@0 | 503 | // Now go through the items in the palette to see what default |
michael@0 | 504 | // items are in there. |
michael@0 | 505 | let paletteItems = |
michael@0 | 506 | CustomizableUI.getUnusedWidgets(aWindow.gNavToolbox.palette); |
michael@0 | 507 | let defaultRemoved = [item.id for (item of paletteItems) |
michael@0 | 508 | if (DEFAULT_ITEMS.indexOf(item.id) != -1)]; |
michael@0 | 509 | |
michael@0 | 510 | result.defaultKept = defaultKept; |
michael@0 | 511 | result.defaultMoved = defaultMoved; |
michael@0 | 512 | result.nondefaultAdded = nondefaultAdded; |
michael@0 | 513 | result.defaultRemoved = defaultRemoved; |
michael@0 | 514 | |
michael@0 | 515 | // Next, determine how many add-on provided toolbars exist. |
michael@0 | 516 | let addonToolbars = 0; |
michael@0 | 517 | let toolbars = document.querySelectorAll("toolbar[customizable=true]"); |
michael@0 | 518 | for (let toolbar of toolbars) { |
michael@0 | 519 | if (DEFAULT_AREAS.indexOf(toolbar.id) == -1) { |
michael@0 | 520 | addonToolbars++; |
michael@0 | 521 | } |
michael@0 | 522 | } |
michael@0 | 523 | result.addonToolbars = addonToolbars; |
michael@0 | 524 | |
michael@0 | 525 | // Find out how many open tabs we have in each window |
michael@0 | 526 | let winEnumerator = Services.wm.getEnumerator("navigator:browser"); |
michael@0 | 527 | let visibleTabs = []; |
michael@0 | 528 | let hiddenTabs = []; |
michael@0 | 529 | while (winEnumerator.hasMoreElements()) { |
michael@0 | 530 | let someWin = winEnumerator.getNext(); |
michael@0 | 531 | if (someWin.gBrowser) { |
michael@0 | 532 | let visibleTabsNum = someWin.gBrowser.visibleTabs.length; |
michael@0 | 533 | visibleTabs.push(visibleTabsNum); |
michael@0 | 534 | hiddenTabs.push(someWin.gBrowser.tabs.length - visibleTabsNum); |
michael@0 | 535 | } |
michael@0 | 536 | } |
michael@0 | 537 | result.visibleTabs = visibleTabs; |
michael@0 | 538 | result.hiddenTabs = hiddenTabs; |
michael@0 | 539 | |
michael@0 | 540 | return result; |
michael@0 | 541 | }, |
michael@0 | 542 | |
michael@0 | 543 | getToolbarMeasures: function() { |
michael@0 | 544 | let result = this._firstWindowMeasurements || {}; |
michael@0 | 545 | result.countableEvents = this._countableEvents; |
michael@0 | 546 | result.durations = this._durations; |
michael@0 | 547 | return result; |
michael@0 | 548 | }, |
michael@0 | 549 | |
michael@0 | 550 | countCustomizationEvent: function(aEventType) { |
michael@0 | 551 | this._countEvent(["customize", aEventType]); |
michael@0 | 552 | }, |
michael@0 | 553 | |
michael@0 | 554 | _durations: { |
michael@0 | 555 | customization: [], |
michael@0 | 556 | }, |
michael@0 | 557 | |
michael@0 | 558 | onCustomizeStart: function(aWindow) { |
michael@0 | 559 | this._countEvent(["customize", "start"]); |
michael@0 | 560 | let durationMap = WINDOW_DURATION_MAP.get(aWindow); |
michael@0 | 561 | if (!durationMap) { |
michael@0 | 562 | durationMap = {}; |
michael@0 | 563 | WINDOW_DURATION_MAP.set(aWindow, durationMap); |
michael@0 | 564 | } |
michael@0 | 565 | |
michael@0 | 566 | durationMap.customization = { |
michael@0 | 567 | start: aWindow.performance.now(), |
michael@0 | 568 | bucket: this._bucket, |
michael@0 | 569 | }; |
michael@0 | 570 | }, |
michael@0 | 571 | |
michael@0 | 572 | onCustomizeEnd: function(aWindow) { |
michael@0 | 573 | let durationMap = WINDOW_DURATION_MAP.get(aWindow); |
michael@0 | 574 | if (durationMap && "customization" in durationMap) { |
michael@0 | 575 | let duration = aWindow.performance.now() - durationMap.customization.start; |
michael@0 | 576 | this._durations.customization.push({ |
michael@0 | 577 | duration: duration, |
michael@0 | 578 | bucket: durationMap.customization.bucket, |
michael@0 | 579 | }); |
michael@0 | 580 | delete durationMap.customization; |
michael@0 | 581 | } |
michael@0 | 582 | }, |
michael@0 | 583 | |
michael@0 | 584 | _bucket: BUCKET_DEFAULT, |
michael@0 | 585 | _bucketTimer: null, |
michael@0 | 586 | |
michael@0 | 587 | /** |
michael@0 | 588 | * Default bucket name, when no other bucket is active. |
michael@0 | 589 | */ |
michael@0 | 590 | get BUCKET_DEFAULT() BUCKET_DEFAULT, |
michael@0 | 591 | |
michael@0 | 592 | /** |
michael@0 | 593 | * Bucket prefix, for named buckets. |
michael@0 | 594 | */ |
michael@0 | 595 | get BUCKET_PREFIX() BUCKET_PREFIX, |
michael@0 | 596 | |
michael@0 | 597 | /** |
michael@0 | 598 | * Standard separator to use between different parts of a bucket name, such |
michael@0 | 599 | * as primary name and the time step string. |
michael@0 | 600 | */ |
michael@0 | 601 | get BUCKET_SEPARATOR() BUCKET_SEPARATOR, |
michael@0 | 602 | |
michael@0 | 603 | get currentBucket() { |
michael@0 | 604 | return this._bucket; |
michael@0 | 605 | }, |
michael@0 | 606 | |
michael@0 | 607 | /** |
michael@0 | 608 | * Sets a named bucket for all countable events and select durections to be |
michael@0 | 609 | * put into. |
michael@0 | 610 | * |
michael@0 | 611 | * @param aName Name of bucket, or null for default bucket name (__DEFAULT__) |
michael@0 | 612 | */ |
michael@0 | 613 | setBucket: function(aName) { |
michael@0 | 614 | if (this._bucketTimer) { |
michael@0 | 615 | Timer.clearTimeout(this._bucketTimer); |
michael@0 | 616 | this._bucketTimer = null; |
michael@0 | 617 | } |
michael@0 | 618 | |
michael@0 | 619 | if (aName) |
michael@0 | 620 | this._bucket = BUCKET_PREFIX + aName; |
michael@0 | 621 | else |
michael@0 | 622 | this._bucket = BUCKET_DEFAULT; |
michael@0 | 623 | }, |
michael@0 | 624 | |
michael@0 | 625 | /** |
michael@0 | 626 | * Sets a bucket that expires at the rate of a given series of time steps. |
michael@0 | 627 | * Once the bucket expires, the current bucket will automatically revert to |
michael@0 | 628 | * the default bucket. While the bucket is expiring, it's name is postfixed |
michael@0 | 629 | * by '|' followed by a short string representation of the time step it's |
michael@0 | 630 | * currently in. |
michael@0 | 631 | * If any other bucket (expiring or normal) is set while an expiring bucket is |
michael@0 | 632 | * still expiring, the old expiring bucket stops expiring and the new bucket |
michael@0 | 633 | * immediately takes over. |
michael@0 | 634 | * |
michael@0 | 635 | * @param aName Name of bucket. |
michael@0 | 636 | * @param aTimeSteps An array of times in milliseconds to count up to before |
michael@0 | 637 | * reverting back to the default bucket. The array of times |
michael@0 | 638 | * is expected to be pre-sorted in ascending order. |
michael@0 | 639 | * For example, given a bucket name of 'bucket', the times: |
michael@0 | 640 | * [60000, 300000, 600000] |
michael@0 | 641 | * will result in the following buckets: |
michael@0 | 642 | * * bucket|1m - for the first 1 minute |
michael@0 | 643 | * * bucket|5m - for the following 4 minutes |
michael@0 | 644 | * (until 5 minutes after the start) |
michael@0 | 645 | * * bucket|10m - for the following 5 minutes |
michael@0 | 646 | * (until 10 minutes after the start) |
michael@0 | 647 | * * __DEFAULT__ - until a new bucket is set |
michael@0 | 648 | * @param aTimeOffset Time offset, in milliseconds, from which to start |
michael@0 | 649 | * counting. For example, if the first time step is 1000ms, |
michael@0 | 650 | * and the time offset is 300ms, then the next time step |
michael@0 | 651 | * will become active after 700ms. This affects all |
michael@0 | 652 | * following time steps also, meaning they will also all be |
michael@0 | 653 | * timed as though they started expiring 300ms before |
michael@0 | 654 | * setExpiringBucket was called. |
michael@0 | 655 | */ |
michael@0 | 656 | setExpiringBucket: function(aName, aTimeSteps, aTimeOffset = 0) { |
michael@0 | 657 | if (aTimeSteps.length === 0) { |
michael@0 | 658 | this.setBucket(null); |
michael@0 | 659 | return; |
michael@0 | 660 | } |
michael@0 | 661 | |
michael@0 | 662 | if (this._bucketTimer) { |
michael@0 | 663 | Timer.clearTimeout(this._bucketTimer); |
michael@0 | 664 | this._bucketTimer = null; |
michael@0 | 665 | } |
michael@0 | 666 | |
michael@0 | 667 | // Make a copy of the time steps array, so we can safely modify it without |
michael@0 | 668 | // modifying the original array that external code has passed to us. |
michael@0 | 669 | let steps = [...aTimeSteps]; |
michael@0 | 670 | let msec = steps.shift(); |
michael@0 | 671 | let postfix = this._toTimeStr(msec); |
michael@0 | 672 | this.setBucket(aName + BUCKET_SEPARATOR + postfix); |
michael@0 | 673 | |
michael@0 | 674 | this._bucketTimer = Timer.setTimeout(() => { |
michael@0 | 675 | this._bucketTimer = null; |
michael@0 | 676 | this.setExpiringBucket(aName, steps, aTimeOffset + msec); |
michael@0 | 677 | }, msec - aTimeOffset); |
michael@0 | 678 | }, |
michael@0 | 679 | |
michael@0 | 680 | /** |
michael@0 | 681 | * Formats a time interval, in milliseconds, to a minimal non-localized string |
michael@0 | 682 | * representation. Format is: 'h' for hours, 'm' for minutes, 's' for seconds, |
michael@0 | 683 | * 'ms' for milliseconds. |
michael@0 | 684 | * Examples: |
michael@0 | 685 | * 65 => 65ms |
michael@0 | 686 | * 1000 => 1s |
michael@0 | 687 | * 60000 => 1m |
michael@0 | 688 | * 61000 => 1m01s |
michael@0 | 689 | * |
michael@0 | 690 | * @param aTimeMS Time in milliseconds |
michael@0 | 691 | * |
michael@0 | 692 | * @return Minimal string representation. |
michael@0 | 693 | */ |
michael@0 | 694 | _toTimeStr: function(aTimeMS) { |
michael@0 | 695 | let timeStr = ""; |
michael@0 | 696 | |
michael@0 | 697 | function reduce(aUnitLength, aSymbol) { |
michael@0 | 698 | if (aTimeMS >= aUnitLength) { |
michael@0 | 699 | let units = Math.floor(aTimeMS / aUnitLength); |
michael@0 | 700 | aTimeMS = aTimeMS - (units * aUnitLength) |
michael@0 | 701 | timeStr += units + aSymbol; |
michael@0 | 702 | } |
michael@0 | 703 | } |
michael@0 | 704 | |
michael@0 | 705 | reduce(MS_HOUR, "h"); |
michael@0 | 706 | reduce(MS_MINUTE, "m"); |
michael@0 | 707 | reduce(MS_SECOND, "s"); |
michael@0 | 708 | reduce(1, "ms"); |
michael@0 | 709 | |
michael@0 | 710 | return timeStr; |
michael@0 | 711 | }, |
michael@0 | 712 | }; |
michael@0 | 713 | |
michael@0 | 714 | /** |
michael@0 | 715 | * Returns the id of the first ancestor of aNode that has an id. If aNode |
michael@0 | 716 | * has no parent, or no ancestor has an id, returns null. |
michael@0 | 717 | * |
michael@0 | 718 | * @param aNode the node to find the first ID'd ancestor of |
michael@0 | 719 | */ |
michael@0 | 720 | function getIDBasedOnFirstIDedAncestor(aNode) { |
michael@0 | 721 | while (!aNode.id) { |
michael@0 | 722 | aNode = aNode.parentNode; |
michael@0 | 723 | if (!aNode) { |
michael@0 | 724 | return null; |
michael@0 | 725 | } |
michael@0 | 726 | } |
michael@0 | 727 | |
michael@0 | 728 | return aNode.id; |
michael@0 | 729 | } |