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