1/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2/* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6const { XPCOMUtils } = ChromeUtils.import( 7 "resource://gre/modules/XPCOMUtils.jsm" 8); 9const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 10 11// Stop updating jumplists after some idle time. 12const IDLE_TIMEOUT_SECONDS = 5 * 60; 13 14// Prefs 15const PREF_TASKBAR_BRANCH = "browser.taskbar.lists."; 16const PREF_TASKBAR_ENABLED = "enabled"; 17const PREF_TASKBAR_ITEMCOUNT = "maxListItemCount"; 18const PREF_TASKBAR_FREQUENT = "frequent.enabled"; 19const PREF_TASKBAR_RECENT = "recent.enabled"; 20const PREF_TASKBAR_TASKS = "tasks.enabled"; 21const PREF_TASKBAR_REFRESH = "refreshInSeconds"; 22 23// Hash keys for pendingStatements. 24const LIST_TYPE = { 25 FREQUENT: 0, 26 RECENT: 1, 27}; 28 29/** 30 * Exports 31 */ 32 33var EXPORTED_SYMBOLS = ["WinTaskbarJumpList"]; 34 35/** 36 * Smart getters 37 */ 38 39XPCOMUtils.defineLazyGetter(this, "_prefs", function() { 40 return Services.prefs.getBranch(PREF_TASKBAR_BRANCH); 41}); 42 43XPCOMUtils.defineLazyGetter(this, "_stringBundle", function() { 44 return Services.strings.createBundle( 45 "chrome://browser/locale/taskbar.properties" 46 ); 47}); 48 49XPCOMUtils.defineLazyServiceGetter( 50 this, 51 "_idle", 52 "@mozilla.org/widget/useridleservice;1", 53 "nsIUserIdleService" 54); 55XPCOMUtils.defineLazyServiceGetter( 56 this, 57 "_taskbarService", 58 "@mozilla.org/windows-taskbar;1", 59 "nsIWinTaskbar" 60); 61 62ChromeUtils.defineModuleGetter( 63 this, 64 "PlacesUtils", 65 "resource://gre/modules/PlacesUtils.jsm" 66); 67ChromeUtils.defineModuleGetter( 68 this, 69 "PrivateBrowsingUtils", 70 "resource://gre/modules/PrivateBrowsingUtils.jsm" 71); 72 73/** 74 * Global functions 75 */ 76 77function _getString(name) { 78 return _stringBundle.GetStringFromName(name); 79} 80 81// Task list configuration data object. 82 83var tasksCfg = [ 84 /** 85 * Task configuration options: title, description, args, iconIndex, open, close. 86 * 87 * title - Task title displayed in the list. (strings in the table are temp fillers.) 88 * description - Tooltip description on the list item. 89 * args - Command line args to invoke the task. 90 * iconIndex - Optional win icon index into the main application for the 91 * list item. 92 * open - Boolean indicates if the command should be visible after the browser opens. 93 * close - Boolean indicates if the command should be visible after the browser closes. 94 */ 95 // Open new tab 96 { 97 get title() { 98 return _getString("taskbar.tasks.newTab.label"); 99 }, 100 get description() { 101 return _getString("taskbar.tasks.newTab.description"); 102 }, 103 args: "-new-tab about:blank", 104 iconIndex: 3, // New window icon 105 open: true, 106 close: true, // The jump list already has an app launch icon, but 107 // we don't always update the list on shutdown. 108 // Thus true for consistency. 109 }, 110 111 // Open new window 112 { 113 get title() { 114 return _getString("taskbar.tasks.newWindow.label"); 115 }, 116 get description() { 117 return _getString("taskbar.tasks.newWindow.description"); 118 }, 119 args: "-browser", 120 iconIndex: 2, // New tab icon 121 open: true, 122 close: true, // No point, but we don't always update the list on 123 // shutdown. Thus true for consistency. 124 }, 125]; 126 127// Open new private window 128let privateWindowTask = { 129 get title() { 130 return _getString("taskbar.tasks.newPrivateWindow.label"); 131 }, 132 get description() { 133 return _getString("taskbar.tasks.newPrivateWindow.description"); 134 }, 135 args: "-private-window", 136 iconIndex: 4, // Private browsing mode icon 137 open: true, 138 close: true, // No point, but we don't always update the list on 139 // shutdown. Thus true for consistency. 140}; 141 142// Implementation 143 144var WinTaskbarJumpList = { 145 _builder: null, 146 _tasks: null, 147 _shuttingDown: false, 148 149 /** 150 * Startup, shutdown, and update 151 */ 152 153 startup: function WTBJL_startup() { 154 // exit if this isn't win7 or higher. 155 if (!this._initTaskbar()) { 156 return; 157 } 158 159 // Store our task list config data 160 this._tasks = tasksCfg; 161 162 if (PrivateBrowsingUtils.enabled) { 163 tasksCfg.push(privateWindowTask); 164 } 165 166 // retrieve taskbar related prefs. 167 this._refreshPrefs(); 168 169 // observer for private browsing and our prefs branch 170 this._initObs(); 171 172 // jump list refresh timer 173 this._updateTimer(); 174 }, 175 176 update: function WTBJL_update() { 177 // are we disabled via prefs? don't do anything! 178 if (!this._enabled) { 179 return; 180 } 181 182 // do what we came here to do, update the taskbar jumplist 183 this._buildList(); 184 }, 185 186 _shutdown: function WTBJL__shutdown() { 187 this._shuttingDown = true; 188 this._free(); 189 }, 190 191 /** 192 * List building 193 * 194 * @note Async builders must add their mozIStoragePendingStatement to 195 * _pendingStatements object, using a different LIST_TYPE entry for 196 * each statement. Once finished they must remove it and call 197 * commitBuild(). When there will be no more _pendingStatements, 198 * commitBuild() will commit for real. 199 */ 200 201 _pendingStatements: {}, 202 _hasPendingStatements: function WTBJL__hasPendingStatements() { 203 return !!Object.keys(this._pendingStatements).length; 204 }, 205 206 async _buildList() { 207 if (this._hasPendingStatements()) { 208 // We were requested to update the list while another update was in 209 // progress, this could happen at shutdown, idle or privatebrowsing. 210 // Abort the current list building. 211 for (let listType in this._pendingStatements) { 212 this._pendingStatements[listType].cancel(); 213 delete this._pendingStatements[listType]; 214 } 215 this._builder.abortListBuild(); 216 } 217 218 // anything to build? 219 if (!this._showFrequent && !this._showRecent && !this._showTasks) { 220 // don't leave the last list hanging on the taskbar. 221 this._deleteActiveJumpList(); 222 return; 223 } 224 225 await this._startBuild(); 226 227 if (this._showTasks) { 228 this._buildTasks(); 229 } 230 231 // Space for frequent items takes priority over recent. 232 if (this._showFrequent) { 233 this._buildFrequent(); 234 } 235 236 if (this._showRecent) { 237 this._buildRecent(); 238 } 239 240 this._commitBuild(); 241 }, 242 243 /** 244 * Taskbar api wrappers 245 */ 246 247 async _startBuild() { 248 this._builder.abortListBuild(); 249 let URIsToRemove = await this._builder.initListBuild(); 250 if (URIsToRemove.length) { 251 // Prior to building, delete removed items from history. 252 this._clearHistory(URIsToRemove); 253 } 254 }, 255 256 _commitBuild: function WTBJL__commitBuild() { 257 if (this._hasPendingStatements()) { 258 return; 259 } 260 261 this._builder.commitListBuild(succeed => { 262 if (!succeed) { 263 this._builder.abortListBuild(); 264 } 265 }); 266 }, 267 268 _buildTasks: function WTBJL__buildTasks() { 269 var items = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); 270 this._tasks.forEach(function(task) { 271 if ( 272 (this._shuttingDown && !task.close) || 273 (!this._shuttingDown && !task.open) 274 ) { 275 return; 276 } 277 var item = this._getHandlerAppItem( 278 task.title, 279 task.description, 280 task.args, 281 task.iconIndex, 282 null 283 ); 284 items.appendElement(item); 285 }, this); 286 287 if (items.length) { 288 this._builder.addListToBuild( 289 this._builder.JUMPLIST_CATEGORY_TASKS, 290 items 291 ); 292 } 293 }, 294 295 _buildCustom: function WTBJL__buildCustom(title, items) { 296 if (items.length) { 297 this._builder.addListToBuild( 298 this._builder.JUMPLIST_CATEGORY_CUSTOMLIST, 299 items, 300 title 301 ); 302 } 303 }, 304 305 _buildFrequent: function WTBJL__buildFrequent() { 306 // Windows supports default frequent and recent lists, 307 // but those depend on internal windows visit tracking 308 // which we don't populate. So we build our own custom 309 // frequent and recent lists using our nav history data. 310 311 var items = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); 312 // track frequent items so that we don't add them to 313 // the recent list. 314 this._frequentHashList = []; 315 316 this._pendingStatements[LIST_TYPE.FREQUENT] = this._getHistoryResults( 317 Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING, 318 this._maxItemCount, 319 function(aResult) { 320 if (!aResult) { 321 delete this._pendingStatements[LIST_TYPE.FREQUENT]; 322 // The are no more results, build the list. 323 this._buildCustom(_getString("taskbar.frequent.label"), items); 324 this._commitBuild(); 325 return; 326 } 327 328 let title = aResult.title || aResult.uri; 329 let faviconPageUri = Services.io.newURI(aResult.uri); 330 let shortcut = this._getHandlerAppItem( 331 title, 332 title, 333 aResult.uri, 334 1, 335 faviconPageUri 336 ); 337 items.appendElement(shortcut); 338 this._frequentHashList.push(aResult.uri); 339 }, 340 this 341 ); 342 }, 343 344 _buildRecent: function WTBJL__buildRecent() { 345 var items = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); 346 // Frequent items will be skipped, so we select a double amount of 347 // entries and stop fetching results at _maxItemCount. 348 var count = 0; 349 350 this._pendingStatements[LIST_TYPE.RECENT] = this._getHistoryResults( 351 Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING, 352 this._maxItemCount * 2, 353 function(aResult) { 354 if (!aResult) { 355 // The are no more results, build the list. 356 this._buildCustom(_getString("taskbar.recent.label"), items); 357 delete this._pendingStatements[LIST_TYPE.RECENT]; 358 this._commitBuild(); 359 return; 360 } 361 362 if (count >= this._maxItemCount) { 363 return; 364 } 365 366 // Do not add items to recent that have already been added to frequent. 367 if ( 368 this._frequentHashList && 369 this._frequentHashList.includes(aResult.uri) 370 ) { 371 return; 372 } 373 374 let title = aResult.title || aResult.uri; 375 let faviconPageUri = Services.io.newURI(aResult.uri); 376 let shortcut = this._getHandlerAppItem( 377 title, 378 title, 379 aResult.uri, 380 1, 381 faviconPageUri 382 ); 383 items.appendElement(shortcut); 384 count++; 385 }, 386 this 387 ); 388 }, 389 390 _deleteActiveJumpList: function WTBJL__deleteAJL() { 391 this._builder.deleteActiveList(); 392 }, 393 394 /** 395 * Jump list item creation helpers 396 */ 397 398 _getHandlerAppItem: function WTBJL__getHandlerAppItem( 399 name, 400 description, 401 args, 402 iconIndex, 403 faviconPageUri 404 ) { 405 var file = Services.dirsvc.get("XREExeF", Ci.nsIFile); 406 407 var handlerApp = Cc[ 408 "@mozilla.org/uriloader/local-handler-app;1" 409 ].createInstance(Ci.nsILocalHandlerApp); 410 handlerApp.executable = file; 411 // handlers default to the leaf name if a name is not specified 412 if (name && name.length) { 413 handlerApp.name = name; 414 } 415 handlerApp.detailedDescription = description; 416 handlerApp.appendParameter(args); 417 418 var item = Cc["@mozilla.org/windows-jumplistshortcut;1"].createInstance( 419 Ci.nsIJumpListShortcut 420 ); 421 item.app = handlerApp; 422 item.iconIndex = iconIndex; 423 item.faviconPageUri = faviconPageUri; 424 return item; 425 }, 426 427 _getSeparatorItem: function WTBJL__getSeparatorItem() { 428 var item = Cc["@mozilla.org/windows-jumplistseparator;1"].createInstance( 429 Ci.nsIJumpListSeparator 430 ); 431 return item; 432 }, 433 434 /** 435 * Nav history helpers 436 */ 437 438 _getHistoryResults: function WTBLJL__getHistoryResults( 439 aSortingMode, 440 aLimit, 441 aCallback, 442 aScope 443 ) { 444 var options = PlacesUtils.history.getNewQueryOptions(); 445 options.maxResults = aLimit; 446 options.sortingMode = aSortingMode; 447 var query = PlacesUtils.history.getNewQuery(); 448 449 // Return the pending statement to the caller, to allow cancelation. 450 return PlacesUtils.history.asyncExecuteLegacyQuery(query, options, { 451 handleResult(aResultSet) { 452 for (let row; (row = aResultSet.getNextRow()); ) { 453 try { 454 aCallback.call(aScope, { 455 uri: row.getResultByIndex(1), 456 title: row.getResultByIndex(2), 457 }); 458 } catch (e) {} 459 } 460 }, 461 handleError(aError) { 462 Cu.reportError( 463 "Async execution error (" + aError.result + "): " + aError.message 464 ); 465 }, 466 handleCompletion(aReason) { 467 aCallback.call(WinTaskbarJumpList, null); 468 }, 469 }); 470 }, 471 472 _clearHistory: function WTBJL__clearHistory(uriSpecsToRemove) { 473 let URIsToRemove = uriSpecsToRemove 474 .map(spec => { 475 try { 476 // in case we get a bad uri 477 return Services.io.newURI(spec); 478 } catch (e) { 479 return null; 480 } 481 }) 482 .filter(uri => !!uri); 483 484 if (URIsToRemove.length) { 485 PlacesUtils.history.remove(URIsToRemove).catch(Cu.reportError); 486 } 487 }, 488 489 /** 490 * Prefs utilities 491 */ 492 493 _refreshPrefs: function WTBJL__refreshPrefs() { 494 this._enabled = _prefs.getBoolPref(PREF_TASKBAR_ENABLED); 495 this._showFrequent = _prefs.getBoolPref(PREF_TASKBAR_FREQUENT); 496 this._showRecent = _prefs.getBoolPref(PREF_TASKBAR_RECENT); 497 this._showTasks = _prefs.getBoolPref(PREF_TASKBAR_TASKS); 498 this._maxItemCount = _prefs.getIntPref(PREF_TASKBAR_ITEMCOUNT); 499 }, 500 501 /** 502 * Init and shutdown utilities 503 */ 504 505 _initTaskbar: function WTBJL__initTaskbar() { 506 this._builder = _taskbarService.createJumpListBuilder(); 507 if (!this._builder || !this._builder.available) { 508 return false; 509 } 510 511 return true; 512 }, 513 514 _initObs: function WTBJL__initObs() { 515 // If the browser is closed while in private browsing mode, the "exit" 516 // notification is fired on quit-application-granted. 517 // History cleanup can happen at profile-change-teardown. 518 Services.obs.addObserver(this, "profile-before-change"); 519 Services.obs.addObserver(this, "browser:purge-session-history"); 520 _prefs.addObserver("", this); 521 this._placesObserver = new PlacesWeakCallbackWrapper( 522 this.update.bind(this) 523 ); 524 PlacesUtils.observers.addListener( 525 ["history-cleared"], 526 this._placesObserver 527 ); 528 }, 529 530 _freeObs: function WTBJL__freeObs() { 531 Services.obs.removeObserver(this, "profile-before-change"); 532 Services.obs.removeObserver(this, "browser:purge-session-history"); 533 _prefs.removeObserver("", this); 534 if (this._placesObserver) { 535 PlacesUtils.observers.removeListener( 536 ["history-cleared"], 537 this._placesObserver 538 ); 539 } 540 }, 541 542 _updateTimer: function WTBJL__updateTimer() { 543 if (this._enabled && !this._shuttingDown && !this._timer) { 544 this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 545 this._timer.initWithCallback( 546 this, 547 _prefs.getIntPref(PREF_TASKBAR_REFRESH) * 1000, 548 this._timer.TYPE_REPEATING_SLACK 549 ); 550 } else if ((!this._enabled || this._shuttingDown) && this._timer) { 551 this._timer.cancel(); 552 delete this._timer; 553 } 554 }, 555 556 _hasIdleObserver: false, 557 _updateIdleObserver: function WTBJL__updateIdleObserver() { 558 if (this._enabled && !this._shuttingDown && !this._hasIdleObserver) { 559 _idle.addIdleObserver(this, IDLE_TIMEOUT_SECONDS); 560 this._hasIdleObserver = true; 561 } else if ( 562 (!this._enabled || this._shuttingDown) && 563 this._hasIdleObserver 564 ) { 565 _idle.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS); 566 this._hasIdleObserver = false; 567 } 568 }, 569 570 _free: function WTBJL__free() { 571 this._freeObs(); 572 this._updateTimer(); 573 this._updateIdleObserver(); 574 delete this._builder; 575 }, 576 577 notify: function WTBJL_notify(aTimer) { 578 // Add idle observer on the first notification so it doesn't hit startup. 579 this._updateIdleObserver(); 580 Services.tm.idleDispatchToMainThread(() => { 581 this.update(); 582 }); 583 }, 584 585 observe: function WTBJL_observe(aSubject, aTopic, aData) { 586 switch (aTopic) { 587 case "nsPref:changed": 588 if (this._enabled && !_prefs.getBoolPref(PREF_TASKBAR_ENABLED)) { 589 this._deleteActiveJumpList(); 590 } 591 this._refreshPrefs(); 592 this._updateTimer(); 593 this._updateIdleObserver(); 594 Services.tm.idleDispatchToMainThread(() => { 595 this.update(); 596 }); 597 break; 598 599 case "profile-before-change": 600 this._shutdown(); 601 break; 602 603 case "browser:purge-session-history": 604 this.update(); 605 break; 606 case "idle": 607 if (this._timer) { 608 this._timer.cancel(); 609 delete this._timer; 610 } 611 break; 612 613 case "active": 614 this._updateTimer(); 615 break; 616 } 617 }, 618}; 619