├── .gitignore
├── LICENSE
├── README.md
├── bootstrap.js
├── chrome.manifest
├── icon.png
├── install.rdf
├── options.xul
├── release.cmd
├── skin
├── style.css
└── urlbar-icon.png
└── update.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | *.xpi
2 | *.exe
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### [⬇ Lull The Tabs](https://github.com/JustOff/lull-the-tabs/releases)
2 |
3 | This add-on is designed to unload inactive tabs to free up browser memory. It is the full-featured successor of the BarTab / BarTab Heavy (Tycho) / BarTab Lite (X) extensions family, which uses modern APIs to ensure maximum performance and no conflicts with built-in browser functions and other extensions.
4 |
5 | Here is a small comparison table:
6 |
7 | | | BarTab Heavy | BarTab Lite X | Lull The Tabs |
8 | |-----------------------------------|----------------|-----------------|---------------|
9 | | Hook into Progress Listeners | on every tab | on every tab | **no** |
10 | | Hook into Web Navigation | on every tab | no | **no** |
11 | | Hook into Session Manager | yes | yes | **no** |
12 | | Conflict with "Find in page" | yes | no | **no** |
13 | | Prevent background tab loading | broken | no | **yes** |
14 | | Auto unload inactive tabs | yes | no | **yes** |
15 | | Manual unload whitelisted tabs | no | - | **yes** |
16 | | Compatible with full-screen video | no | - | **yes** |
17 | | Support wildcard whitelisting | no | - | **yes** |
18 | | Support IDN in the whitelist | no | - | **yes** |
19 | | Custom protocols whitelisting | no | - | **yes** |
20 | | Provide the unload button | no | no | **yes** |
21 | | Support for syncing settings | no | no | **yes** |
22 | | Restartless | no | yes | **yes** |
23 |
24 | If you used BarTab Heavy (Tycho), then its settings will be automatically imported at the first start. To add a wildcard domain to the exception list, hold down the Ctrl key while clicking on the corresponding context menu. Whitelisted tabs can be forced to unload using Ctrl+click on the unload button in the address bar. Any tab can be recreated ("full reload") using Ctrl+Shift+click on this button, and Ctrl+Alt+click on it - opens extension preferences.
25 |
--------------------------------------------------------------------------------
/bootstrap.js:
--------------------------------------------------------------------------------
1 | let Cc = Components.classes, Ci = Components.interfaces, Cu = Components.utils;
2 | Cu.import("resource://gre/modules/Services.jsm");
3 | Cu.import("resource://gre/modules/XPCOMUtils.jsm");
4 |
5 | const branch = "extensions.lull-the-tabs.";
6 | const ON_DEMAND_PREF = "browser.sessionstore.restore_on_demand";
7 | const PINNED_ON_DEMAND_PREF = "browser.sessionstore.restore_pinned_tabs_on_demand";
8 | const LOAD_IN_BACKGROUND = "browser.tabs.loadInBackground";
9 |
10 | const DEFAULT_PREFS = {
11 | importBarTab: true,
12 | showContext: true,
13 | showButton: true,
14 | selectOnUnload: 0,
15 | pauseBackgroundTabs: false,
16 | openNextToCurrent: false,
17 | autoUnload: false,
18 | unloadTimeout: 120,
19 | exceptionList: "",
20 | selectOnClose: 1,
21 | leftIsNearest: false,
22 | };
23 |
24 | XPCOMUtils.defineLazyServiceGetter(this, "gSessionStore",
25 | "@mozilla.org/browser/sessionstore;1",
26 | "nsISessionStore");
27 | XPCOMUtils.defineLazyServiceGetter(this, "eTLDService",
28 | "@mozilla.org/network/effective-tld-service;1",
29 | "nsIEffectiveTLDService");
30 | XPCOMUtils.defineLazyServiceGetter(this, "IDNService",
31 | "@mozilla.org/network/idn-service;1",
32 | "nsIIDNService");
33 | XPCOMUtils.defineLazyServiceGetter(this, "gFaviconService",
34 | "@mozilla.org/browser/favicon-service;1",
35 | "nsIFaviconService");
36 | XPCOMUtils.defineLazyServiceGetter(this, "gHistoryService",
37 | "@mozilla.org/browser/nav-history-service;1",
38 | "nsINavHistoryService");
39 | XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
40 | "resource://gre/modules/PlacesUtils.jsm");
41 | XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
42 | "resource://gre/modules/PrivateBrowsingUtils.jsm");
43 |
44 | let styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
45 | let styleSheetURI = Services.io.newURI("chrome://lull-the-tabs/skin/style.css", null, null);
46 |
47 | let domRegex = null, gWindowListener;
48 |
49 | function initPreferences() {
50 | let defaultBranch = Services.prefs.getDefaultBranch(branch);
51 | let syncBranch = Services.prefs.getDefaultBranch("services.sync.prefs.sync." + branch);
52 | for (let pref in DEFAULT_PREFS) {
53 | switch (typeof DEFAULT_PREFS[pref]) {
54 | case "string":
55 | defaultBranch.setCharPref(pref, DEFAULT_PREFS[pref]);
56 | break;
57 | case "number":
58 | defaultBranch.setIntPref(pref, DEFAULT_PREFS[pref]);
59 | break;
60 | case "boolean":
61 | defaultBranch.setBoolPref(pref, DEFAULT_PREFS[pref]);
62 | break;
63 | }
64 | syncBranch.setBoolPref(pref, true);
65 | }
66 |
67 | if (Services.prefs.getBoolPref(branch + "importBarTab")) {
68 | Services.prefs.setBoolPref(branch + "importBarTab", false);
69 | try {
70 | Services.prefs.setCharPref(branch + "exceptionList", Services.prefs.getCharPref("extensions.bartab.whitelist"));
71 | } catch (e) {}
72 | try {
73 | Services.prefs.setBoolPref(branch + "autoUnload", Services.prefs.getBoolPref("extensions.bartab.unloadAfterTimeout"));
74 | Services.prefs.setIntPref(branch + "unloadTimeout",
75 | Math.round(Services.prefs.getIntPref("extensions.bartab.timeoutUnit") *
76 | Services.prefs.getIntPref("extensions.bartab.timeoutValue") / 60));
77 | } catch (e) {}
78 | try {
79 | if (!Services.prefs.getBoolPref("extensions.bartab.findClosestLoadedTab")) {
80 | Services.prefs.setIntPref(branch + "selectOnClose", 0);
81 | }
82 | } catch (e) {}
83 | try {
84 | Services.prefs.setBoolPref(branch + "pauseBackgroundTabs",
85 | Services.prefs.getIntPref("extensions.bartab.loadBackgroundTabs") == 1);
86 | } catch (e) {}
87 | }
88 | }
89 |
90 | function getHostOrCustomProtoURL(aURI) {
91 | try {
92 | return aURI.host;
93 | } catch (e) {
94 | let match = /^(\w+:\w+)(\?.+)?$/.exec(aURI.spec);
95 | if (match) {
96 | return match[1];
97 | }
98 | }
99 | }
100 |
101 | function isWhiteListed(aURI) {
102 | if (domRegex === null) {
103 | try {
104 | var exceptionList = Services.prefs.getComplexValue(branch + "exceptionList", Ci.nsISupportsString).data;
105 | domRegex = new RegExp("^(" + exceptionList.replace(/;/g,"|").replace(/\./g,"\\.").replace(/\*/g,".*") + ")$");
106 | } catch (e) {
107 | return false;
108 | }
109 | }
110 | return domRegex.test(getHostOrCustomProtoURL(aURI));
111 | }
112 |
113 | /*
114 | * In relation to a given tab, find the closest tab that is loaded.
115 | * Note: if there's no such tab available, this will return unloaded
116 | * tabs as a last resort.
117 | */
118 | function findClosestLoadedTab(aTab, aTabbrowser) {
119 | let visibleTabs = aTabbrowser.visibleTabs;
120 |
121 | // Shortcut: if this is the only tab available, we're not going to
122 | // find another active one, are we...
123 | if (visibleTabs.length == 1) {
124 | return null;
125 | }
126 |
127 | // If leftIsNearest, then try previous sibling first
128 | if (Services.prefs.getBoolPref(branch + "leftIsNearest") &&
129 | aTab.previousSibling && !aTab.previousSibling.hasAttribute("pending")) {
130 | return aTab.previousSibling;
131 | }
132 |
133 | // The most obvious choice would be the owner tab, if it's active and is
134 | // part of the same tab group.
135 | if (aTab.owner
136 | && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
137 | && !aTab.owner.hasAttribute("pending")) {
138 | let i = 0;
139 | while (i < visibleTabs.length) {
140 | if (visibleTabs[i] == aTab.owner) {
141 | return aTab.owner;
142 | }
143 | i++;
144 | }
145 | }
146 |
147 | // Otherwise walk the list of visible tabs and see if we can find an
148 | // active one.
149 | // To do that, first we need the index of the current tab in the visible-
150 | // tabs array.
151 | // However, if the current tab is being closed, it's already been removed
152 | // from that array. Therefore, we have to also accept its next-higher
153 | // sibling, if one is found. If one isn't, then the current tab was at
154 | // the end of the visible-tabs array, and the new end-of-array tab is the
155 | // best choice for a substitute index.
156 | let tabIndex = 0;
157 | while (tabIndex + 1 < visibleTabs.length &&
158 | visibleTabs[tabIndex] != aTab &&
159 | visibleTabs[tabIndex] != aTab.nextSibling) {
160 | // This loop will result in tabIndex pointing to one of three places:
161 | // The current tab (visibleTabs[i] == aTab)
162 | // The tab which had one index higher than the current tab, until the
163 | // current tab was closed (visibleTabs[i] == aTab.nextSibling)
164 | // The final tab in the array (tabIndex + 1 == visibleTabs.length)
165 | tabIndex++;
166 | }
167 |
168 | let i = 0;
169 | while ((tabIndex - i >= 0) ||
170 | (tabIndex + i < visibleTabs.length)) {
171 | let offsetIncremented = 0;
172 | if (tabIndex + i < visibleTabs.length) {
173 | if (!visibleTabs[tabIndex + i].hasAttribute("pending") &&
174 | visibleTabs[tabIndex + i] != aTab) {
175 | // The '!= aTab' test is to rule out the case where i == 0 and
176 | // aTab is being unloaded rather than closed, so that tabIndex
177 | // points to aTab instead of its nextSibling.
178 | return visibleTabs[tabIndex + i];
179 | }
180 | }
181 | if(i == 0 && visibleTabs[tabIndex] != aTab) {
182 | // This is ugly, but should work.
183 | // If aTab has been closed, and nextSibling is unloaded, then we
184 | // have to check previousSibling before the next loop, or we'll take
185 | // nextSibling.nextSibling (if loaded) over previousSibling, which is
186 | // closer to the true "x.5" tabIndex offset.
187 | offsetIncremented = 1;
188 | i++;
189 | }
190 | if (tabIndex - i >= 0) {
191 | if(!visibleTabs[tabIndex - i].hasAttribute("pending") &&
192 | visibleTabs[tabIndex - i] != aTab) {
193 | return visibleTabs[tabIndex - i];
194 | }
195 | }
196 | if(offsetIncremented > 0) {
197 | offsetIncremented = 0;
198 | i--;
199 | }
200 | i++;
201 | }
202 |
203 | // Fallback: there isn't an active tab available, so we're going
204 | // to have to nominate a non-active one.
205 |
206 | // Start with the owner, if appropriate.
207 | if (aTab.owner &&
208 | Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")) {
209 | let i = 0;
210 | while (i < visibleTabs.length) {
211 | if (visibleTabs[i] == aTab.owner) {
212 | return aTab.owner;
213 | }
214 | i++;
215 | }
216 | }
217 | // Otherwise, fall back to one of the adjacent tabs.
218 | if (tabIndex < visibleTabs.length &&
219 | visibleTabs[tabIndex] != aTab) {
220 | // aTab was closed, so the tab at its previous index is the correct
221 | // first choice
222 | return visibleTabs[tabIndex];
223 | }
224 | if (tabIndex + 1 < visibleTabs.length) {
225 | return visibleTabs[tabIndex + 1];
226 | }
227 | if (tabIndex - 1 >= 0) {
228 | return visibleTabs[tabIndex - 1];
229 | }
230 |
231 | // If we get this far, something's wrong. It shouldn't be possible for
232 | // there to not be an adjacent tab unless (visibleTabs.length == 1).
233 | Cu.reportError("Lull The Tabs: there are " + visibleTabs.length + " visible tabs, which is greater than 1, but no suitable tab was found from index " + tabIndex);
234 | return null;
235 | }
236 |
237 | /**
238 | * This handler attaches to the tabbrowser. It listens to various tab
239 | * related events.
240 | */
241 | function LullTheTabs(aWindow) {
242 | this.init(aWindow);
243 | }
244 | LullTheTabs.prototype = {
245 |
246 | init: function(aWindow) {
247 | this.browserWindow = aWindow;
248 | this.tabBrowser = aWindow.gBrowser;
249 | this.smoothScroll = this.tabBrowser.tabContainer.mTabstrip.smoothScroll;
250 | this.previousTab = null;
251 | this.selectedTab = this.tabBrowser.selectedTab;
252 |
253 | this.tabBrowser.tabContainer.addEventListener('TabOpen', this, false);
254 | this.tabBrowser.tabContainer.addEventListener('TabSelect', this, false);
255 | this.tabBrowser.tabContainer.addEventListener('TabClose', this, false);
256 |
257 | this.prefBranch = Services.prefs.getBranch(branch);
258 | this.prefBranch.addObserver("", this, false);
259 |
260 | if (Services.prefs.getBoolPref(branch + "autoUnload")) {
261 | this.startAllTimers();
262 | }
263 |
264 | if (Services.prefs.getBoolPref(branch + "showContext")) {
265 | this.addContext();
266 | }
267 |
268 | if (Services.prefs.getBoolPref(branch + "showButton")) {
269 | this.addButton();
270 | }
271 |
272 | if (Services.prefs.getBoolPref(branch + "pauseBackgroundTabs")) {
273 | this.hookOpenInBackground();
274 | }
275 | },
276 |
277 | done: function() {
278 | this.clearAllTimers();
279 |
280 | if (Services.prefs.getBoolPref(branch + "showContext")) {
281 | this.removeContext();
282 | }
283 |
284 | if (Services.prefs.getBoolPref(branch + "showButton")) {
285 | this.removeButton();
286 | }
287 |
288 | if (Services.prefs.getBoolPref(branch + "pauseBackgroundTabs")) {
289 | this.unhookOpenInBackground();
290 | }
291 |
292 | this.tabBrowser.tabContainer.removeEventListener('TabOpen', this, false);
293 | this.tabBrowser.tabContainer.removeEventListener('TabSelect', this, false);
294 | this.tabBrowser.tabContainer.removeEventListener('TabClose', this, false);
295 |
296 | this.prefBranch.removeObserver("", this);
297 | this.prefBranch = null;
298 |
299 | this.previousTab = null;
300 | this.selectedTab = null;
301 | this.smoothScroll = null;
302 | this.tabBrowser = null;
303 | this.browserWindow = null;
304 | },
305 |
306 | handleEvent: function(aEvent) {
307 | switch (aEvent.type) {
308 | case 'popupshowing':
309 | this.onPopupShowing(aEvent);
310 | return;
311 | case 'TabOpen':
312 | this.onTabOpen(aEvent);
313 | return;
314 | case 'TabSelect':
315 | this.onTabSelect(aEvent);
316 | return;
317 | case 'TabClose':
318 | this.onTabClose(aEvent);
319 | return;
320 | case 'TabPinned':
321 | this.onTabPinned(aEvent);
322 | return;
323 | case 'TabUnpinned':
324 | this.onTabPinned(aEvent);
325 | return;
326 | }
327 | },
328 |
329 | observe: function(aSubject, aTopic, aData) {
330 | if (aTopic != "nsPref:changed") return;
331 | switch (aData) {
332 | case 'autoUnload':
333 | if (Services.prefs.getBoolPref(branch + "autoUnload")) {
334 | this.startAllTimers();
335 | } else {
336 | this.clearAllTimers();
337 | }
338 | break;
339 | case 'unloadTimeout':
340 | if (Services.prefs.getBoolPref(branch + "autoUnload")) {
341 | this.clearAllTimers();
342 | this.startAllTimers();
343 | }
344 | break;
345 | case 'showContext':
346 | if (Services.prefs.getBoolPref(branch + "showContext")) {
347 | this.addContext();
348 | } else {
349 | this.removeContext();
350 | }
351 | break;
352 | case 'showButton':
353 | if (Services.prefs.getBoolPref(branch + "showButton")) {
354 | this.addButton();
355 | } else {
356 | this.removeButton();
357 | }
358 | break;
359 | case 'pauseBackgroundTabs':
360 | if (Services.prefs.getBoolPref(branch + "pauseBackgroundTabs")) {
361 | this.hookOpenInBackground();
362 | } else {
363 | this.unhookOpenInBackground();
364 | }
365 | break;
366 | }
367 | },
368 |
369 | /**
370 | * Handle the 'popupshowing' event from "tabContextMenu"
371 | * and disable "Unload Tab" if the context menu was opened on a pending tab.
372 | */
373 | onPopupShowing: function(aEvent) {
374 | let tabContextMenu = aEvent.originalTarget;
375 | let document = tabContextMenu.ownerDocument;
376 | let tab = tabContextMenu.contextTab;
377 | tab = tab || tabContextMenu.triggerNode.localName == "tab" ?
378 | tabContextMenu.triggerNode : this.tabBrowser.selectedTab;
379 |
380 | let menuitem_unloadTab = document.getElementById("lull-the-tabs-unload");
381 | let menuitem_neverUnload = document.getElementById("lull-the-tabs-never-unload");
382 |
383 | let needlessToUnload = tab.hasAttribute("pending") ||
384 | tab.hasAttribute("pinned") &&
385 | !Services.prefs.getBoolPref(PINNED_ON_DEMAND_PREF);
386 |
387 | let host = getHostOrCustomProtoURL(tab.linkedBrowser.currentURI);
388 |
389 | if (!host) {
390 | menuitem_neverUnload.setAttribute("hidden", "true");
391 | if (needlessToUnload) {
392 | menuitem_unloadTab.setAttribute("disabled", "true");
393 | } else {
394 | menuitem_unloadTab.removeAttribute("disabled");
395 | }
396 | return;
397 | }
398 |
399 | if (isWhiteListed(tab.linkedBrowser.currentURI)) {
400 | // If we whitelisting by a wildcard, display it instead of the current host.
401 | let whitelist = [];
402 | let wlpref = Services.prefs.getComplexValue(branch + "exceptionList", Ci.nsISupportsString).data;
403 | if (wlpref) {
404 | whitelist = wlpref.split(";");
405 | }
406 | for (let i = 0; i < whitelist.length; i++) {
407 | let reg = new RegExp("^" + whitelist[i].replace(/\./g,"\\.").replace(/\*/g,".*") + "$");
408 | if (reg.test(host)) {
409 | host = whitelist[i];
410 | break;
411 | }
412 | }
413 | menuitem_neverUnload.setAttribute("checked", "true");
414 | menuitem_unloadTab.setAttribute("disabled", "true");
415 | } else {
416 | menuitem_neverUnload.removeAttribute("checked");
417 | if (needlessToUnload) {
418 | menuitem_unloadTab.setAttribute("disabled", "true");
419 | } else {
420 | menuitem_unloadTab.removeAttribute("disabled");
421 | }
422 | }
423 |
424 | menuitem_neverUnload.setAttribute("label", 'Never Unload "' + host + '"');
425 | menuitem_neverUnload.removeAttribute("hidden");
426 | },
427 |
428 | onTabOpen: function(aEvent) {
429 | let tab = aEvent.originalTarget;
430 | if (!tab.selected && Services.prefs.getBoolPref(branch + "autoUnload")) {
431 | this.startTimer(tab);
432 | }
433 | },
434 |
435 | onTabSelect: function(aEvent) {
436 | this.previousTab = this.selectedTab;
437 | this.selectedTab = aEvent.originalTarget;
438 | this.selectedTab.removeAttribute("bgpending");
439 |
440 | // The previous tab may not be available because it has been closed.
441 | if (this.previousTab && Services.prefs.getBoolPref(branch + "autoUnload")) {
442 | this.startTimer(this.previousTab);
443 | }
444 | this.clearTimer(this.selectedTab);
445 | },
446 |
447 | onTabClose: function(aEvent) {
448 | let tab = aEvent.originalTarget;
449 | this.clearTimer(tab);
450 |
451 | if (tab == this.selectedTab) {
452 | this.selectedTab = null;
453 | };
454 | if (tab == this.previousTab) {
455 | this.previousTab = null;
456 | };
457 |
458 | // Check selectOnClose option.
459 | let selectOnClose = Services.prefs.getIntPref(branch + "selectOnClose");
460 | if (selectOnClose == 0) {
461 | // Return if browser default behaviour is selected.
462 | return;
463 | }
464 |
465 | // If we are on the selected tab.
466 | if (tab.selected) {
467 | if (selectOnClose == 1) {
468 | // Find the closest tab that isn't unloaded.
469 | let activeTab = findClosestLoadedTab(tab, this.tabBrowser);
470 | if (activeTab) {
471 | this.tabBrowser.selectedTab = activeTab;
472 | }
473 | } else {
474 | // Or select the previous tab.
475 | if (this.previousTab) {
476 | this.tabBrowser.selectedTab = this.previousTab;
477 | }
478 | }
479 | }
480 | },
481 |
482 | /**
483 | * Unload a tab.
484 | */
485 | unloadTab: function(aTab, aOptions) {
486 | // Ignore tabs that are pinned, already unloaded or whitelisted, unless the unload is forced.
487 | if (isWhiteListed(aTab.linkedBrowser.currentURI) && (!aOptions || aOptions && !aOptions.force) ||
488 | aTab.hasAttribute("pending") || !Services.prefs.getBoolPref(ON_DEMAND_PREF) ||
489 | aTab.hasAttribute("pinned") && !Services.prefs.getBoolPref(PINNED_ON_DEMAND_PREF)) {
490 | return;
491 | }
492 |
493 | // If we were called from the timer and the browser is in full-screen, reschedule the unloading.
494 | if (aOptions && aOptions.timer && (this.browserWindow.document.fullscreenElement ||
495 | this.browserWindow.document.mozFullScreenElement)) {
496 | this.startTimer(aTab, 5);
497 | return;
498 | }
499 |
500 | let tabbrowser = this.tabBrowser;
501 |
502 | // If we are not in the full reload mode, find a tab to select.
503 | if (!aOptions || aOptions && (!aOptions.force || !aOptions.reload)) {
504 | // Make sure that we're not on this tab.
505 | if (aTab.selected) {
506 | // If we are, then check selectOnUnload option.
507 | if (Services.prefs.getIntPref(branch + "selectOnUnload") == 0) {
508 | // Find the closest tab that isn't unloaded.
509 | let activeTab = findClosestLoadedTab(aTab, tabbrowser);
510 | if (activeTab) {
511 | tabbrowser.selectedTab = activeTab;
512 | }
513 | } else {
514 | // Or select the previous tab.
515 | if (this.previousTab) {
516 | tabbrowser.selectedTab = this.previousTab;
517 | }
518 | }
519 | }
520 | }
521 |
522 | // If we were called from the timer, temporarily disable smoothScroll
523 | // to avoid undesirable side effects of the addTab() call.
524 | if (aOptions && aOptions.timer && this.smoothScroll) {
525 | tabbrowser.tabContainer.mTabstrip.smoothScroll = false;
526 | }
527 |
528 | let newtab = tabbrowser.addTab(null, {skipAnimation: true});
529 |
530 | if (aOptions && aOptions.timer && this.smoothScroll) {
531 | // We need to use setTimeout() because addTab() uses it to call _handleNewTab().
532 | this.browserWindow.setTimeout(function() {
533 | tabbrowser.tabContainer.mTabstrip.smoothScroll = true;
534 | }, 0);
535 | }
536 |
537 | // Copy the session state from the original tab to the new one.
538 | // If we ever support a mode where 'browser.sessionstore.max_concurrent_tabs'
539 | // wasn't set to 0, we'd have to do some trickery here.
540 | gSessionStore.setTabState(newtab, gSessionStore.getTabState(aTab));
541 |
542 | // Move the new tab next to the one we're removing, but not in
543 | // front of it as that confuses Tree Style Tab.
544 | tabbrowser.moveTabTo(newtab, aTab._tPos + 1);
545 |
546 | // Restore tree when using Tree Style Tab
547 | if (tabbrowser.treeStyleTab) {
548 | let parent = tabbrowser.treeStyleTab.getParentTab(aTab);
549 | if (parent) {
550 | tabbrowser.treeStyleTab.attachTabTo(newtab, parent,
551 | {dontAnimate: true, insertBefore: aTab.nextSibling});
552 | }
553 | let children = tabbrowser.treeStyleTab.getChildTabs(aTab);
554 | children.forEach(function(aChild) {
555 | // Explicitly detach tabs to prevent them from closing due to a bug in attachTabTo
556 | tabbrowser.treeStyleTab.detachTab(
557 | aChild, {dontAnimate: true});
558 | tabbrowser.treeStyleTab.attachTabTo(
559 | aChild, newtab, {dontAnimate: true});
560 | });
561 | }
562 |
563 | // Restore tree when using Tab Kit 2
564 | if (this.browserWindow.tabkit && this.browserWindow.tabkit.api) {
565 | let tk2api = this.browserWindow.tabkit.api;
566 | let parent = tk2api.getParentTab(aTab);
567 | if (parent) {
568 | tk2api.addChildTabs(parent, [newtab]);
569 | }
570 | let children = tk2api.getChildTabs(aTab);
571 | if (children && children.length) {
572 | tk2api.addChildTabs(newtab, children);
573 | }
574 | tk2api.resetTab(aTab);
575 | }
576 |
577 | // If we are in the full reload mode, select the new tab.
578 | if (aOptions && aOptions.force && aOptions.reload) {
579 | tabbrowser.selectedTab = newtab;
580 | }
581 |
582 | // Close the original tab. We're taking the long way round to
583 | // ensure the nsISessionStore service won't save this in the
584 | // recently closed tabs.
585 | if (tabbrowser._beginRemoveTab(aTab, true, null, false)) {
586 | let browser = tabbrowser.getBrowserForTab(aTab);
587 | if (browser.registeredOpenURI) {
588 | if (tabbrowser._placesAutocomplete) {
589 | tabbrowser._placesAutocomplete.unregisterOpenPage(browser.registeredOpenURI);
590 | } else if (tabbrowser._unifiedComplete) {
591 | try {
592 | tabbrowser._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI);
593 | } catch (e) {
594 | let userContextId = tabbrowser.getAttribute("usercontextid") || 0;
595 | tabbrowser._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI, userContextId);
596 | }
597 | }
598 | delete browser.registeredOpenURI;
599 | }
600 | tabbrowser._endRemoveTab(aTab);
601 | }
602 | },
603 |
604 | unloadOtherTabs: function(aTab) {
605 | let tabbrowser = this.tabBrowser;
606 |
607 | // Make sure we're sitting on the tab that isn't going to be unloaded.
608 | if (tabbrowser.selectedTab != aTab) {
609 | tabbrowser.selectedTab = aTab;
610 | }
611 |
612 | // unloadTab() mutates the tabs so the only sane thing to do is to
613 | // copy the list of tabs now and then work off that list.
614 | //
615 | // Which tab list to copy depends on the pref.
616 | //
617 | //TODO can we use Array.slice() here?
618 | let tabs = [];
619 | let tabSource = tabbrowser.visibleTabs;
620 | if (!tabSource) {
621 | return;
622 | }
623 | for (let i = 0; i < tabSource.length; i++) {
624 | tabs.push(tabSource[i]);
625 | }
626 | for (let i = 0; i < tabs.length; i++) {
627 | if (tabs[i] != aTab) {
628 | this.unloadTab(tabs[i]);
629 | }
630 | }
631 | },
632 |
633 | toggleWhitelist: function(aTab, e) {
634 | let host = getHostOrCustomProtoURL(aTab.linkedBrowser.currentURI);
635 | if (!host) {
636 | return;
637 | }
638 |
639 | let whitelist = [];
640 | let wlpref = Services.prefs.getComplexValue(branch + "exceptionList", Ci.nsISupportsString).data;
641 | if (wlpref) {
642 | whitelist = wlpref.split(";");
643 | }
644 | if (isWhiteListed(aTab.linkedBrowser.currentURI)) {
645 | for (let i = 0; i < whitelist.length; i++) {
646 | let reg = new RegExp("^" + whitelist[i].replace(/\./g,"\\.").replace(/\*/g,".*") + "$");
647 | if (reg.test(host)) {
648 | whitelist.splice(i, 1);
649 | break;
650 | }
651 | }
652 | } else {
653 | if (e.ctrlKey) {
654 | try {
655 | host = "(*.)?" + IDNService.convertACEtoUTF8(eTLDService.getBaseDomainFromHost(host));
656 | } catch (e) {}
657 | }
658 | whitelist.push(host);
659 | }
660 |
661 | let str = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
662 | str.data = whitelist.join(";");
663 | Services.prefs.setComplexValue(branch + "exceptionList", Ci.nsISupportsString, str);
664 | domRegex = null;
665 |
666 | if (this.button && aTab == this.tabBrowser.selectedTab) {
667 | this.updateButton(aTab.linkedBrowser.currentURI);
668 | }
669 | },
670 |
671 | startTimer: function(aTab, aTimeout) {
672 | if (aTab.hasAttribute("pending")) {
673 | return;
674 | }
675 | if (aTab._lullTheTabsTimer) {
676 | this.clearTimer(aTab);
677 | }
678 | let timeout = Services.prefs.getIntPref(branch + "unloadTimeout") * 60 * 1000;
679 | if (aTimeout) {
680 | timeout = Math.min(timeout, aTimeout * 60 * 1000);
681 | }
682 | let window = aTab.ownerDocument.defaultView;
683 | // Allow 'this' to leak into the inline function
684 | let self = this;
685 | aTab._lullTheTabsTimer = window.setTimeout(function() {
686 | // The timer will be removed automatically since
687 | // unloadTab() will close and replace the original tab.
688 | self.unloadTab(aTab, {timer: true});
689 | }, timeout);
690 | },
691 |
692 | clearTimer: function(aTab) {
693 | let window = aTab.ownerDocument.defaultView;
694 | window.clearTimeout(aTab._lullTheTabsTimer);
695 | aTab._lullTheTabsTimer = null;
696 | },
697 |
698 | startAllTimers: function() {
699 | let visibleTabs = this.tabBrowser.visibleTabs;
700 | for (let i = 0; i < visibleTabs.length; i++) {
701 | if (!visibleTabs[i].selected) {
702 | this.startTimer(visibleTabs[i]);
703 | }
704 | }
705 | },
706 |
707 | clearAllTimers: function() {
708 | let visibleTabs = this.tabBrowser.visibleTabs;
709 | for (let i = 0; i < visibleTabs.length; i++) {
710 | this.clearTimer(visibleTabs[i]);
711 | }
712 | },
713 |
714 | addContext: function() {
715 | let document = this.tabBrowser.ownerDocument;
716 | let tabContextMenu = document.getElementById("tabContextMenu");
717 | let openTabInWindow = document.getElementById("context_openTabInWindow");
718 |
719 | // add "Unload Tab" menuitem to tab context menu
720 | let menuitem_unloadTab = document.createElement("menuitem");
721 | menuitem_unloadTab.setAttribute("id", "lull-the-tabs-unload");
722 | menuitem_unloadTab.setAttribute("label", "Unload Tab"); // TODO l10n
723 | menuitem_unloadTab.setAttribute("tbattr", "tabbrowser-multiple");
724 | menuitem_unloadTab.setAttribute(
725 | "oncommand", "gBrowser.LullTheTabs.unloadTab(gBrowser.mContextTab);");
726 | tabContextMenu.insertBefore(menuitem_unloadTab, openTabInWindow);
727 |
728 | // add "Unload Other Tabs" menuitem to tab context menu
729 | let menuitem_unloadOtherTabs = document.createElement("menuitem");
730 | menuitem_unloadOtherTabs.setAttribute("id", "lull-the-tabs-unload-others");
731 | menuitem_unloadOtherTabs.setAttribute("label", "Unload Other Tabs"); // TODO l10n
732 | menuitem_unloadOtherTabs.setAttribute("tbattr", "tabbrowser-multiple");
733 | menuitem_unloadOtherTabs.setAttribute(
734 | "oncommand", "gBrowser.LullTheTabs.unloadOtherTabs(gBrowser.mContextTab);");
735 | tabContextMenu.insertBefore(menuitem_unloadOtherTabs, openTabInWindow);
736 |
737 | // add "Never Unload" menuitem to tab context menu
738 | let menuitem_neverUnload = document.createElement("menuitem");
739 | menuitem_neverUnload.setAttribute("id", "lull-the-tabs-never-unload");
740 | menuitem_neverUnload.setAttribute("label", "Never Unload Tab"); // TODO l10n
741 | menuitem_neverUnload.setAttribute("type", "checkbox");
742 | menuitem_neverUnload.setAttribute("autocheck", "false");
743 | menuitem_neverUnload.setAttribute(
744 | "oncommand", "gBrowser.LullTheTabs.toggleWhitelist(gBrowser.mContextTab, event);");
745 | tabContextMenu.insertBefore(menuitem_neverUnload, openTabInWindow);
746 |
747 | tabContextMenu.addEventListener('popupshowing', this, false);
748 | },
749 |
750 | removeContext: function() {
751 | let document = this.tabBrowser.ownerDocument;
752 | let tabContextMenu = document.getElementById("tabContextMenu");
753 |
754 | tabContextMenu.removeEventListener('popupshowing', this, false);
755 |
756 | // remove tab context menu related stuff
757 | let menuitem_unloadTab = document.getElementById("lull-the-tabs-unload");
758 | if (menuitem_unloadTab && menuitem_unloadTab.parentNode) {
759 | menuitem_unloadTab.parentNode.removeChild(menuitem_unloadTab);
760 | }
761 | let menuitem_unloadOtherTabs = document.getElementById("lull-the-tabs-unload-others");
762 | if (menuitem_unloadOtherTabs && menuitem_unloadOtherTabs.parentNode) {
763 | menuitem_unloadOtherTabs.parentNode.removeChild(menuitem_unloadOtherTabs);
764 | }
765 | let menuitem_neverUnload = document.getElementById("lull-the-tabs-never-unload");
766 | if (menuitem_neverUnload && menuitem_neverUnload.parentNode) {
767 | menuitem_neverUnload.parentNode.removeChild(menuitem_neverUnload);
768 | }
769 | },
770 |
771 | updateButton: function(aURI) {
772 | if (isWhiteListed(aURI) ||
773 | this.tabBrowser.selectedTab.hasAttribute("pinned") &&
774 | !Services.prefs.getBoolPref(PINNED_ON_DEMAND_PREF)) {
775 | if (!this.button.hasAttribute("protected")) {
776 | this.button.setAttribute("protected", "true");
777 | this.button.setAttribute("tooltiptext", "Active tab is protected from unloading");
778 | }
779 | } else {
780 | if (this.button.hasAttribute("protected")) {
781 | this.button.removeAttribute("protected");
782 | this.button.setAttribute("tooltiptext", "Unload active tab");
783 | }
784 | }
785 | },
786 |
787 | onLocationChange: function(aWebProgress, aRequest, aLocation, aFlag) {
788 | this.updateButton(aLocation);
789 | },
790 |
791 | onTabPinned: function(aEvent) {
792 | let tab = aEvent.originalTarget;
793 | if (tab == this.tabBrowser.selectedTab) {
794 | this.updateButton(tab.linkedBrowser.currentURI);
795 | }
796 | },
797 |
798 | onClickButton: function(aEvent) {
799 | if ((aEvent.ctrlKey || aEvent.metaKey) && aEvent.altKey) {
800 | this.browserWindow.BrowserOpenAddonsMgr("addons://detail/lull-the-tabs@Off.JustOff/preferences");
801 | } else {
802 | this.unloadTab(this.tabBrowser.selectedTab, {force: aEvent.ctrlKey || aEvent.metaKey,
803 | reload: aEvent.shiftKey});
804 | }
805 | },
806 |
807 | addButton: function() {
808 | let document = this.tabBrowser.ownerDocument;
809 | let button = document.createElement("image");
810 | button.setAttribute("id", "lull-the-tabs-button");
811 | button.setAttribute("class", "urlbar-icon");
812 | button.setAttribute("tooltiptext", "Unload active tab");
813 | button.setAttribute("onclick", "gBrowser.LullTheTabs.onClickButton(event);");
814 | let urlBarIcons = document.getElementById("urlbar-icons");
815 | urlBarIcons.insertBefore(button, urlBarIcons.firstChild);
816 | this.button = button;
817 | this.tabBrowser.addProgressListener(this);
818 | this.tabBrowser.tabContainer.addEventListener('TabPinned', this, false);
819 | this.tabBrowser.tabContainer.addEventListener('TabUnpinned', this, false);
820 | },
821 |
822 | removeButton: function() {
823 | this.tabBrowser.removeProgressListener(this);
824 | this.tabBrowser.tabContainer.removeEventListener('TabPinned', this, false);
825 | this.tabBrowser.tabContainer.removeEventListener('TabUnpinned', this, false);
826 | this.button.parentNode.removeChild(this.button);
827 | this.button = null;
828 | },
829 |
830 | openInBackground: function(aWindow, aHref, aTitle, aReferrer) {
831 | let session = {"entries": [{"url": aHref, "referrer": aReferrer}]};
832 | if (aTitle != "") {
833 | session["entries"][0]["title"] = aTitle + ' :: ' + aHref;
834 | }
835 | if (aWindow.gBrowser.selectedTab.getAttribute("privateTab-isPrivate")) {
836 | session["attributes"] = {"privateTab-isPrivate": "true"};
837 | }
838 | let asyncFavicons = gFaviconService.QueryInterface(Ci.mozIAsyncFavicons);
839 | let sHref = aHref.split(/\/+/g);
840 | asyncFavicons.getFaviconURLForPage(Services.io.newURI(sHref[0] + "//" + sHref[1], null, null), function (aURI) {
841 | if (aURI && aURI.spec) {
842 | session["image"] = aURI.spec;
843 | } else if (typeof sHref[1] == "string") {
844 | let hist = gHistoryService;
845 | let hopt = hist.getNewQueryOptions();
846 | hopt.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
847 | hopt.maxResults = 1;
848 | let hquery = hist.getNewQuery();
849 | hquery.domain = sHref[1];
850 | hquery.domainIsHost = true;
851 | let hresult = hist.executeQuery(hquery, hopt);
852 | hresult.root.containerOpen = true;
853 | if (hresult.root.childCount) {
854 | let info = hresult.root.getChild(0);
855 | if (info.icon) {
856 | session["image"] = info.icon;
857 | }
858 | }
859 | hresult.root.containerOpen = false;
860 | }
861 | let newtab = aWindow.gBrowser.addTab(null, {skipAnimation: true});
862 | gSessionStore.setTabState(newtab, JSON.stringify(session));
863 | newtab.setAttribute("bgpending", true);
864 | if (Services.prefs.getBoolPref(branch + "openNextToCurrent")) {
865 | aWindow.gBrowser.moveTabTo(newtab, aWindow.gBrowser.selectedTab._tPos + 1);
866 | } else if (Services.prefs.getBoolPref("browser.tabs.insertRelatedAfterCurrent")) {
867 | let newTabPos = (aWindow.gBrowser._lastRelatedTab || aWindow.gBrowser.selectedTab)._tPos + 1;
868 | if (aWindow.gBrowser._lastRelatedTab) {
869 | aWindow.gBrowser._lastRelatedTab.owner = null;
870 | } else {
871 | newtab.owner = aWindow.gBrowser.selectedTab;
872 | }
873 | aWindow.gBrowser.moveTabTo(newtab, newTabPos);
874 | aWindow.gBrowser._lastRelatedTab = newtab;
875 | }
876 | if (PrivateBrowsingUtils.isWindowPrivate(aWindow) ||
877 | aWindow.gBrowser.selectedTab.getAttribute("privateTab-isPrivate")) {
878 | return;
879 | }
880 | let places = [{
881 | uri: Services.io.newURI(aHref, null, null),
882 | title: aTitle,
883 | visits: [{
884 | transitionType: PlacesUtils.history.TRANSITION_LINK,
885 | visitDate: Date.now() * 1000
886 | }],
887 | }];
888 | PlacesUtils.asyncHistory.updatePlaces(places);
889 | });
890 | },
891 |
892 | contextNewTab: function(aWindow, aEvent) {
893 | let gContextMenu = aWindow.gContextMenu;
894 | if (Services.prefs.getBoolPref(LOAD_IN_BACKGROUND)) {
895 | aWindow.urlSecurityCheck(gContextMenu.linkURL, aEvent.target.ownerDocument.nodePrincipal);
896 | this.openInBackground(aWindow, gContextMenu.linkURL,
897 | gContextMenu.link ? gContextMenu.link.textContent.trim() : "",
898 | aWindow.content.location.href);
899 | } else {
900 | gContextMenu.openLinkInTab(aEvent);
901 | }
902 | },
903 |
904 | hookOpenInBackground: function() {
905 | let openlinkintab = this.tabBrowser.ownerDocument.getElementById("context-openlinkintab");
906 | this.bgCommand = openlinkintab.getAttribute("oncommand");
907 | openlinkintab.setAttribute("oncommand", "gBrowser.LullTheTabs.contextNewTab(window, event);")
908 |
909 | let win = this.browserWindow;
910 | let openInBackground = this.openInBackground;
911 | win.original_handleLinkClick = win.handleLinkClick;
912 | win.handleLinkClick = function(event, href, linkNode){
913 | // Based on code from /browser/base/content/browser.js from Pale Moon 27.x
914 | if (event.button == 2) // right click
915 | return false;
916 |
917 | let doc = event.target.ownerDocument;
918 |
919 | let where = win.whereToOpenLink(event);
920 | if (where == "current") {
921 | // Respect Tab Mix Plus "protected" attribute
922 | if (win.gBrowser.selectedTab.hasAttribute("protected") &&
923 | href.split('#')[0] != doc.documentURIObject.specIgnoringRef) {
924 | where = "tab";
925 | } else {
926 | return false;
927 | }
928 | }
929 |
930 | if (where == "save") {
931 | win.saveURL(href, linkNode ? win.gatherTextUnder(linkNode) : "", null, true,
932 | true, doc.documentURIObject, doc);
933 | event.preventDefault();
934 | return true;
935 | }
936 |
937 | win.urlSecurityCheck(href, doc.nodePrincipal);
938 | if (where == "tab" && Services.prefs.getBoolPref(LOAD_IN_BACKGROUND)) {
939 | openInBackground(win, href, linkNode ? win.gatherTextUnder(linkNode).trim() : "", doc.documentURIObject.spec);
940 | } else {
941 | win.openLinkIn(href, where, { referrerURI: doc.documentURIObject, charset: doc.characterSet });
942 | }
943 | event.preventDefault();
944 | return true;
945 | };
946 | },
947 |
948 | unhookOpenInBackground: function() {
949 | this.tabBrowser.ownerDocument.getElementById("context-openlinkintab").setAttribute("oncommand", this.bgCommand);
950 | this.bgCommand = null;
951 |
952 | this.browserWindow.handleLinkClick = this.browserWindow.original_handleLinkClick;
953 | delete this.browserWindow.original_handleLinkClick;
954 | },
955 | };
956 |
957 | let globalPrefsWatcher = {
958 | observe: function(aSubject, aTopic, aData) {
959 | if (aTopic != "nsPref:changed" || aData != "exceptionList") return;
960 |
961 | let exceptionList = Services.prefs.getBranch(branch).getComplexValue("exceptionList", Ci.nsISupportsString).data;
962 | if (exceptionList == "") {
963 | Services.prefs.getBranch(branch).clearUserPref("exceptionList");
964 | }
965 | domRegex = null;
966 | },
967 | register: function() {
968 | this.prefBranch = Services.prefs.getBranch(branch);
969 | this.prefBranch.addObserver("", this, false);
970 | },
971 | unregister: function() {
972 | this.prefBranch.removeObserver("", this);
973 | this.prefBranch = null;
974 | }
975 | }
976 |
977 | function BrowserWindowObserver(aHandlers) {
978 | this.handlers = aHandlers;
979 | }
980 |
981 | BrowserWindowObserver.prototype = {
982 | observe: function(aSubject, aTopic, aData) {
983 | if (aTopic == "domwindowopened") {
984 | aSubject.QueryInterface(Ci.nsIDOMWindow).addEventListener("load", this, false);
985 | } else if (aTopic == "domwindowclosed") {
986 | if (aSubject.document.documentElement.getAttribute("windowtype") == "navigator:browser") {
987 | this.handlers.onShutdown(aSubject);
988 | }
989 | }
990 | },
991 | handleEvent: function(aEvent) {
992 | let aWindow = aEvent.currentTarget;
993 | aWindow.removeEventListener(aEvent.type, this, false);
994 |
995 | if (aWindow.document.documentElement.getAttribute("windowtype") == "navigator:browser") {
996 | this.handlers.onStartup(aWindow);
997 | }
998 | }
999 | };
1000 |
1001 | function browserWindowStartup(aWindow) {
1002 | aWindow.gBrowser.LullTheTabs = new LullTheTabs(aWindow);
1003 | }
1004 |
1005 | function browserWindowShutdown(aWindow) {
1006 | aWindow.gBrowser.LullTheTabs.done();
1007 | delete aWindow.gBrowser.LullTheTabs;
1008 | }
1009 |
1010 | function startup(aData, aReason) {
1011 | initPreferences();
1012 |
1013 | if (!styleSheetService.sheetRegistered(styleSheetURI, styleSheetService.USER_SHEET)) {
1014 | styleSheetService.loadAndRegisterSheet(styleSheetURI, styleSheetService.USER_SHEET);
1015 | }
1016 |
1017 | globalPrefsWatcher.register();
1018 |
1019 | gWindowListener = new BrowserWindowObserver({
1020 | onStartup: browserWindowStartup,
1021 | onShutdown: browserWindowShutdown
1022 | });
1023 | Services.ww.registerNotification(gWindowListener);
1024 |
1025 | let winenu = Services.wm.getEnumerator("navigator:browser");
1026 | while (winenu.hasMoreElements()) {
1027 | browserWindowStartup(winenu.getNext());
1028 | }
1029 | }
1030 |
1031 | function shutdown(aData, aReason) {
1032 | if (aReason == APP_SHUTDOWN) return;
1033 |
1034 | Services.ww.unregisterNotification(gWindowListener);
1035 | gWindowListener = null;
1036 |
1037 | let winenu = Services.wm.getEnumerator("navigator:browser");
1038 | while (winenu.hasMoreElements()) {
1039 | browserWindowShutdown(winenu.getNext());
1040 | }
1041 |
1042 | globalPrefsWatcher.unregister();
1043 |
1044 | if (styleSheetService.sheetRegistered(styleSheetURI, styleSheetService.USER_SHEET)) {
1045 | styleSheetService.unregisterSheet(styleSheetURI, styleSheetService.USER_SHEET);
1046 | }
1047 | }
1048 |
1049 | function install(aData, aReason) {}
1050 | function uninstall(aData, aReason) {}
1051 |
--------------------------------------------------------------------------------
/chrome.manifest:
--------------------------------------------------------------------------------
1 | skin lull-the-tabs classic/1.0 skin/
2 |
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustOff/lull-the-tabs/736f5ad63267e47a3aeae9ff77d48e2bd25cf6de/icon.png
--------------------------------------------------------------------------------
/install.rdf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | lull-the-tabs@Off.JustOff
5 | 1.5.2
6 | 2
7 | true
8 | Lull The Tabs
9 | Unload inactive tabs to free up browser memory
10 | Off JustOff <Off.Just.Off@gmail.com>
11 | Philipp von Weitershausen
12 | Szabolcs Hubai
13 | https://github.com/JustOff/lull-the-tabs/
14 | https://raw.githubusercontent.com/JustOff/lull-the-tabs/master/update.xml
15 |
16 |
17 | {8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}
18 | 27.0.0
19 | 29.*
20 |
21 |
22 |
23 |
24 | {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
25 | 45.0
26 | 56.*
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/options.xul:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | And don't load tabs until selected on browser start
5 |
6 |
7 |
8 |
9 |
10 |
11 | And don't load them on browser start
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Applies when the option above is activated
23 |
24 |
25 |
26 |
27 |
28 |
29 | Use ; as a delimiter for multiple domains
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/release.cmd:
--------------------------------------------------------------------------------
1 | @echo off
2 | set VER=1.5.2
3 |
4 | sed -i -E "s/version>.+?%VER%" install.rdf
5 | sed -i -E "s/version>.+?%VER%; s/download\/.+?\/lull-the-tabs-.+?\.xpi/download\/%VER%\/lull-the-tabs-%VER%\.xpi/" update.xml
6 |
7 | set XPI=lull-the-tabs-%VER%.xpi
8 | if exist %XPI% del %XPI%
9 | zip -r9q %XPI% * -x .git/* .gitignore update.xml LICENSE README.md *.cmd *.xpi *.exe
10 |
--------------------------------------------------------------------------------
/skin/style.css:
--------------------------------------------------------------------------------
1 | @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
2 | @-moz-document url("chrome://browser/content/browser.xul") {
3 | .tabbrowser-tab[pending=true],
4 | menuitem.alltabs-item[pending=true] {
5 | opacity: .5;
6 | }
7 |
8 | #lull-the-tabs-button {
9 | list-style-image: url("chrome://lull-the-tabs/skin/urlbar-icon.png");
10 | -moz-image-region: rect(0px 16px 16px 0px);
11 | }
12 |
13 | #lull-the-tabs-button:not([protected]):hover {
14 | -moz-image-region: rect(0px 32px 16px 16px);
15 | }
16 |
17 | #lull-the-tabs-button[protected] {
18 | -moz-image-region: rect(0px 48px 16px 32px);
19 | }
20 |
21 | #lull-the-tabs-button[protected]:hover {
22 | -moz-image-region: rect(0px 64px 16px 48px);
23 | }
24 |
25 | .tabbrowser-tab[bgpending] .tab-label {
26 | text-decoration: underline wavy;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/skin/urlbar-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustOff/lull-the-tabs/736f5ad63267e47a3aeae9ff77d48e2bd25cf6de/skin/urlbar-icon.png
--------------------------------------------------------------------------------
/update.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 1.5.2
9 |
10 |
11 | {8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}
12 | 27.0
13 | 29.*
14 | https://github.com/JustOff/lull-the-tabs/releases/download/1.5.2/lull-the-tabs-1.5.2.xpi
15 |
16 |
17 |
18 |
19 | {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
20 | 45.0
21 | 56.*
22 | https://github.com/JustOff/lull-the-tabs/releases/download/1.5.2/lull-the-tabs-1.5.2.xpi
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------