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