├── .gitmodules
├── Makefile
├── README.md
├── bootstrap.js
├── chrome.manifest
├── components
└── loader.js
├── content
├── config.xul
└── content-utils.js
├── history.en.md
├── history.ja.md
├── install.rdf
├── locale
├── en-US
│ ├── label.dtd
│ └── label.properties
└── ja
│ ├── label.dtd
│ └── label.properties
├── make.bat
├── make.sh
└── modules
├── const.js
├── defaults.js
├── lib
├── WindowManager.js
├── here.js
├── locale.js
└── prefs.js
├── main.js
├── suspendtab-internal.js
└── suspendtab.js
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "makexpi"]
2 | path = makexpi
3 | url = https://github.com/piroor/makexpi.git
4 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PACKAGE_NAME = suspendtab
2 |
3 | .PHONY: all xpi signed clean
4 |
5 | all: xpi
6 |
7 | xpi: makexpi/makexpi.sh
8 | makexpi/makexpi.sh -n $(PACKAGE_NAME) -o
9 |
10 | makexpi/makexpi.sh:
11 | git submodule update --init
12 |
13 | signed: xpi
14 | makexpi/sign_xpi.sh -k $(JWT_KEY) -s $(JWT_SECRET) -p ./$(PACKAGE_NAME)_noupdate.xpi
15 |
16 | clean:
17 | rm $(PACKAGE_NAME).xpi $(PACKAGE_NAME)_noupdate.xpi sha1hash.txt
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # suspendtab
2 | Suspends background old tabs automatically to save memory usage, for Firefox older than its version 57.
3 |
4 | This project is obsolete and not maintained anymore.
5 |
--------------------------------------------------------------------------------
/bootstrap.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileOverview Bootstrap code for restartless addons
3 | * @author YUKI "Piro" Hiroshi
4 | * @version 3
5 | *
6 | * @description
7 | * This provides ability to load a script file placed to "modules/main.js".
8 | * Functions named "shutdown", defined in main.js and any loaded script
9 | * will be called when the addon is disabled or uninstalled (include
10 | * updating).
11 | *
12 | * @license
13 | * The MIT License, Copyright (c) 2010-2012 YUKI "Piro" Hiroshi.
14 | * https://github.com/piroor/restartless/blob/master/license.txt
15 | * @url http://github.com/piroor/restartless
16 | */
17 |
18 | var _gLoader;
19 | var _gResourceRegistered = false;
20 |
21 | function _load(aScriptName, aId, aRoot, aReason)
22 | {
23 | const IOService = Components.classes['@mozilla.org/network/io-service;1']
24 | .getService(Components.interfaces.nsIIOService);
25 |
26 | var resource, loader, script;
27 | if (aRoot.isDirectory()) {
28 | resource = IOService.newFileURI(aRoot);
29 |
30 | loader = aRoot.clone();
31 | loader.append('components');
32 | loader.append('loader.js');
33 | loader = IOService.newFileURI(loader).spec;
34 |
35 | script = aRoot.clone();
36 | script.append('modules');
37 | script.append(aScriptName+'.js');
38 | script = IOService.newFileURI(script).spec;
39 | }
40 | else {
41 | let base = 'jar:'+IOService.newFileURI(aRoot).spec+'!/';
42 | loader = base + 'components/loader.js';
43 | script = base + 'modules/'+aScriptName+'.js';
44 | resource = IOService.newURI(base, null, null);
45 | }
46 |
47 | if (!_gLoader) {
48 | _gLoader = {};
49 | Components.classes['@mozilla.org/moz/jssubscript-loader;1']
50 | .getService(Components.interfaces.mozIJSSubScriptLoader)
51 | .loadSubScript(loader, _gLoader);
52 | }
53 |
54 | if (!_gLoader.exists('modules/'+aScriptName+'.js', resource.spec))
55 | return;
56 |
57 | if (!_gResourceRegistered) {
58 | _gLoader.registerResource(aId.split('@')[0]+'-resources', resource);
59 | _gResourceRegistered = true;
60 | }
61 | _gLoader.load(script);
62 | }
63 |
64 | function _reasonToString(aReason)
65 | {
66 | switch (aReason)
67 | {
68 | case APP_STARTUP: return 'APP_STARTUP';
69 | case APP_SHUTDOWN: return 'APP_SHUTDOWN';
70 | case ADDON_ENABLE: return 'ADDON_ENABLE';
71 | case ADDON_DISABLE: return 'ADDON_DISABLE';
72 | case ADDON_INSTALL: return 'ADDON_INSTALL';
73 | case ADDON_UNINSTALL: return 'ADDON_UNINSTALL';
74 | case ADDON_UPGRADE: return 'ADDON_UPGRADE';
75 | case ADDON_DOWNGRADE: return 'ADDON_DOWNGRADE';
76 | }
77 | return aReason;
78 | }
79 |
80 | function _free()
81 | {
82 | _gLoader =
83 | _load =
84 | _reasonToString =
85 | _free = _gResourceRegistered =
86 | install =
87 | uninstall =
88 | startup =
89 | shoutdown =
90 | undefined;
91 | }
92 |
93 | /**
94 | * handlers for bootstrap
95 | */
96 |
97 | function install(aData, aReason)
98 | {
99 | _load('install', aData.id, aData.installPath, _reasonToString(aReason));
100 | _gLoader.install(_reasonToString(aReason));
101 | }
102 |
103 | function startup(aData, aReason)
104 | {
105 | _load('main', aData.id, aData.installPath, _reasonToString(aReason));
106 | }
107 |
108 | function shutdown(aData, aReason)
109 | {
110 | if (!_gLoader) return;
111 | if (_gResourceRegistered) {
112 | _gLoader.unregisterResource(aData.id.split('@')[0]+'-resources');
113 | }
114 | _gLoader.shutdown(_reasonToString(aReason));
115 | _free();
116 | }
117 |
118 | function uninstall(aData, aReason)
119 | {
120 | if (!_gLoader) {
121 | _load('install', aData.id, aData.installPath, _reasonToString(aReason));
122 | }
123 | _gLoader.uninstall(_reasonToString(aReason));
124 | if (_gResourceRegistered) {
125 | _gLoader.unregisterResource(aData.id.split('@')[0]+'-resources');
126 | }
127 | _free();
128 | }
129 |
--------------------------------------------------------------------------------
/chrome.manifest:
--------------------------------------------------------------------------------
1 | content suspendtab content/
2 | locale suspendtab en-US locale/en-US/
3 | locale suspendtab ja locale/ja/
4 |
--------------------------------------------------------------------------------
/components/loader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileOverview Loader module for restartless addons
3 | * @author YUKI "Piro" Hiroshi
4 | * @contributor Infocatcher
5 | * @version 16
6 | *
7 | * @license
8 | * The MIT License, Copyright (c) 2010-2015 YUKI "Piro" Hiroshi.
9 | * https://github.com/piroor/restartless/blob/master/license.txt
10 | * @url http://github.com/piroor/restartless
11 | */
12 |
13 | function toPropertyDescriptors(aProperties) {
14 | var descriptors = {};
15 | Object.keys(aProperties).forEach(function(aProperty) {
16 | var description = Object.getOwnPropertyDescriptor(aProperties, aProperty);
17 | descriptors[aProperty] = description;
18 | });
19 | return descriptors;
20 | }
21 |
22 | function inherit(aParent, aExtraProperties) {
23 | var global;
24 | if (Components.utils.getGlobalForObject)
25 | global = Components.utils.getGlobalForObject(aParent);
26 | else
27 | global = aParent.valueOf.call();
28 | global = global || this;
29 |
30 | var ObjectClass = global.Object || Object;
31 | if (aExtraProperties)
32 | return ObjectClass.create(aParent, toPropertyDescriptors(aExtraProperties));
33 | else
34 | return ObjectClass.create(aParent);
35 | }
36 |
37 | // import base64 utilities from the js code module namespace
38 | try {
39 | var { atob, btoa } = Components.utils.import('resource://gre/modules/Services.jsm', {});
40 | } catch(e) {
41 | Components.utils.reportError(new Error('failed to load Services.jsm'));
42 | }
43 | try {
44 | var { console } = Components.utils.import('resource://gre/modules/devtools/Console.jsm', {});
45 | } catch(e) {
46 | Components.utils.reportError(new Error('failed to load Console.jsm'));
47 | }
48 |
49 | var { Promise } = Components.utils.import('resource://gre/modules/Promise.jsm', {});
50 |
51 | var _namespacePrototype = {
52 | Cc : Components.classes,
53 | Ci : Components.interfaces,
54 | Cu : Components.utils,
55 | Cr : Components.results,
56 | console : this.console,
57 | btoa : function(aInput) {
58 | return btoa(aInput);
59 | },
60 | atob : function(aInput) {
61 | return atob(aInput);
62 | },
63 | inherit : function(aParent, aExtraProperties) {
64 | return inherit(aParent, aExtraProperties);
65 | },
66 | Promise : Promise,
67 | };
68 | var _namespaces;
69 |
70 | /**
71 | * This functiom loads specified script into a unique namespace for the URL.
72 | * Namespaces for loaded scripts have a wrapped version of this function.
73 | * Both this and wrapped work like as Components.utils.import().
74 | * Due to the reserved symbol "import", we have to use another name "load"
75 | * instead it.
76 | *
77 | * @param {String} aScriptURL
78 | * URL of a script. Wrapped version of load() can handle related path.
79 | * Related path will be resolved based on the location of the caller script.
80 | * @param {Object=} aExportTargetForImport
81 | * EXPORTED_SYMBOLS in the loaded script will be exported to the object.
82 | * If no object is specified, symbols will be exported to the global object
83 | * of the caller.
84 | * @param {Object=} aExportTargetForRequire
85 | * Properties of "exports" in the loaded script will be exported to the object.
86 | *
87 | * @returns {Object}
88 | * The global object for the loaded script.
89 | */
90 | function load(aURISpec, aExportTargetForImport, aExportTargetForRequire, aRoot)
91 | {
92 | if (!_namespaces)
93 | _namespaces = {};
94 | var ns;
95 | if (aURISpec in _namespaces) {
96 | ns = _namespaces[aURISpec];
97 | _exportForImport(ns, aExportTargetForImport);
98 | _exportForRequire(ns, aExportTargetForRequire);
99 | return ns;
100 | }
101 | ns = _createNamespace(aURISpec, aRoot || aURISpec);
102 | try {
103 | Components.classes['@mozilla.org/moz/jssubscript-loader;1']
104 | .getService(Components.interfaces.mozIJSSubScriptLoader)
105 | .loadSubScript(aURISpec, ns);
106 | }
107 | catch(e) {
108 | let message = 'Loader::load('+aURISpec+') failed!\n'+e+'\n';
109 | dump(message);
110 | Components.utils.reportError(message + e.stack.replace(/( -> )/g, '\n$1'));
111 | throw e;
112 | }
113 | _exportForImport(ns, aExportTargetForImport);
114 | _exportForRequire(ns, aExportTargetForRequire);
115 | return _namespaces[aURISpec] = ns;
116 | }
117 |
118 | // JavaScript code module style
119 | function _exportForImport(aSource, aTarget)
120 | {
121 | if (
122 | !aTarget ||
123 | !('EXPORTED_SYMBOLS' in aSource) ||
124 | !aSource.EXPORTED_SYMBOLS ||
125 | !aSource.EXPORTED_SYMBOLS.forEach
126 | )
127 | return;
128 | for each (var symbol in aSource.EXPORTED_SYMBOLS)
129 | {
130 | aTarget[symbol] = aSource[symbol];
131 | }
132 | }
133 |
134 | // CommonJS style
135 | function _exportForRequire(aSource, aTarget)
136 | {
137 | if (
138 | !aTarget ||
139 | !('exports' in aSource) ||
140 | !aSource.exports ||
141 | typeof aSource.exports != 'object'
142 | )
143 | return;
144 | for (var symbol in aSource.exports)
145 | {
146 | aTarget[symbol] = aSource.exports[symbol];
147 | }
148 | }
149 |
150 | var IOService = Components.classes['@mozilla.org/network/io-service;1']
151 | .getService(Components.interfaces.nsIIOService);
152 | var FileHandler = IOService.getProtocolHandler('file')
153 | .QueryInterface(Components.interfaces.nsIFileProtocolHandler);
154 |
155 | /**
156 | * Checks existence of the file specified by the given relative path and the base URI.
157 | *
158 | * @param {String} aPath
159 | * A relative path to a file or directory, from the aBaseURI.
160 | * @param {String} aBaseURI
161 | * An absolute URI (with scheme) for relative paths.
162 | *
163 | * @returns {String}
164 | * If the file (or directory) exists, returns the absolute URI. Otherwise null.
165 | */
166 | function exists(aPath, aBaseURI)
167 | {
168 | if (/^\w+:/.test(aPath)) {
169 | let leafName = aPath.match(/([^\/]+)$/);
170 | leafName = leafName ? leafName[1] : '' ;
171 | aBaseURI = aPath.replace(/(?:[^\/]+)$/, '');
172 | aPath = leafName;
173 | }
174 | var baseURI = aBaseURI.indexOf('file:') == 0 ?
175 | IOService.newFileURI(FileHandler.getFileFromURLSpec(aBaseURI)) :
176 | IOService.newURI(aBaseURI, null, null);
177 | if (aBaseURI.indexOf('jar:') == 0) {
178 | baseURI = baseURI.QueryInterface(Components.interfaces.nsIJARURI);
179 | var reader = Components.classes['@mozilla.org/libjar/zip-reader;1']
180 | .createInstance(Components.interfaces.nsIZipReader);
181 | reader.open(baseURI.JARFile.QueryInterface(Components.interfaces.nsIFileURL).file);
182 | try {
183 | let baseEntry = baseURI.JAREntry.replace(/[^\/]+$/, '');
184 | let entries = reader.findEntries(baseEntry + aPath + '$');
185 | let found = entries.hasMore();
186 | return found ? baseURI.resolve(aPath) : null ;
187 | }
188 | finally {
189 | reader.close();
190 | }
191 | }
192 | else {
193 | let resolved = baseURI.resolve(aPath);
194 | return FileHandler.getFileFromURLSpec(resolved).exists() ? resolved : null ;
195 | }
196 | }
197 |
198 | function _readFrom(aURISpec, aEncoding)
199 | {
200 | const Cc = Components.classes;
201 | const Ci = Components.interfaces;
202 |
203 | var uri = aURISpec.indexOf('file:') == 0 ?
204 | IOService.newFileURI(FileHandler.getFileFromURLSpec(aURISpec)) :
205 | IOService.newURI(aURISpec, null, null) ;
206 | var channel = IOService.newChannelFromURI(uri.QueryInterface(Ci.nsIURI));
207 | var stream = channel.open();
208 |
209 | var fileContents = null;
210 | try {
211 | if (aEncoding) {
212 | var converterStream = Cc['@mozilla.org/intl/converter-input-stream;1']
213 | .createInstance(Ci.nsIConverterInputStream);
214 | var buffer = stream.available();
215 | converterStream.init(stream, aEncoding, buffer,
216 | converterStream.DEFAULT_REPLACEMENT_CHARACTER);
217 | var out = { value : null };
218 | converterStream.readString(stream.available(), out);
219 | converterStream.close();
220 | fileContents = out.value;
221 | }
222 | else {
223 | var scriptableStream = Cc['@mozilla.org/scriptableinputstream;1']
224 | .createInstance(Ci.nsIScriptableInputStream);
225 | scriptableStream.init(stream);
226 | fileContents = scriptableStream.read(scriptableStream.available());
227 | scriptableStream.close();
228 | }
229 | }
230 | finally {
231 | stream.close();
232 | }
233 | return fileContents;
234 | }
235 |
236 | function _createNamespace(aURISpec, aRoot)
237 | {
238 | var baseURI = aURISpec.indexOf('file:') == 0 ?
239 | IOService.newFileURI(FileHandler.getFileFromURLSpec(aURISpec)) :
240 | IOService.newURI(aURISpec, null, null);
241 | var rootURI = typeof aRoot == 'string' ?
242 | (aRoot.indexOf('file:') == 0 ?
243 | IOService.newFileURI(FileHandler.getFileFromURLSpec(aRoot)) :
244 | IOService.newURI(aRoot, null, null)
245 | ) :
246 | aRoot ;
247 | var ns = inherit(_namespacePrototype, {
248 | location : _createFakeLocation(baseURI),
249 | exists : function(aPath, aBase) {
250 | return exists(aPath, aBase || baseURI.spec);
251 | },
252 | exist : function(aPath, aBase) { // alias
253 | return exists(aPath, aBase || baseURI.spec);
254 | },
255 | /** JavaScript code module style */
256 | load : function(aURISpec, aExportTarget) {
257 | if (!/\.jsm?$/.test(aURISpec)) {
258 | if (exists(aURISpec+'.js', baseURI.spec))
259 | aURISpec += '.js'
260 | else if (exists(aURISpec+'.jsm', baseURI.spec))
261 | aURISpec += '.jsm'
262 | }
263 | var resolved = baseURI.resolve(aURISpec);
264 | if (resolved == aURISpec)
265 | throw new Error('Recursive load!');
266 | return load(resolved, aExportTarget || ns, aExportTarget, rootURI);
267 | },
268 | 'import' : function() { // alias
269 | return this.load.apply(this, arguments);
270 | },
271 | /**
272 | * CommonJS style
273 | * @url http://www.commonjs.org/specs/
274 | */
275 | require : function(aURISpec) {
276 | if (!/\.jsm?$/.test(aURISpec)) {
277 | if (exists(aURISpec+'.js', baseURI.spec))
278 | aURISpec += '.js'
279 | else if (exists(aURISpec+'.jsm', baseURI.spec))
280 | aURISpec += '.jsm'
281 | }
282 | var resolved = (aURISpec.charAt(0) == '.' ? rootURI : baseURI ).resolve(aURISpec);
283 | if (resolved == aURISpec)
284 | throw new Error('Recursive load!');
285 | var exported = {};
286 | load(resolved, exported, exported, rootURI);
287 | return exported;
288 | },
289 | /* utility to resolve relative path from the file */
290 | resolve : function(aURISpec, aBaseURI) {
291 | var base = !aBaseURI ?
292 | baseURI :
293 | aBaseURI.indexOf('file:') == 0 ?
294 | IOService.newFileURI(FileHandler.getFileFromURLSpec(aURISpec)) :
295 | IOService.newURI(aURISpec, null, null) ;
296 | return base.resolve(aURISpec);
297 | },
298 | /* utility to read contents of a text file */
299 | read : function(aURISpec, aEncoding, aBaseURI) {
300 | return _readFrom(this.resolve(aURISpec, aBaseURI), aEncoding);
301 | },
302 | exports : {}
303 | });
304 | return ns;
305 | }
306 |
307 | function _createFakeLocation(aURI)
308 | {
309 | aURI = aURI.QueryInterface(Components.interfaces.nsIURL)
310 | .QueryInterface(Components.interfaces.nsIURI);
311 | return {
312 | href : aURI.spec,
313 | search : aURI.query ? '?'+aURI.query : '' ,
314 | hash : aURI.ref ? '#'+aURI.ref : '' ,
315 | host : aURI.scheme == 'jar' ? '' : aURI.hostPort,
316 | hostname : aURI.scheme == 'jar' ? '' : aURI.host,
317 | port : aURI.scheme == 'jar' ? -1 : aURI.port,
318 | pathname : aURI.path,
319 | protocol : aURI.scheme+':',
320 | reload : function() {},
321 | replace : function() {},
322 | toString : function() {
323 | return this.href;
324 | }
325 | };
326 | }
327 |
328 | function _callHandler(aHandler, aReason)
329 | {
330 | var handlers = [];
331 | for (var i in _namespaces)
332 | {
333 | if (_namespaces[i][aHandler] &&
334 | typeof _namespaces[i][aHandler] == 'function')
335 | handlers.push({
336 | key : i,
337 | namespace : _namespaces[i],
338 | handler : _namespaces[i][aHandler]
339 | });
340 | }
341 |
342 | return new Promise(function(aResolve, aReject) {
343 | var processHandler = function() {
344 | var handler = handlers.shift();
345 | if (!handler)
346 | return aResolve();
347 |
348 | try {
349 | var result = handler.handler.call(handler.namespace, aReason);
350 | }
351 | catch(e) {
352 | let message = i+'('+aHandler+', '+aReason+')\n'+e+'\n';
353 | dump(message);
354 | Components.utils.reportError(message + e.stack.replace(/( -> )/g, '\n$1'));
355 | }
356 |
357 | if (result && typeof result.then == 'function') {
358 | result.then(processHandler);
359 | }
360 | else {
361 | processHandler();
362 | }
363 | };
364 | processHandler();
365 | });
366 | }
367 |
368 | function registerResource(aName, aRoot)
369 | {
370 | IOService.getProtocolHandler('resource')
371 | .QueryInterface(Components.interfaces.nsIResProtocolHandler)
372 | .setSubstitution(aName, aRoot);
373 | }
374 |
375 | function unregisterResource(aName)
376 | {
377 | IOService.getProtocolHandler('resource')
378 | .QueryInterface(Components.interfaces.nsIResProtocolHandler)
379 | .setSubstitution(aName, null);
380 | }
381 |
382 | /** Handler for "install" of the bootstrap.js */
383 | function install(aReason)
384 | {
385 | _callHandler('install', aReason);
386 | }
387 |
388 | /** Handler for "uninstall" of the bootstrap.js */
389 | function uninstall(aReason)
390 | {
391 | _callHandler('uninstall', aReason);
392 | }
393 |
394 | /** Handler for "shutdown" of the bootstrap.js */
395 | function shutdown(aReason)
396 | {
397 | _callHandler('shutdown', aReason)
398 | .then(function() {
399 | for each (let ns in _namespaces)
400 | {
401 | for (let i in ns.exports)
402 | {
403 | if (ns.exports.hasOwnProperty(i))
404 | delete ns.exports[i];
405 | }
406 | }
407 | _namespaces = void(0);
408 | _namespacePrototype = void(0);
409 |
410 | IOService = void(0);
411 | FileHandler = void(0);
412 | Promise = void(0);
413 |
414 | load = void(0);
415 | _exportSymbols = void(0);
416 | exists = void(0);
417 | _createNamespace = void(0);
418 | _callHandler = void(0);
419 | registerResource = void(0);
420 | unregisterResource = void(0);
421 | install = void(0);
422 | uninstall = void(0);
423 | shutdown = void(0);
424 | });
425 | }
426 |
--------------------------------------------------------------------------------
/content/config.xul:
--------------------------------------------------------------------------------
1 |
2 |
36 |
37 |
39 | %mainDTD;
40 | ]>
41 |
44 |
45 |
47 |
48 |
51 |
54 |
57 |
60 |
63 |
66 |
69 |
72 |
73 |
74 |
76 |
77 |
78 |
80 |
86 |
88 |
97 |
99 |
100 |
101 |
102 |
104 |
105 |
106 |
108 |
114 |
116 |
117 |
119 |
120 |
121 |
123 |
124 |
125 |
126 |
128 |
129 |
132 |
133 |
134 |
135 |
147 |
149 |
151 |
153 |
154 |
155 |
156 |
158 |
159 |
162 |
163 |
164 |
165 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
244 |
245 |
--------------------------------------------------------------------------------
/content/content-utils.js:
--------------------------------------------------------------------------------
1 | (function(global) {
2 | var Cc = Components.classes;
3 | var Ci = Components.interfaces;
4 | var Cu = Components.utils;
5 | var Cr = Components.results;
6 |
7 | var { Services } = Cu.import('resource://gre/modules/Services.jsm', {});
8 |
9 | var MESSAGE_TYPE = 'suspendtab@piro.sakura.ne.jp';
10 |
11 | function free()
12 | {
13 | free =
14 | Cc = Ci = Cu = Cr =
15 | Services =
16 | MESSAGE_TYPE =
17 | suspend =
18 | onContentLoaded =
19 | handleMessage =
20 | undefined;
21 | }
22 |
23 | function suspend(aParams)
24 | {
25 | aParams = aParams || {};
26 |
27 | var webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
28 | var SHistory = webNavigation.sessionHistory;
29 |
30 | global.addEventListener('load', function onLoad() {
31 | global.removeEventListener('load', onLoad, true);
32 |
33 | var uri = Services.io.newURI(aParams.uri || 'about:blank', null, null);
34 | docShell.setCurrentURI(uri);
35 | content.document.title = aParams.label || '';
36 |
37 | // Don't purge all histories - leave the last one!
38 | // The SS module stores the title of the history entry
39 | // as the title of the restored tab.
40 | // If there is no history entry, Firefox will restore
41 | // the tab with the default title (the URI of the page).
42 | if (SHistory.count > 1)
43 | SHistory.PurgeHistory(SHistory.count - 1);
44 |
45 | if (aParams.debug)
46 | content.alert('PURGED');
47 |
48 | global.sendAsyncMessage(MESSAGE_TYPE, {
49 | command : 'suspended',
50 | params : aParams
51 | });
52 | }, true);
53 |
54 | if (aParams.debug)
55 | content.alert('MAKE BLANK');
56 |
57 | // Load a blank page to clear out the current history entries.
58 | content.location.href = 'about:blank';
59 | }
60 |
61 | function onContentLoaded()
62 | {
63 | global.sendAsyncMessage(MESSAGE_TYPE, {
64 | command : 'loaded'
65 | });
66 | }
67 | global.addEventListener('load', onContentLoaded, true);
68 | global.addEventListener('DOMTitleChanged', onContentLoaded, true);
69 |
70 | function handleMessage(aMessage)
71 | {
72 | switch (aMessage.json.command)
73 | {
74 | case 'suspend':
75 | if (aMessage.json.params.debug)
76 | content.alert('SUSPEND');
77 | suspend(aMessage.json.params);
78 | return;
79 |
80 | case 'shutdown':
81 | global.removeMessageListener(MESSAGE_TYPE, handleMessage);
82 | global.removeEventListener('load', onContentLoaded, true);
83 | global.removeEventListener('DOMTitleChanged', onContentLoaded, true);
84 | free();
85 | return;
86 | }
87 | }
88 | global.addMessageListener(MESSAGE_TYPE, handleMessage);
89 |
90 | global.sendAsyncMessage(MESSAGE_TYPE, {
91 | command : 'initialized'
92 | });
93 | })(this);
94 |
--------------------------------------------------------------------------------
/history.en.md:
--------------------------------------------------------------------------------
1 | # History
2 |
3 | - master/HEAD
4 | - 1.0.2016061501
5 | * Don't select pending tab after suspending of the current tab, if possible.
6 | * Respect preferences for visibility of "Suspend Other Items" menu items.
7 | - 1.0.2016020401
8 | * Works on Nightly 47.0a1.
9 | * Supports e10s.
10 | * Add "Don't suspend this site automatically" to the tab context menu.
11 | * Detect host name of web pages more correctly.
12 | * Visibility options for menu items work correctly.
13 | * Next focused tab after the current tab is suspended is now configurable.
14 | * Newly opened background tabs can be suspended by default.
15 | * Features are available from the context menu on the contents area.
16 | * Provide ability to limit the number of on-memory tabs.
17 | - 0.2.2014050201
18 | * Works on Firefox 29 and Nightly 32.0a1.
19 | * Drop support for Firefox 25, 26, 27 and 28.
20 | - 0.1.2013111801
21 | * Works on Firefox 25 and later.
22 | * Fixed: Background tabs are suspended correctly after a while, even if there is no exception. (by YosukeM, thanks!)
23 | - 0.1.2013053101
24 | * Modified: Just to pass through AMO Editor's review, make codes free from "evalInSandbox()" and E4X. They were still there only for backward compatibilities so they never caused errors/security issues on lately Firefox, however, editors persecutive rejected those codes, then I've given up and removed them.
25 | - 0.1.2013052901
26 | * Modified: Some codes depending on "evalInSandbox()" are just removed. AMO Editors always banned new releases, because an included library had codes with "evalInSandbox()" for backward compatibility - even if it is NEVER called on this addon.
27 | * Modified: Update codes around [session store API](http://dutherenverseauborddelatable.wordpress.com/2013/05/23/add-on-breakage-continued-list-of-add-ons-that-will-probably-be-affected/).
28 | - 0.1.2013040601
29 | * Fixed: Restore suspended tab automatically when it is reloaded.
30 | - 0.1.2012122901
31 | * Released.
32 |
--------------------------------------------------------------------------------
/history.ja.md:
--------------------------------------------------------------------------------
1 | # 更新履歴
2 |
3 | - master/HEAD
4 | - 1.0.2016061501
5 | * 現在のタブをサスペンドした後、可能な限りサスペンドされていないタブにフォーカスを移すようにした
6 | * 「他のタブをサスペンド」を非表示にする設定が反映されていなかったのを修正
7 | - 1.0.2016020401
8 | * Nightly 47.0a1に対応
9 | * e10s時にも動作するようにした
10 | * タブのコンテキストメニューに「このサイトは自動的にサスペンドしない」を追加
11 | * 自動的にサスペンドする対象のドメイン名をより正しく認識するようにした
12 | * 設定ダイアログで、メニュー項目の表示・非表示を制御するチェックボックスが機能していなかったのを修正
13 | * 現在のタブをサスペンドした後にフォーカスするタブを制御できるようにした
14 | * 新しくバックグラウンドで開いたタブをすぐにサスペンドできるようにした
15 | * コンテンツ領域のコンテキストメニューからも機能を呼べるようにした
16 | * メモリ上に読み込んでおけるタブの最大数を制限できるようにした
17 | - 0.2.2014050201
18 | * Firefox 29とNightly 32.0a1に対応
19 | * Firefox 25から28までのサポートを終了
20 | - 0.1.2013111801
21 | * Firefox 25に対応
22 | * 除外リストが空の時に、一定時間でバックグラウンドのタブを休止する機能が働いていなかったのを修正(by YosukeM, thanks!)
23 | - 0.1.2013053101
24 | * Mozilla Add-onsのEditorによるレビューで、後方互換性のためにライブラリ内に含めてあった・このアドオンでは問題を起こし得ないコードが原因でレビューを蹴られる状況だったため、ライブラリを削除した
25 | - 0.1.2013052901
26 | * Mozilla Add-onsのEditorによるレビューで、後方互換性のためにライブラリ内に含めてあった・このアドオンでは到達し得ないコードが原因でレビューを蹴られる状況だったため、ライブラリから当該コードを削除した
27 | * [セッション保存APIの仕様変更](http://dutherenverseauborddelatable.wordpress.com/2013/05/23/add-on-breakage-continued-list-of-add-ons-that-will-probably-be-affected/)に追従
28 | - 0.1.2013040601
29 | * タブの再読み込み時に、休止されていたタブを自動的に復元するようにした
30 | - 0.1.2012122901
31 | * 公開
32 |
--------------------------------------------------------------------------------
/install.rdf:
--------------------------------------------------------------------------------
1 |
2 |
5 |
15 |
16 |
17 | http://piro.sakura.ne.jp/xul/update.rdf
18 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDLI43lgUhWE0E7bfiuTrQMKIP53KM3w71VEPc9FxE7YS3Hpr85dC/GgEUDPd6xD7UP7ckzEusN49LCAKoipepxgyAWzQjZ3CzvkD7PWsZFTi7lpj9ytYDysoFwHs0L96i5oiVopNj1NQM2AC0Y5Lr0b6lz2L5ece0/z6monpZXPQIDAQAB
19 |
20 |
21 |
22 |
26 | YosukeM
27 | vzvu3k6k
28 |
29 |
30 |
31 |
35 | YosukeM
36 | vzvu3k6k
37 |
38 |
39 |
40 |
41 |
42 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/locale/en-US/label.dtd:
--------------------------------------------------------------------------------
1 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/locale/en-US/label.properties:
--------------------------------------------------------------------------------
1 | # ***** BEGIN LICENSE BLOCK *****
2 | # Version: MPL 1.1/GPL 2.0/LGPL 2.1
3 | #
4 | # The contents of these files are subject to the Mozilla Public License Version
5 | # 1.1 (the "License"); you may not use these files except in compliance with
6 | # the License. You may obtain a copy of the License at
7 | # http://www.mozilla.org/MPL/
8 | #
9 | # Software distributed under the License is distributed on an "AS IS" basis,
10 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 | # for the specific language governing rights and limitations under the
12 | # License.
13 | #
14 | # The Original Code is the Suspend Tab.
15 | #
16 | # The Initial Developer of the Original Code is YUKI "Piro" Hiroshi.
17 | # Portions created by the Initial Developer are Copyright (C) 2012-2014
18 | # the Initial Developer. All Rights Reserved.
19 | #
20 | # Contributor(s): YUKI "Piro" Hiroshi
21 | #
22 | # Alternatively, the contents of these files may be used under the terms of
23 | # either the GNU General Public License Version 2 or later (the "GPL"), or
24 | # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
25 | # in which case the provisions of the GPL or the LGPL are applicable instead
26 | # of those above. If you wish to allow use of your version of these files only
27 | # under the terms of either the GPL or the LGPL, and not to allow others to
28 | # use your version of these files under the terms of the MPL, indicate your
29 | # decision by deleting the provisions above and replace them with the notice
30 | # and other provisions required by the LGPL or the GPL. If you do not delete
31 | # the provisions above, a recipient may use your version of these files under
32 | # the terms of any one of the MPL, the GPL or the LGPL.
33 | #
34 | # ***** END LICENSE BLOCK *****
35 |
36 | tab.suspend.label=Suspend Tab
37 | tab.suspend.accesskey=s
38 | tab.resume.label=Resume Tab
39 | tab.resume.accesskey=r
40 | tab.exception.add.label=Don't suspend this site automatically
41 | tab.exception.add.accesskey=d
42 | tab.suspendOthers.label=Suspend Other Tabs
43 | tab.suspendOthers.accesskey=o
44 |
45 | tab.suspendTree.label=Suspend Tree
46 | tab.suspendTree.accesskey=s
47 | tab.resumeTree.label=Resume Tree
48 | tab.resumeTree.accesskey=r
49 |
50 | toBeSuspended.tooltip=%1$S (to be suspended at %2$S)
51 |
--------------------------------------------------------------------------------
/locale/ja/label.dtd:
--------------------------------------------------------------------------------
1 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/locale/ja/label.properties:
--------------------------------------------------------------------------------
1 | # ***** BEGIN LICENSE BLOCK *****
2 | # Version: MPL 1.1/GPL 2.0/LGPL 2.1
3 | #
4 | # The contents of these files are subject to the Mozilla Public License Version
5 | # 1.1 (the "License"); you may not use these files except in compliance with
6 | # the License. You may obtain a copy of the License at
7 | # http://www.mozilla.org/MPL/
8 | #
9 | # Software distributed under the License is distributed on an "AS IS" basis,
10 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 | # for the specific language governing rights and limitations under the
12 | # License.
13 | #
14 | # The Original Code is the Suspend Tab.
15 | #
16 | # The Initial Developer of the Original Code is YUKI "Piro" Hiroshi.
17 | # Portions created by the Initial Developer are Copyright (C) 2012-2014
18 | # the Initial Developer. All Rights Reserved.
19 | #
20 | # Contributor(s): YUKI "Piro" Hiroshi
21 | #
22 | # Alternatively, the contents of these files may be used under the terms of
23 | # either the GNU General Public License Version 2 or later (the "GPL"), or
24 | # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
25 | # in which case the provisions of the GPL or the LGPL are applicable instead
26 | # of those above. If you wish to allow use of your version of these files only
27 | # under the terms of either the GPL or the LGPL, and not to allow others to
28 | # use your version of these files under the terms of the MPL, indicate your
29 | # decision by deleting the provisions above and replace them with the notice
30 | # and other provisions required by the LGPL or the GPL. If you do not delete
31 | # the provisions above, a recipient may use your version of these files under
32 | # the terms of any one of the MPL, the GPL or the LGPL.
33 | #
34 | # ***** END LICENSE BLOCK *****
35 |
36 | tab.suspend.label=このタブをサスペンドする
37 | tab.suspend.accesskey=s
38 | tab.resume.label=このタブを復帰させる
39 | tab.resume.accesskey=r
40 | tab.resume.label=このタブを復帰させる
41 | tab.exception.add.label=このサイトは自動的にサスペンドしない
42 | tab.exception.add.accesskey=d
43 | tab.suspendOthers.label=他のタブをすべてサスペンドする
44 | tab.suspendOthers.accesskey=o
45 |
46 | tab.suspendTree.label=このツリーをサスペンドする
47 | tab.suspendTree.accesskey=s
48 | tab.resumeTree.label=このツリーを復帰させる
49 | tab.resumeTree.accesskey=r
50 |
51 | toBeSuspended.tooltip=%1$S(%2$S にサスペンドされます)
52 |
--------------------------------------------------------------------------------
/make.bat:
--------------------------------------------------------------------------------
1 | copy makexpi\makexpi.sh .\
2 | bash makexpi.sh -n suspendtab -o
3 | del makexpi.sh
4 |
--------------------------------------------------------------------------------
/make.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | appname=suspendtab
4 |
5 | cp makexpi/makexpi.sh ./
6 | ./makexpi.sh -n $appname -o
7 | rm ./makexpi.sh
8 |
9 |
--------------------------------------------------------------------------------
/modules/const.js:
--------------------------------------------------------------------------------
1 | /* ***** BEGIN LICENSE BLOCK *****
2 | * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3 | *
4 | * The contents of this file are subject to the Mozilla Public License Version
5 | * 1.1 (the "License"); you may not use this file except in compliance with
6 | * the License. You may obtain a copy of the License at
7 | * http://www.mozilla.org/MPL/
8 | *
9 | * Software distributed under the License is distributed on an "AS IS" basis,
10 | * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 | * for the specific language governing rights and limitations under the
12 | * License.
13 | *
14 | * The Original Code is Suspend Tab.
15 | *
16 | * The Initial Developer of the Original Code is YUKI "Piro" Hiroshi.
17 | * Portions created by the Initial Developer are Copyright (C) 2012-2016
18 | * the Initial Developer. All Rights Reserved.
19 | *
20 | * Contributor(s):: YUKI "Piro" Hiroshi
21 | *
22 | * Alternatively, the contents of this file may be used under the terms of
23 | * either the GNU General Public License Version 2 or later (the "GPL"), or
24 | * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
25 | * in which case the provisions of the GPL or the LGPL are applicable instead
26 | * of those above. If you wish to allow use of your version of this file only
27 | * under the terms of either the GPL or the LGPL, and not to allow others to
28 | * use your version of this file under the terms of the MPL, indicate your
29 | * decision by deleting the provisions above and replace them with the notice
30 | * and other provisions required by the GPL or the LGPL. If you do not delete
31 | * the provisions above, a recipient may use your version of this file under
32 | * the terms of any one of the MPL, the GPL or the LGPL.
33 | *
34 | * ***** END LICENSE BLOCK ***** */
35 |
36 | var exports = {
37 | domain : 'extensions.suspendtab@piro.sakura.ne.jp.',
38 |
39 | STATE : 'suspendtab-state',
40 | OPTIONS : 'suspendtab-options',
41 | INDEX : 'suspendtab-current-index',
42 | SUSPENDING : 'suspendtab-suspending',
43 | SUSPENDED : 'suspendtab-suspended',
44 | MENUITEM_AVAILABLE : 'suspendtab-available',
45 | MENUITEM_ENABLED : 'suspendtab-enabled',
46 |
47 | EVENT_TYPE_SUSPENDING : 'TabSuspending',
48 | EVENT_TYPE_SUSPENDED : 'TabSuspended',
49 | EVENT_TYPE_RESUMING : 'TabResuming',
50 | EVENT_TYPE_RESUMED : 'TabResumed',
51 | EVENT_TYPE_TAB_LOADED : 'SuspendTabContentLoaded',
52 |
53 | NEXT_FOCUS_AUTO : -1,
54 | NEXT_FOCUS_PRECEDING : 0,
55 | NEXT_FOCUS_FOLLOWING : 1,
56 | NEXT_FOCUS_FIRST : 2,
57 | NEXT_FOCUS_LAST : 3,
58 | NEXT_FOCUS_PREVIOUSLY_FOCUSED : 4
59 | };
60 |
--------------------------------------------------------------------------------
/modules/defaults.js:
--------------------------------------------------------------------------------
1 | /* ***** BEGIN LICENSE BLOCK *****
2 | * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3 | *
4 | * The contents of this file are subject to the Mozilla Public License Version
5 | * 1.1 (the "License"); you may not use this file except in compliance with
6 | * the License. You may obtain a copy of the License at
7 | * http://www.mozilla.org/MPL/
8 | *
9 | * Software distributed under the License is distributed on an "AS IS" basis,
10 | * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 | * for the specific language governing rights and limitations under the
12 | * License.
13 | *
14 | * The Original Code is Suspend Tab.
15 | *
16 | * The Initial Developer of the Original Code is YUKI "Piro" Hiroshi.
17 | * Portions created by the Initial Developer are Copyright (C) 2012-2016
18 | * the Initial Developer. All Rights Reserved.
19 | *
20 | * Contributor(s):: YUKI "Piro" Hiroshi
21 | *
22 | * Alternatively, the contents of this file may be used under the terms of
23 | * either the GNU General Public License Version 2 or later (the "GPL"), or
24 | * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
25 | * in which case the provisions of the GPL or the LGPL are applicable instead
26 | * of those above. If you wish to allow use of your version of this file only
27 | * under the terms of either the GPL or the LGPL, and not to allow others to
28 | * use your version of this file under the terms of the MPL, indicate your
29 | * decision by deleting the provisions above and replace them with the notice
30 | * and other provisions required by the GPL or the LGPL. If you do not delete
31 | * the provisions above, a recipient may use your version of this file under
32 | * the terms of any one of the MPL, the GPL or the LGPL.
33 | *
34 | * ***** END LICENSE BLOCK ***** */
35 |
36 | var prefs = require('lib/prefs').prefs;
37 |
38 | var SuspendTabConst = require('const');
39 | var domain = SuspendTabConst.domain;
40 |
41 | prefs.setDefaultPref(domain+'autoSuspend.enabled', true);
42 | prefs.setDefaultPref(domain+'autoSuspend.timeout', 30);
43 | prefs.setDefaultPref(domain+'autoSuspend.timeout.factor', 1000 * 60);
44 | prefs.setDefaultPref(domain+'autoSuspend.tooManyTabs', false);
45 | prefs.setDefaultPref(domain+'autoSuspend.tooManyTabs.maxTabsOnMemory', 10);
46 | prefs.setDefaultPref(domain+'autoSuspend.blockList', '');
47 | prefs.setDefaultPref(domain+'autoSuspend.resetOnReload', true);
48 | prefs.setDefaultPref(domain+'autoSuspend.newBackgroundTab', false);
49 | prefs.setDefaultPref(domain+'autoSuspend.newBackgroundTab.afterLoad', false);
50 | prefs.setDefaultPref(domain+'autoSuspend.nextFocus', SuspendTabConst.NEXT_FOCUS_AUTO);
51 | prefs.setDefaultPref(domain+'menu.context_toggleTabSuspended', true);
52 | prefs.setDefaultPref(domain+'menu.context_toggleTabSuspendException', true);
53 | prefs.setDefaultPref(domain+'menu.context_suspendOthers', true);
54 | prefs.setDefaultPref(domain+'menu.context_suspendTree', true);
55 | prefs.setDefaultPref(domain+'menu.context_resumeTree', true);
56 | prefs.setDefaultPref(domain+'menu.contentContext_suspend', true);
57 | prefs.setDefaultPref(domain+'menu.contentContext_toggleTabSuspendException', true);
58 | prefs.setDefaultPref(domain+'menu.contentContext_suspendOthers', true);
59 | prefs.setDefaultPref(domain+'menu.contentContext_suspendTree', true);
60 | prefs.setDefaultPref(domain+'menu.contentContext_resumeTree', true);
61 | prefs.setDefaultPref(domain+'debug', false);
62 | prefs.setDefaultPref(domain+'debug.content', false);
63 |
--------------------------------------------------------------------------------
/modules/lib/WindowManager.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileOverview Window manager module for restartless addons
3 | * @author YUKI "Piro" Hiroshi
4 | * @version 5
5 | *
6 | * @license
7 | * The MIT License, Copyright (c) 2010-2014 YUKI "Piro" Hiroshi.
8 | * https://github.com/piroor/restartless/blob/master/license.txt
9 | * @url http://github.com/piroor/restartless
10 | */
11 |
12 | var EXPORTED_SYMBOLS = ['WindowManager'];
13 |
14 | var _WindowWatcher = Cc['@mozilla.org/embedcomp/window-watcher;1']
15 | .getService(Ci.nsIWindowWatcher);
16 | var _WindowMediator = Cc['@mozilla.org/appshell/window-mediator;1']
17 | .getService(Ci.nsIWindowMediator);
18 |
19 | var _gListener = {
20 | observe : function(aSubject, aTopic, aData)
21 | {
22 | if (
23 | aTopic == 'domwindowopened' &&
24 | !aSubject
25 | .QueryInterface(Ci.nsIInterfaceRequestor)
26 | .getInterface(Ci.nsIWebNavigation)
27 | .QueryInterface(Ci.nsIDocShell)
28 | .QueryInterface(Ci.nsIDocShellTreeNode || Ci.nsIDocShellTreeItem) // nsIDocShellTreeNode is merged to nsIDocShellTreeItem by https://bugzilla.mozilla.org/show_bug.cgi?id=331376
29 | .QueryInterface(Ci.nsIDocShellTreeItem)
30 | .parent
31 | )
32 | aSubject
33 | .QueryInterface(Ci.nsIDOMWindow)
34 | .addEventListener('DOMContentLoaded', this, false);
35 | },
36 | handleEvent : function(aEvent)
37 | {
38 | aEvent.currentTarget.removeEventListener(aEvent.type, this, false);
39 |
40 | var window = aEvent.target.defaultView;
41 | this.listeners.forEach(function(aListener) {
42 | try {
43 | if (aListener.handleEvent &&
44 | typeof aListener.handleEvent == 'function')
45 | aListener.handleEvent(aEvent);
46 | if (aListener.handleWindow &&
47 | typeof aListener.handleWindow == 'function')
48 | aListener.handleWindow(window);
49 | if (typeof aListener == 'function')
50 | aListener(window);
51 | }
52 | catch(e) {
53 | dump(e+'\n');
54 | }
55 | });
56 | },
57 | listeners : []
58 | };
59 | _WindowWatcher.registerNotification(_gListener);
60 |
61 | /**
62 | * @class
63 | * Provides features to get existing chrome windows, etc.
64 | */
65 | var WindowManager = {
66 | /**
67 | * Registers a handler for newly opened chrome windows. Handlers will
68 | * be called when DOMContentLoaded events are fired in newly opened
69 | * windows.
70 | *
71 | * @param {Object} aHandler
72 | * A handler for new windows. If you specify a function, it will be
73 | * called with the DOMWindow object as the first argument. If the
74 | * specified object has a method named "handleWindow", then the
75 | * method will be called with the DOMWindow. If the object has a
76 | * method named "handleEvent", then it will be called with the
77 | * DOMContentLoaded event object (not DOMWindow object.)
78 | */
79 | addHandler : function(aListener)
80 | {
81 | if (!_gListener) return;
82 | if (
83 | aListener &&
84 | (
85 | typeof aListener == 'function' ||
86 | (aListener.handleWindow && typeof aListener.handleWindow == 'function') ||
87 | (aListener.handleEvent && typeof aListener.handleEvent == 'function')
88 | ) &&
89 | _gListener.listeners.indexOf(aListener) < 0
90 | )
91 | _gListener.listeners.push(aListener);
92 | },
93 | /**
94 | * Unregisters a handler.
95 | */
96 | removeHandler : function(aListener)
97 | {
98 | if (!_gListener) return;
99 | let index = _gListener.listeners.indexOf(aListener);
100 | if (index > -1)
101 | _gListener.listeners.splice(index, 1);
102 | },
103 | /**
104 | * Returns the most recent chrome window (DOMWindow).
105 | *
106 | * @param {string=} aWindowType
107 | * The window type you want to get, ex. "navigator:browser". If you
108 | * specify no type (null, blank string, etc.) then this returns
109 | * the most recent window of any type.
110 | *
111 | * @returns {nsIDOMWindow}
112 | * A found DOMWindow.
113 | */
114 | getWindow : function(aType)
115 | {
116 | return _WindowMediator.getMostRecentWindow(aType || null);
117 | },
118 | /**
119 | * Returns an array of chrome windows (DOMWindow).
120 | *
121 | * @param {string=} aWindowType
122 | * The window type you want to filter, ex. "navigator:browser". If
123 | * you specify no type (null, blank string, etc.) then this returns
124 | * an array of all chrome windows.
125 | *
126 | * @returns {Array}
127 | * An array of found DOMWindows.
128 | */
129 | getWindows : function(aType)
130 | {
131 | var array = [];
132 | var windows = _WindowMediator.getZOrderDOMWindowEnumerator(aType || null, true);
133 |
134 | // By the bug 156333, we cannot find windows by their Z order on Linux.
135 | // https://bugzilla.mozilla.org/show_bug.cgi?id=156333
136 | if (!windows.hasMoreElements())
137 | windows = _WindowMediator.getEnumerator(aType || null);
138 |
139 | while (windows.hasMoreElements())
140 | {
141 | array.push(windows.getNext().QueryInterface(Ci.nsIDOMWindow));
142 | }
143 | return array;
144 | }
145 | };
146 | for (let i in WindowManager)
147 | {
148 | exports[i] = (function(aSymbol) {
149 | return function() {
150 | return WindowManager[aSymbol].apply(WindowManager, arguments);
151 | };
152 | })(i);
153 | }
154 |
155 | /** A handler for bootstrap.js */
156 | function shutdown()
157 | {
158 | _WindowWatcher.unregisterNotification(_gListener);
159 | _WindowWatcher = void(0);
160 | _gListener.listeners = [];
161 | }
162 |
--------------------------------------------------------------------------------
/modules/lib/here.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileOverview Here-document module for restartless addons
3 | * @author YUKI "Piro" Hiroshi
4 | * @version 3
5 | * @description Inspired from https://github.com/cho45/node-here.js
6 | *
7 | * @license
8 | * The MIT License, Copyright (c) 2012 YUKI "Piro" Hiroshi.
9 | * https://github.com/piroor/restartless/blob/master/license.txt
10 | * @url http://github.com/piroor/restartless
11 | */
12 |
13 | var EXPORTED_SYMBOLS = ['here'];
14 |
15 | var cache = {};
16 |
17 | function here() {
18 | var caller = Components.stack.caller;
19 | var filename = caller.filename.split(' -> ').slice(-1)[0];
20 | var line = caller.lineNumber-1;
21 | var key = filename + ':' + line;
22 | if (key in cache) return cache[key];
23 |
24 | var source = read(filename);
25 | var part = source.split(/\r?\n/).slice(line).join('\n');
26 | part = part.replace(/.*\bhere\([^\/]*\/\*/, '');
27 | part = part.split('*/')[0];
28 | cache[key] = part;
29 | return part;
30 | }
31 |
32 | function shutdown() {
33 | cache = undefined;
34 | }
35 |
36 | if (typeof read == 'undefined') {
37 | var Cc = Components.classes;
38 | var Ci = Components.interfaces;
39 | var IOService = Cc['@mozilla.org/network/io-service;1'].getService(Ci.nsIIOService);
40 |
41 | read = function read(aURI) {
42 | var uri = IOService.newURI(aURI, null, null);
43 | var channel = IOService.newChannelFromURI(uri);
44 | var stream = channel.open();
45 |
46 | var fileContents = null;
47 | try {
48 | var scriptableStream = Cc['@mozilla.org/scriptableinputstream;1']
49 | .createInstance(Ci.nsIScriptableInputStream);
50 | scriptableStream.init(stream);
51 | fileContents = scriptableStream.read(scriptableStream.available());
52 | scriptableStream.close();
53 | }
54 | finally {
55 | stream.close();
56 | }
57 |
58 | return fileContents;
59 | };
60 | }
61 |
--------------------------------------------------------------------------------
/modules/lib/locale.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileOverview Locale module for restartless addons
3 | * @author YUKI "Piro" Hiroshi
4 | * @version 7
5 | *
6 | * @license
7 | * The MIT License, Copyright (c) 2010-2013 YUKI "Piro" Hiroshi.
8 | * https://github.com/piroor/restartless/blob/master/license.txt
9 | * @url http://github.com/piroor/restartless
10 | */
11 |
12 | var EXPORTED_SYMBOLS = ['locale'];
13 |
14 | var DEFAULT_LOCALE = 'en-US';
15 |
16 | var gCache = {}
17 | var get = function(aPath, aBaseURI) {
18 | if (/^\w+:/.test(aPath))
19 | aBaseURI = aPath;
20 |
21 | var uri = aPath;
22 | if (!/^chrome:\/\/[^\/]+\/locale\//.test(uri)) {
23 | let locale = DEFAULT_LOCALE;
24 | try {
25 | let prefs = Cc['@mozilla.org/preferences;1'].getService(Ci.nsIPrefBranch);
26 | locale = prefs.getCharPref('general.useragent.locale');
27 | if (/\w+:/.test(locale))
28 | locale = prefs.getComplexValue('general.useragent.locale', Ci.nsIPrefLocalizedString).data;
29 | locale = locale || DEFAULT_LOCALE;
30 | }
31 | catch(e) {
32 | dump(e+'\n');
33 | }
34 | [
35 | aPath+'.'+locale,
36 | aPath+'.'+(locale.split('-')[0]),
37 | aPath+'.'+DEFAULT_LOCALE,
38 | aPath+'.'+(DEFAULT_LOCALE.split('-')[0])
39 | ].some(function(aURI) {
40 | let resolved = exists(aURI, aBaseURI);
41 | if (resolved) {
42 | uri = resolved;
43 | return true;
44 | }
45 | return false;
46 | });
47 | }
48 |
49 | if (!(uri in gCache)) {
50 | gCache[uri] = new StringBundle(uri);
51 | }
52 | return gCache[uri];
53 | };
54 | exports.get = get;
55 |
56 | var locale = { 'get' : get };
57 |
58 | const Service = Cc['@mozilla.org/intl/stringbundle;1']
59 | .getService(Ci.nsIStringBundleService);
60 |
61 | function StringBundle(aURI)
62 | {
63 | this._bundle = Service.createBundle(aURI);
64 | }
65 | StringBundle.prototype = {
66 | getString : function(aKey) {
67 | try {
68 | return this._bundle.GetStringFromName(aKey);
69 | }
70 | catch(e) {
71 | Cu.reportError(new Error('locale.js: failed to call GetStringFromName() with: ' + aKey + '\n' + e));
72 | }
73 | return '';
74 | },
75 | getFormattedString : function(aKey, aArray) {
76 | try {
77 | return this._bundle.formatStringFromName(aKey, aArray, aArray.length);
78 | }
79 | catch(e) {
80 | Cu.reportError(new Error('locale.js: failed to call formatStringFromName() with: ' + JSON.stringify({ key: aKey, args: aArray }) + '\n' + e));
81 | Cu.reportError(e);
82 | }
83 | return '';
84 | },
85 | get strings() {
86 | return this._bundle.getSimpleEnumeration();
87 | }
88 | };
89 |
90 | /** A handler for bootstrap.js */
91 | function shutdown()
92 | {
93 | gCache = {};
94 | Service.flushBundles();
95 | }
96 |
--------------------------------------------------------------------------------
/modules/lib/prefs.js:
--------------------------------------------------------------------------------
1 | /*
2 | Preferences Library
3 |
4 | Usage:
5 | var value = window['piro.sakura.ne.jp'].prefs.getPref('my.extension.pref');
6 | window['piro.sakura.ne.jp'].prefs.setPref('my.extension.pref', true);
7 | window['piro.sakura.ne.jp'].prefs.clearPref('my.extension.pref');
8 | var listener = {
9 | domains : [
10 | 'browser.tabs',
11 | 'extensions.someextension'
12 | ],
13 | observe : function(aSubject, aTopic, aData)
14 | {
15 | if (aTopic != 'nsPref:changed') return;
16 | var value = window['piro.sakura.ne.jp'].prefs.getPref(aData);
17 | }
18 | };
19 | window['piro.sakura.ne.jp'].prefs.addPrefListener(listener);
20 | window['piro.sakura.ne.jp'].prefs.removePrefListener(listener);
21 |
22 | // utility
23 | var store = window['piro.sakura.ne.jp'].prefs.createStore('extensions.someextension.');
24 | // property name/key, default value
25 | store.define('enabled', true);
26 | // property name, default value, pref key (different to the name)
27 | store.define('leftMargin', true, 'margin.left');
28 | var enabled = store.enabled;
29 | store.destroy(); // free the memory.
30 |
31 | license: The MIT License, Copyright (c) 2009-2013 YUKI "Piro" Hiroshi
32 | original:
33 | http://github.com/piroor/fxaddonlib-prefs
34 | */
35 |
36 | /* To work as a JS Code Module */
37 | if (typeof window == 'undefined' ||
38 | (window && typeof window.constructor == 'function')) {
39 | this.EXPORTED_SYMBOLS = ['prefs'];
40 |
41 | // If namespace.jsm is available, export symbols to the shared namespace.
42 | // See: http://github.com/piroor/fxaddonlibs/blob/master/namespace.jsm
43 | try {
44 | let ns = {};
45 | Components.utils.import('resource://my-modules/namespace.jsm', ns);
46 | /* var */ window = ns.getNamespaceFor('piro.sakura.ne.jp');
47 | }
48 | catch(e) {
49 | window = {};
50 | }
51 | }
52 |
53 | (function() {
54 | const currentRevision = 16;
55 |
56 | if (!('piro.sakura.ne.jp' in window)) window['piro.sakura.ne.jp'] = {};
57 |
58 | var loadedRevision = 'prefs' in window['piro.sakura.ne.jp'] ?
59 | window['piro.sakura.ne.jp'].prefs.revision :
60 | 0 ;
61 | if (loadedRevision && loadedRevision > currentRevision) {
62 | return;
63 | }
64 |
65 | const Cc = Components.classes;
66 | const Ci = Components.interfaces;
67 |
68 | window['piro.sakura.ne.jp'].prefs = {
69 | revision : currentRevision,
70 |
71 | Prefs : Cc['@mozilla.org/preferences;1']
72 | .getService(Ci.nsIPrefBranch)
73 | .QueryInterface(Ci.nsIPrefBranch2),
74 |
75 | DefaultPrefs : Cc['@mozilla.org/preferences-service;1']
76 | .getService(Ci.nsIPrefService)
77 | .getDefaultBranch(null),
78 |
79 | getPref : function(aPrefstring, aInterface, aBranch)
80 | {
81 | if (!aInterface || aInterface instanceof Ci.nsIPrefBranch)
82 | [aBranch, aInterface] = [aInterface, aBranch];
83 |
84 | aBranch = aBranch || this.Prefs;
85 |
86 | var type = aBranch.getPrefType(aPrefstring);
87 | if (type == aBranch.PREF_INVALID)
88 | return null;
89 |
90 | if (aInterface)
91 | return aBranch.getComplexValue(aPrefstring, aInterface);
92 |
93 | try {
94 | switch (type)
95 | {
96 | case aBranch.PREF_STRING:
97 | return decodeURIComponent(escape(aBranch.getCharPref(aPrefstring)));
98 |
99 | case aBranch.PREF_INT:
100 | return aBranch.getIntPref(aPrefstring);
101 |
102 | case aBranch.PREF_BOOL:
103 | return aBranch.getBoolPref(aPrefstring);
104 |
105 | case aBranch.PREF_INVALID:
106 | default:
107 | return null;
108 | }
109 | } catch(e) {
110 | // getXXXPref can raise an error if it is the default branch.
111 | return null;
112 | }
113 | },
114 |
115 | getLocalizedPref : function(aPrefstring)
116 | {
117 | try {
118 | return this.getPref(aPrefstring, Ci.nsIPrefLocalizedString).data;
119 | } catch(e) {
120 | return this.getPref(aPrefstring);
121 | }
122 | },
123 |
124 | getDefaultPref : function(aPrefstring, aInterface)
125 | {
126 | return this.getPref(aPrefstring, this.DefaultPrefs, aInterface);
127 | },
128 |
129 | setPref : function(aPrefstring, aNewValue)
130 | {
131 | var branch = this.Prefs;
132 | var interface = null;
133 | if (arguments.length > 2) {
134 | for (let i = 2; i < arguments.length; i++)
135 | {
136 | let arg = arguments[i];
137 | if (!arg)
138 | continue;
139 | if (arg instanceof Ci.nsIPrefBranch)
140 | branch = arg;
141 | else
142 | interface = arg;
143 | }
144 | }
145 | if (interface &&
146 | aNewValue instanceof Ci.nsISupports) {
147 | return branch.setComplexValue(aPrefstring, interface, aNewValue);
148 | }
149 | switch (typeof aNewValue)
150 | {
151 | case 'string':
152 | return branch.setCharPref(aPrefstring, unescape(encodeURIComponent(aNewValue)));
153 |
154 | case 'number':
155 | return branch.setIntPref(aPrefstring, parseInt(aNewValue));
156 |
157 | default:
158 | return branch.setBoolPref(aPrefstring, !!aNewValue);
159 | }
160 | },
161 |
162 | setDefaultPref : function(aPrefstring, aNewValue)
163 | {
164 | return this.setPref(aPrefstring, aNewValue, this.DefaultPrefs);
165 | },
166 |
167 | clearPref : function(aPrefstring)
168 | {
169 | if (this.Prefs.prefHasUserValue(aPrefstring))
170 | this.Prefs.clearUserPref(aPrefstring);
171 | },
172 |
173 | getDescendant : function(aRoot, aBranch)
174 | {
175 | aBranch = aBranch || this.Prefs;
176 | return aBranch.getChildList(aRoot, {}).sort();
177 | },
178 |
179 | getChildren : function(aRoot, aBranch)
180 | {
181 | aRoot = aRoot.replace(/\.$/, '');
182 | var foundChildren = {};
183 | var possibleChildren = [];
184 | this.getDescendant(aRoot, aBranch)
185 | .forEach(function(aPrefstring) {
186 | var name = aPrefstring.replace(aRoot + '.', '');
187 | let possibleChildKey = aRoot + '.' + name.split('.')[0];
188 | if (possibleChildKey && !(possibleChildKey in foundChildren)) {
189 | possibleChildren.push(possibleChildKey);
190 | foundChildren[possibleChildKey] = true;
191 | }
192 | });
193 | return possibleChildren.sort();
194 | },
195 |
196 | addPrefListener : function(aObserver)
197 | {
198 | var domains = ('domains' in aObserver) ? aObserver.domains : [aObserver.domain] ;
199 | try {
200 | for (var domain of domains)
201 | this.Prefs.addObserver(domain, aObserver, false);
202 | }
203 | catch(e) {
204 | }
205 | },
206 |
207 | removePrefListener : function(aObserver)
208 | {
209 | var domains = ('domains' in aObserver) ? aObserver.domains : [aObserver.domain] ;
210 | try {
211 | for (var domain of domains)
212 | this.Prefs.removeObserver(domain, aObserver, false);
213 | }
214 | catch(e) {
215 | }
216 | },
217 |
218 | createStore : function(aDomain)
219 | {
220 | var listener = {
221 | domain : aDomain,
222 | observe : function(aSubject, aTopic, aData) {
223 | if (aTopic != 'nsPref:changed')
224 | return;
225 | var name = keyToName[aData];
226 | store[name] = window['piro.sakura.ne.jp'].prefs.getPref(aData);
227 | }
228 | };
229 | this.addPrefListener(listener);
230 | var keyToName = {};
231 | var base = aDomain.replace(/\.$/, '') + '.';
232 | var store = {
233 | define : function(aName, aValue, aKey) {
234 | aKey = base + (aKey || aName);
235 | window['piro.sakura.ne.jp'].prefs.setDefaultPref(aKey, aValue);
236 | this[aName] = window['piro.sakura.ne.jp'].prefs.getPref(aKey);
237 | keyToName[aKey] = aName;
238 | },
239 | destroy : function() {
240 | window['piro.sakura.ne.jp'].prefs.removePrefListener(listener);
241 | aDomain = undefined;
242 | base = undefined;
243 | listener = undefined;
244 | keyToName = undefined;
245 | store = undefined;
246 | }
247 | };
248 | return store;
249 | }
250 | };
251 | })();
252 |
253 | if (window != this) { // work as a JS Code Module
254 | this.prefs = window['piro.sakura.ne.jp'].prefs;
255 | }
256 |
--------------------------------------------------------------------------------
/modules/main.js:
--------------------------------------------------------------------------------
1 | /* ***** BEGIN LICENSE BLOCK *****
2 | * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3 | *
4 | * The contents of this file are subject to the Mozilla Public License Version
5 | * 1.1 (the "License"); you may not use this file except in compliance with
6 | * the License. You may obtain a copy of the License at
7 | * http://www.mozilla.org/MPL/
8 | *
9 | * Software distributed under the License is distributed on an "AS IS" basis,
10 | * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 | * for the specific language governing rights and limitations under the
12 | * License.
13 | *
14 | * The Original Code is Suspend Tab.
15 | *
16 | * The Initial Developer of the Original Code is YUKI "Piro" Hiroshi.
17 | * Portions created by the Initial Developer are Copyright (C) 2012
18 | * the Initial Developer. All Rights Reserved.
19 | *
20 | * Contributor(s):: YUKI "Piro" Hiroshi
21 | *
22 | * Alternatively, the contents of this file may be used under the terms of
23 | * either the GNU General Public License Version 2 or later (the "GPL"), or
24 | * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
25 | * in which case the provisions of the GPL or the LGPL are applicable instead
26 | * of those above. If you wish to allow use of your version of this file only
27 | * under the terms of either the GPL or the LGPL, and not to allow others to
28 | * use your version of this file under the terms of the MPL, indicate your
29 | * decision by deleting the provisions above and replace them with the notice
30 | * and other provisions required by the GPL or the LGPL. If you do not delete
31 | * the provisions above, a recipient may use your version of this file under
32 | * the terms of any one of the MPL, the GPL or the LGPL.
33 | *
34 | * ***** END LICENSE BLOCK ***** */
35 |
36 | load('lib/WindowManager');
37 |
38 | load('defaults');
39 | load('suspendtab');
40 |
41 | const TYPE_BROWSER = 'navigator:browser';
42 |
43 | function handleWindow(aWindow)
44 | {
45 | aWindow.addEventListener('DOMContentLoaded', function() {
46 | aWindow.removeEventListener('DOMContentLoaded', arguments.callee, false);
47 | if (aWindow.document.documentElement.getAttribute('windowtype') == TYPE_BROWSER)
48 | aWindow.SuspendTab = new SuspendTab(aWindow);
49 | }, false);
50 | }
51 |
52 | WindowManager.getWindows(TYPE_BROWSER).forEach(function(aWindow) {
53 | aWindow.SuspendTab = new SuspendTab(aWindow);
54 | });
55 | WindowManager.addHandler(handleWindow);
56 |
57 | function shutdown()
58 | {
59 | WindowManager.getWindows(TYPE_BROWSER).forEach(function(aWindow) {
60 | if (aWindow.SuspendTab) {
61 | aWindow.SuspendTab.destroy();
62 | delete aWindow.SuspendTab;
63 | }
64 | });
65 |
66 | WindowManager = undefined;
67 | SuspendTab = undefined;
68 |
69 | shutdown = undefined;
70 | }
71 |
--------------------------------------------------------------------------------
/modules/suspendtab-internal.js:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */
4 |
5 | var EXPORTED_SYMBOLS = ['SuspendTabInternal'];
6 |
7 | load('lib/prefs');
8 | load('lib/here');
9 |
10 | var SS = Cc['@mozilla.org/browser/sessionstore;1']
11 | .getService(Ci.nsISessionStore);
12 |
13 | var { Services } = Cu.import('resource://gre/modules/Services.jsm', {});
14 | var { setTimeout, clearTimeout } = Cu.import('resource://gre/modules/Timer.jsm', {});
15 |
16 | var { SessionStoreInternal, TabRestoreQueue } = Cu.import('resource:///modules/sessionstore/SessionStore.jsm', {});
17 | var TAB_STATE_NEEDS_RESTORE = 1;
18 | var TAB_STATE_RESTORING = 2;
19 | var TAB_STATE_WILL_RESTORE = 3;
20 | //var { TabRestoreStates } = Cu.import('resource:///modules/sessionstore/SessionStore.jsm', {});
21 | var { TabState } = Cu.import('resource:///modules/sessionstore/TabState.jsm', {});
22 | var { TabStateCache } = Cu.import('resource:///modules/sessionstore/TabStateCache.jsm', {});
23 | try {
24 | var { TabStateFlusher } = Cu.import('resource:///modules/sessionstore/TabStateFlusher.jsm', {});
25 | }
26 | catch(e) {
27 | // for old Firefox
28 | var TabStateFlusher;
29 | }
30 |
31 | function isInternalAPIsAvailable() {
32 | if (!SessionStoreInternal) {
33 | Cu.reportError(new Error('suspendtab: Failed to load SessionStoreInternal'));
34 | return false;
35 | }
36 | if (!SessionStoreInternal.restoreTabContent) {
37 | Cu.reportError(new Error('suspendtab: SessionStoreInternal does not have restoreTabContent() method'));
38 | return false;
39 | }
40 | if (
41 | typeof SessionStoreInternal.startNextEpoch == 'undefined') {
42 | if (typeof SessionStoreInternal._nextRestoreEpoch == 'undefined') { // for old Firefox
43 | Cu.reportError(new Error('suspendtab: SessionStoreInternal does not have startNextEpoch or _nextRestoreEpoch'));
44 | return false;
45 | }
46 | if (typeof SessionStoreInternal._browserEpochs == 'undefined') {
47 | Cu.reportError(new Error('suspendtab: SessionStoreInternal does not have _browserEpochs'));
48 | return false;
49 | }
50 | }
51 |
52 | if (!TabState) {
53 | Cu.reportError(new Error('suspendtab: Failed to load TabState'));
54 | return false;
55 | }
56 | if (!TabStateFlusher || !TabStateFlusher.flush && !TabState.flush) {
57 | Cu.reportError(new Error('suspendtab: Missing both TabStateFlusher.flush() and TabState.flush()'));
58 | return false;
59 | }
60 | if (!TabState.clone) {
61 | Cu.reportError(new Error('suspendtab: TabState does not have clone() method'));
62 | return false;
63 | }
64 |
65 | if (!TabStateCache) {
66 | Cu.reportError(new Error('suspendtab: Failed to load TabStateCache'));
67 | return false;
68 | }
69 | if (!TabStateCache.update) {
70 | Cu.reportError(new Error('suspendtab: TabStateCache does not have update() method'));
71 | return false;
72 | }
73 |
74 | return true;
75 | }
76 |
77 | var fullStates = new WeakMap();
78 |
79 | function SuspendTabInternal(aWindow)
80 | {
81 | this.init(aWindow);
82 | }
83 | SuspendTabInternal.prototype = inherit(require('const'), {
84 | MESSAGE_TYPE: 'suspendtab@piro.sakura.ne.jp',
85 | SCRIPT_URL: 'chrome://suspendtab/content/content-utils.js',
86 |
87 | destroyed : false,
88 |
89 | get debug()
90 | {
91 | return prefs.getPref(this.domain + 'debug');
92 | },
93 |
94 | get document()
95 | {
96 | return this.window.document;
97 | },
98 | get browser()
99 | {
100 | return this.window.gBrowser;
101 | },
102 | get tabs()
103 | {
104 | return this.browser.mTabContainer.childNodes;
105 | },
106 |
107 | init : function(aWindow)
108 | {
109 | SuspendTabInternal.instances.push(this);
110 | this.window = aWindow;
111 |
112 | this.handleMessage = this.handleMessage.bind(this);
113 | this.window.messageManager.addMessageListener(this.MESSAGE_TYPE, this.handleMessage);
114 | this.window.messageManager.loadFrameScript(this.SCRIPT_URL, true);
115 | },
116 |
117 | destroy : function(aIsGoingToBeDisabled)
118 | {
119 | this.destroyed = true;
120 |
121 | this.window.messageManager.broadcastAsyncMessage(this.MESSAGE_TYPE, {
122 | command : 'shutdown'
123 | });
124 | this.window.messageManager.removeDelayedFrameScript(this.SCRIPT_URL);
125 | this.window.messageManager.removeMessageListener(this.MESSAGE_TYPE, this.handleMessage);
126 | this.handleMessage = undefined;
127 |
128 | delete this.window;
129 |
130 | if (SuspendTabInternal)
131 | SuspendTabInternal.instances.splice(SuspendTabInternal.instances.indexOf(this), 1);
132 | },
133 |
134 | isSuspended : function(aTab)
135 | {
136 | var browser = aTab.linkedBrowser;
137 | return browser && browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE;
138 | },
139 |
140 | isSuspending : function(aTab)
141 | {
142 | return aTab[this.SUSPENDING];
143 | },
144 |
145 | isSuspendable : function(aTab)
146 | {
147 | var browser = aTab.linkedBrowser;
148 | return (
149 | browser &&
150 | browser.__SS_restoreState != TAB_STATE_NEEDS_RESTORE &&
151 | (
152 | !SessionStoreInternal._windowBusyStates ||
153 | !SessionStoreInternal._windowBusyStates.get(browser.ownerDocument.defaultView)
154 | )
155 | );
156 | },
157 |
158 | suspend : function(aTab, aOptions)
159 | {
160 | return new Promise((function(aResolve, aReject) {
161 | var browser = aTab.linkedBrowser;
162 | if (browser.__SS_restoreState == TAB_STATE_RESTORING ||
163 | browser.__SS_restoreState == TAB_STATE_WILL_RESTORE) {
164 | var onRestored = (function() {
165 | aTab.removeEventListener('SSTabRestored', onRestored, false);
166 | this.suspend(aTab, aOptions)
167 | .then(aResolve);
168 | }).bind(this);
169 | aTab.addEventListener('SSTabRestored', onRestored, false);
170 | return;
171 | }
172 |
173 | aOptions = aOptions || {};
174 | if (this.isSuspended(aTab))
175 | return aResolve(true);
176 |
177 | {
178 | let event = this.document.createEvent('Events');
179 | event.initEvent(this.EVENT_TYPE_SUSPENDING, true, true);
180 | if (!aTab.dispatchEvent(event))
181 | return aResolve(false);
182 | }
183 |
184 | if (this.debug)
185 | dump(' suspend '+aTab._tPos+'\n');
186 |
187 | aTab[this.SUSPENDING] = true;
188 |
189 | if (TabStateFlusher) {
190 | return TabStateFlusher.flush(browser)
191 | .then((function() {
192 | return this.suspendPostProcess(aTab, aOptions);
193 | }).bind(this))
194 | .then(aResolve);
195 | }
196 | else {
197 | TabState.flush(browser);
198 | this.suspendPostProcess(aTab, aOptions);
199 | return aResolve(true);
200 | }
201 | }).bind(this));
202 | },
203 | suspendPostProcess : function(aTab, aOptions)
204 | {
205 | if (
206 | !aTab.parentNode || // already removed tab
207 | !TabState // service already destroyed
208 | )
209 | return;
210 |
211 | var label = aTab.label;
212 | var browser = aTab.linkedBrowser;
213 | var wasBusy = aTab.getAttribute('busy') == 'true';
214 |
215 | var state = TabState.clone(aTab);
216 | fullStates.set(aTab, state);
217 |
218 | var uri = browser.currentURI.clone();
219 | if (uri.spec == 'about:blank' && state.userTypedValue)
220 | uri = Services.io.newURI(state.userTypedValue, null, null);
221 |
222 | if (wasBusy)
223 | label = aOptions.label = uri.spec;
224 |
225 | // We only need minimum data required to restore the session history,
226 | // so drop needless information.
227 | var partialState = {
228 | entries : state.entries,
229 | storage : state.storage || null,
230 | index : state.index,
231 | pageStyle : state.pageStyle || null
232 | };
233 | SS.setTabValue(aTab, this.STATE, JSON.stringify(partialState));
234 | SS.setTabValue(aTab, this.OPTIONS, JSON.stringify(aOptions));
235 |
236 | aTab.linkedBrowser.messageManager.sendAsyncMessage(this.MESSAGE_TYPE, {
237 | command : 'suspend',
238 | params : {
239 | uri : uri.spec,
240 | label : label,
241 | icon : state.attributes.image || state.image,
242 | debug : prefs.getPref(this.domain + 'debug.content')
243 | }
244 | });
245 | },
246 | completeSuspend : function(aTab, aParams)
247 | {
248 | aParams = aParams || {};
249 |
250 | var label = aParams.label || '';
251 | var icon = aParams.icon || '';
252 |
253 | aTab.setAttribute('label', label);
254 | aTab.setAttribute('visibleLabel', label);
255 | if (this.debug)
256 | aTab.setAttribute('tooltiptext', label +' (suspended)');
257 |
258 | // Because Firefox sets the default favicon on this event loop,
259 | // we have to reset the favicon in the next loop.
260 | setTimeout((function() {
261 | if (!aTab.parentNode)
262 | return;
263 | if (this.debug)
264 | dump(' => set icon '+icon+'\n');
265 | this.browser.setIcon(aTab, icon, aTab.linkedBrowser.contentPrincipal);
266 | }).bind(this), 0);
267 |
268 | aTab.setAttribute('pending', true);
269 | aTab.setAttribute(this.SUSPENDED, true);
270 |
271 | this.readyToResume(aTab);
272 |
273 | delete aTab[this.SUSPENDING];
274 |
275 | {
276 | let event = this.document.createEvent('Events');
277 | event.initEvent(this.EVENT_TYPE_SUSPENDED, true, false);
278 | aTab.dispatchEvent(event);
279 | }
280 | },
281 |
282 | handleMessage : function(aMessage)
283 | {
284 | /*
285 | if (this.debug) {
286 | dump('*********************handleMessage*******************\n');
287 | dump('TARGET IS: '+aMessage.target.localName+'\n');
288 | dump(JSON.stringify(aMessage.json)+'\n');
289 | }
290 | */
291 |
292 | var tab;
293 | try {
294 | tab = this.browser.getTabForBrowser(aMessage.target);
295 | }
296 | catch(e) {
297 | dump(e + '\n');
298 | }
299 |
300 | if (!tab) {
301 | dump(' => message from non-tab target\n');
302 | return;
303 | }
304 |
305 | switch (aMessage.json.command)
306 | {
307 | case 'initialized':
308 | dump(' => tab '+tab._tPos+' initialized\n');
309 | return;
310 |
311 | case 'suspended':
312 | this.completeSuspend(tab, aMessage.json.params);
313 | return;
314 |
315 | case 'loaded':
316 | if (tab.getAttribute('pending') != 'true')
317 | tab.removeAttribute(this.SUSPENDED);
318 | if (!tab.selected && tab.__suspendtab__suspendAfterLoad) {
319 | setTimeout((function() {
320 | if (!tab.parentNode)
321 | return;
322 | delete tab.__suspendtab__suspendAfterLoad;
323 | if (tab.selected)
324 | return;
325 | this.suspend(tab);
326 | }).bind(this), 500);
327 | }
328 | let event = this.document.createEvent('Events');
329 | event.initEvent(this.EVENT_TYPE_TAB_LOADED, true, false);
330 | tab.dispatchEvent(event);
331 | return;
332 | }
333 | },
334 |
335 | resume : function(aTabs)
336 | {
337 | if (aTabs instanceof this.window.Element)
338 | aTabs = [aTabs];
339 |
340 | return Promise.all(aTabs.map(function(aTab) {
341 | return this.resumeOne(aTab);
342 | }, this));
343 | },
344 |
345 | resumeOne : function(aTab, aIdMap, aDocIdentMap)
346 | {
347 | if (this.isSuspending(aTab)) {
348 | return new Promise((function(aResolve, aReject) {
349 | var onSuspended = (function(aEvent) {
350 | aTab.removeEventListener(aEvent.type, onSuspended, false);
351 | this.resumeOne(aTab, aIdMap, aDocIdentMap)
352 | .then(aResolve);
353 | }).bind(this);
354 | aTab.addEventListener(this.EVENT_TYPE_SUSPENDED, onSuspended, false);
355 | }).bind(this));
356 | }
357 |
358 | if (!this.isSuspended(aTab))
359 | return Promise.resolve(true);
360 |
361 | if (!aTab.selected) {
362 | // Reloading action resumes the pending restoration.
363 | // This will fire "SSTabRestored" event, then this method
364 | // will be called again to restore actual history entries.
365 | aTab.linkedBrowser.reload();
366 | return Promise.resolve(true);
367 | }
368 |
369 | {
370 | let event = this.document.createEvent('Events');
371 | event.initEvent(this.EVENT_TYPE_RESUMING, true, true);
372 | if (!aTab.dispatchEvent(event))
373 | return Promise.resolve(false);
374 | }
375 |
376 | var state = this.getTabState(aTab, true);
377 | var options = this.getTabOptions(aTab, true);
378 | if (!state)
379 | return Promise.resolve(true);
380 |
381 | fullStates.delete(aTab);
382 |
383 | SessionStoreInternal.restoreTabContent(aTab);
384 |
385 | var event = this.document.createEvent('Events');
386 | event.initEvent(this.EVENT_TYPE_RESUMED, true, false);
387 | aTab.dispatchEvent(event);
388 |
389 | if (this.debug)
390 | aTab.setAttribute('tooltiptext', aTab.label);
391 |
392 | return Promise.resolve(true);
393 | },
394 |
395 | resumeAll : function(aRestoreOnlySuspendedByMe)
396 | {
397 | return Promise.all([...this.tabs].map(function(aTab) {
398 | if (!aRestoreOnlySuspendedByMe ||
399 | aTab.getAttribute(this.SUSPENDED) == 'true')
400 | return this.resumeOne(aTab);
401 | }, this));
402 | },
403 |
404 | getTabState : function(aTab, aClear)
405 | {
406 | var state = SS.getTabValue(aTab, this.STATE);
407 | if (!state)
408 | return null;
409 |
410 | if (aClear)
411 | SS.setTabValue(aTab, this.STATE, '');
412 |
413 | return fullStates.get(aTab) || JSON.parse(state);
414 | },
415 |
416 | getTabOptions : function(aTab, aClear)
417 | {
418 | var options = SS.getTabValue(aTab, this.OPTIONS);
419 | if (!options)
420 | return {};
421 |
422 | if (aClear)
423 | SS.setTabValue(aTab, this.OPTIONS, '');
424 |
425 | return JSON.parse(options);
426 | },
427 |
428 | // This restores history entries, but they don't eat the RAM
429 | // because Firefox doesn't build DOM tree until they are actually loaded.
430 | readyToResume : function(aTab)
431 | {
432 | var state = this.getTabState(aTab);
433 | if (!state)
434 | return;
435 |
436 | var browser = aTab.linkedBrowser;
437 | var tabbrowser = this.browser;
438 |
439 |
440 | // ==BEGIN==
441 | // these codes are imported from SessionStoreInternal.restoreTabs()
442 |
443 | // Ensure the index is in bounds.
444 | let activeIndex = (state.index || state.entries.length) - 1;
445 | activeIndex = Math.min(activeIndex, state.entries.length - 1);
446 | activeIndex = Math.max(activeIndex, 0);
447 |
448 | // Save the index in case we updated it above.
449 | state.index = activeIndex + 1;
450 |
451 | // In electrolysis, we may need to change the browser's remote
452 | // attribute so that it runs in a content process.
453 | let activePageData = state.entries[activeIndex] || null;
454 | let uri = activePageData ? activePageData.url || null : null;
455 | tabbrowser.updateBrowserRemotenessByURL(browser, uri);
456 |
457 | // Start a new epoch and include the epoch in the restoreHistory
458 | // message. If a message is received that relates to a previous epoch, we
459 | // discard it.
460 | let epoch;
461 | if (typeof SessionStoreInternal.startNextEpoch == 'function') {
462 | epoch = SessionStoreInternal.startNextEpoch(browser);
463 | } else {
464 | epoch = SessionStoreInternal._nextRestoreEpoch++;
465 | SessionStoreInternal._browserEpochs.set(browser.permanentKey, epoch);
466 | }
467 |
468 | // keep the data around to prevent dataloss in case
469 | // a tab gets closed before it's been properly restored
470 | browser.__SS_data = state;
471 | browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE;
472 | browser.setAttribute('pending', 'true');
473 | aTab.setAttribute('pending', 'true');
474 |
475 | // Update the persistent tab state cache with |state| information.
476 | TabStateCache.update(browser, {
477 | history: {entries: state.entries, index: state.index},
478 | scroll: state.scroll || null,
479 | storage: state.storage || null,
480 | formdata: state.formdata || null,
481 | disallow: state.disallow || null,
482 | pageStyle: state.pageStyle || null,
483 | // This information is only needed until the tab has finished restoring.
484 | // When that's done it will be removed from the cache and we always
485 | // collect it in TabState._collectBaseTabData().
486 | image: state.image || '',
487 | userTypedValue: state.userTypedValue || '',
488 | userTypedClear: state.userTypedClear || 0
489 | });
490 |
491 | browser.messageManager.sendAsyncMessage('SessionStore:restoreHistory',
492 | {tabData: state, epoch: epoch});
493 |
494 | TabRestoreQueue.add(aTab);
495 | // ==END==
496 | }
497 | });
498 | SuspendTabInternal.isAvailable = isInternalAPIsAvailable;
499 |
500 | SuspendTabInternal.instances = [];
501 |
502 | SuspendTabInternal.resumeAll = function(aRestoreOnlySuspendedByMe) {
503 | return Promise.all(this.instances.map(function(aInstance) {
504 | return aInstance.resumeAll(aRestoreOnlySuspendedByMe);
505 | }));
506 | };
507 |
508 | function shutdown(aReason)
509 | {
510 | if (aReason == 'ADDON_DISABLE')
511 | return SuspendTabInternal.resumeAll(true)
512 | .then(shutdownPostProcess);
513 | else
514 | return shutdownPostProcess();
515 | }
516 | function shutdownPostProcess(aReason)
517 | {
518 | return Promise.all(SuspendTabInternal.instances.map(function(aInstance) {
519 | return aInstance.destroy(aReason == 'ADDON_DISABLE');
520 | }))
521 | .then(function() {
522 | SuspendTabInternal.instances = [];
523 | setTimeout = clearTimeout = undefined;
524 |
525 | SS = undefined;
526 | SessionStoreInternal = undefined;
527 | // TabRestoreStates = undefined;
528 | TabState = undefined;
529 | TabStateCache = undefined;
530 | TabStateFlusher = undefined;
531 |
532 | fullStates = undefined;
533 |
534 | SuspendTabInternal = undefined;
535 |
536 | shutdown = undefined;
537 | });
538 | }
539 |
--------------------------------------------------------------------------------
/modules/suspendtab.js:
--------------------------------------------------------------------------------
1 | /* ***** BEGIN LICENSE BLOCK *****
2 | * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3 | *
4 | * The contents of this file are subject to the Mozilla Public License Version
5 | * 1.1 (the "License"); you may not use this file except in compliance with
6 | * the License. You may obtain a copy of the License at
7 | * http://www.mozilla.org/MPL/
8 | *
9 | * Software distributed under the License is distributed on an "AS IS" basis,
10 | * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 | * for the specific language governing rights and limitations under the
12 | * License.
13 | *
14 | * The Original Code is Suspend Tab.
15 | *
16 | * The Initial Developer of the Original Code is YUKI "Piro" Hiroshi.
17 | * Portions created by the Initial Developer are Copyright (C) 2012-2016
18 | * the Initial Developer. All Rights Reserved.
19 | *
20 | * Contributor(s):: YUKI "Piro" Hiroshi
21 | * YosukeM (Yosuke Morimoto) https://github.com/YosukeM
22 | * vzvu3k6k https://github.com/vzvu3k6k
23 | *
24 | * Alternatively, the contents of this file may be used under the terms of
25 | * either the GNU General Public License Version 2 or later (the "GPL"), or
26 | * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27 | * in which case the provisions of the GPL or the LGPL are applicable instead
28 | * of those above. If you wish to allow use of your version of this file only
29 | * under the terms of either the GPL or the LGPL, and not to allow others to
30 | * use your version of this file under the terms of the MPL, indicate your
31 | * decision by deleting the provisions above and replace them with the notice
32 | * and other provisions required by the GPL or the LGPL. If you do not delete
33 | * the provisions above, a recipient may use your version of this file under
34 | * the terms of any one of the MPL, the GPL or the LGPL.
35 | *
36 | * ***** END LICENSE BLOCK ***** */
37 |
38 | var EXPORTED_SYMBOLS = ['SuspendTab'];
39 |
40 | load('lib/WindowManager');
41 | load('lib/prefs');
42 | load('lib/here');
43 |
44 | var bundle = require('lib/locale')
45 | .get('chrome://suspendtab/locale/label.properties');
46 |
47 | var { Services } = Cu.import('resource://gre/modules/Services.jsm', {});
48 | var { setTimeout, clearTimeout } = Cu.import('resource://gre/modules/Timer.jsm', {});
49 |
50 | load('suspendtab-internal');
51 |
52 | function SuspendTab(aWindow)
53 | {
54 | this.init(aWindow);
55 | }
56 | SuspendTab.prototype = inherit(require('const'), {
57 | get debug()
58 | {
59 | return prefs.getPref(this.domain + 'debug');
60 | },
61 |
62 | get autoSuspend()
63 | {
64 | return prefs.getPref(this.domain + 'autoSuspend.enabled');
65 | },
66 | get autoSuspendTimeout()
67 | {
68 | return prefs.getPref(this.domain + 'autoSuspend.timeout') * prefs.getPref(this.domain + 'autoSuspend.timeout.factor');
69 | },
70 | get autoSuspendResetOnReload()
71 | {
72 | return prefs.getPref(this.domain + 'autoSuspend.resetOnReload');
73 | },
74 | get autoSuspendTooManyTabs()
75 | {
76 | return prefs.getPref(this.domain + 'autoSuspend.tooManyTabs');
77 | },
78 | get maxTabsOnMemory()
79 | {
80 | return prefs.getPref(this.domain + 'autoSuspend.tooManyTabs.maxTabsOnMemory');
81 | },
82 | get autoSuspendNewBackgroundTab()
83 | {
84 | return prefs.getPref(this.domain + 'autoSuspend.newBackgroundTab');
85 | },
86 | get autoSuspendNewBackgroundTabAfterLoad()
87 | {
88 | return prefs.getPref(this.domain + 'autoSuspend.newBackgroundTab.afterLoad');
89 | },
90 |
91 | get document()
92 | {
93 | return this.window.document;
94 | },
95 | get browser()
96 | {
97 | return this.window.gBrowser;
98 | },
99 | get tabs()
100 | {
101 | return this.browser.mTabContainer.childNodes;
102 | },
103 | get tabsFromOldToNew()
104 | {
105 | var tabs = [...this.tabs];
106 | return tabs.sort(function(aA, aB) {
107 | var a = aA.__suspendtab__lastFocused || aA.__suspendtab__openedAt || aA._tPos || 0;
108 | var b = aB.__suspendtab__lastFocused || aB.__suspendtab__openedAt || aB._tPos || 0;
109 | return a - b;
110 | });
111 | },
112 | get tabsFromNewToOld()
113 | {
114 | return this.tabsFromOldToNew.reverse();
115 | },
116 | get tabContextPopup()
117 | {
118 | return this.document.getElementById('tabContextMenu');
119 | },
120 | get contentContextPopup()
121 | {
122 | return this.document.getElementById('contentAreaContextMenu');
123 | },
124 |
125 | get blockList()
126 | {
127 | if (!('_blockList' in this)) {
128 | this._blockList = prefs.getPref(this.domain + 'autoSuspend.blockList');
129 |
130 | if (this._blockList) {
131 | this._blockList = this._blockList.split(/\s+/).map(function(aItem) {
132 | return this._generateRegExpFromRule(aItem);
133 | }, this).filter(function(aRule) {
134 | return Boolean(aRule);
135 | });
136 | }
137 | }
138 | return this._blockList;
139 | },
140 | _generateRegExpFromRule : function(aRule)
141 | {
142 | try {
143 | var ruleWithScheme = this.RULE_WITH_SCHEME.test(aRule);
144 | var regexp = aRule.replace(/\./g, '\\.')
145 | .replace(/\?/g, '.')
146 | .replace(/\*/g, '.*');
147 | regexp = ruleWithScheme ? '^' + regexp : regexp ;
148 | return regexp && new RegExp(regexp, 'i');
149 | }
150 | catch(error) {
151 | Cu.reportError(new Error('suspendtab: invalid block rule "' + aRule + '"'));
152 | return null;
153 | }
154 | },
155 |
156 | handleEvent : function(aEvent)
157 | {
158 | switch (aEvent.type)
159 | {
160 | case 'popupshowing':
161 | return this.onPopupShowing(aEvent);
162 |
163 | case 'command':
164 | return this.onCommand(aEvent);
165 |
166 | case 'TabOpen':
167 | return this.onTabOpen(aEvent);
168 |
169 | case 'TabSelect':
170 | return this.onTabSelect(aEvent);
171 |
172 | case 'SSTabRestoring':
173 | return this.cancelTimer(aEvent.originalTarget);
174 |
175 | case 'SSTabRestored':
176 | return this.onTabRestored(aEvent);
177 |
178 | case this.EVENT_TYPE_TAB_LOADED:
179 | return this.handleReloadedTab(aEvent.originalTarget);
180 |
181 | case 'unload':
182 | return this.destroy();
183 | }
184 | },
185 |
186 | observe : function(aSubject, aTopic, aData)
187 | {
188 | if (aTopic != 'nsPref:changed')
189 | return;
190 |
191 | switch (aData)
192 | {
193 | case this.domain + 'autoSuspend.blockList':
194 | delete this._blockList;
195 | case this.domain + 'autoSuspend.enabled':
196 | case this.domain + 'autoSuspend.timeout':
197 | case this.domain + 'autoSuspend.timeout.factor':
198 | case this.domain + 'autoSuspend.tooManyTabs':
199 | case this.domain + 'autoSuspend.tooManyTabs.maxTabsOnMemory':
200 | return this.trySuspendBackgroundTabs(true);
201 | }
202 | },
203 |
204 | onPopupShowing : function(aEvent)
205 | {
206 | if (aEvent.target == this.tabContextPopup)
207 | return this.onTabContextPopupShowing(aEvent);
208 | if (aEvent.target == this.contentContextPopup)
209 | return this.onContentContextPopupShowing(aEvent);
210 | },
211 |
212 | onTabContextPopupShowing : function(aEvent)
213 | {
214 | var isLastTab = this.tabs.length == 1;
215 | var tab = this.browser.mContextTab;
216 |
217 | {
218 | let item = this.tabContextItem;
219 | if (this.isSuspended(tab)) {
220 | item.setAttribute('label', bundle.getString('tab.resume.label'));
221 | item.setAttribute('accesskey', bundle.getString('tab.resume.accesskey'));
222 | }
223 | else {
224 | item.setAttribute('label', bundle.getString('tab.suspend.label'));
225 | item.setAttribute('accesskey', bundle.getString('tab.suspend.accesskey'));
226 | }
227 |
228 | item.disabled = isLastTab;
229 | item.hidden = !prefs.getPref(this.domain + 'menu.' + item.id);
230 | }
231 |
232 | {
233 | let item = this.tabContextSuspendOthersItem;
234 | item.hidden = !prefs.getPref(this.domain + 'menu.' + item.id);
235 | }
236 |
237 | {
238 | let item = this.tabContextAddDomainExceptionItem;
239 | if (this.isBlocked(tab))
240 | item.setAttribute('checked', true);
241 | else
242 | item.removeAttribute('checked');
243 | item.hidden = !prefs.getPref(this.domain + 'menu.' + item.id);
244 | }
245 |
246 | this.showHideExtraItems(this.tabContextExtraMenuItems);
247 | },
248 | showHideExtraItems : function(aExtraItems)
249 | {
250 | var isLastTab = this.tabs.length == 1;
251 | var visibleItemsCount = 0;
252 | var sandbox = new Cu.Sandbox(
253 | this.window,
254 | { sandboxPrototype: this.window }
255 | );
256 | aExtraItems.forEach(function(aItem) {
257 | var availableChecker = aItem.getAttribute(this.MENUITEM_AVAILABLE);
258 | var available = (availableChecker ? Cu.evalInSandbox('(function() { ' + availableChecker + '})()', sandbox) : true);
259 | aItem.hidden = !available || !prefs.getPref(this.domain + 'menu.' + aItem.id);
260 | if (!aItem.hidden)
261 | visibleItemsCount++;
262 |
263 | var enabledChecker = aItem.getAttribute(this.MENUITEM_ENABLED);
264 | var enabled = (enabledChecker ? Cu.evalInSandbox('(function() { ' + enabledChecker + '})()', sandbox) : true);
265 | aItem.disabled = !enabled || isLastTab;
266 | }, this);
267 | return visibleItemsCount > 0;
268 | },
269 |
270 | onContentContextPopupShowing : function(aEvent)
271 | {
272 | var isLastTab = this.tabs.length == 1;
273 | var tab = this.browser.selectedTab;
274 | var visibleItemsCount = 0;
275 |
276 | {
277 | let item = this.contentContextItem;
278 | item.disabled = isLastTab;
279 | item.hidden = !prefs.getPref(this.domain + 'menu.' + item.id);
280 | if (!item.hidden)
281 | visibleItemsCount++;
282 | }
283 |
284 | {
285 | let item = this.contentContextSuspendOthersItem;
286 | item.hidden = !prefs.getPref(this.domain + 'menu.' + item.id);
287 | if (!item.hidden)
288 | visibleItemsCount++;
289 | }
290 |
291 | {
292 | let item = this.contentContextAddDomainExceptionItem;
293 | if (this.isBlocked(tab))
294 | item.setAttribute('checked', true);
295 | else
296 | item.removeAttribute('checked');
297 | item.hidden = !prefs.getPref(this.domain + 'menu.' + item.id);
298 | if (!item.hidden)
299 | visibleItemsCount++;
300 | }
301 |
302 | var anyItemVisible = this.showHideExtraItems(this.contentContextExtraMenuItems);
303 | if (anyItemVisible)
304 | visibleItemsCount++;
305 |
306 | this.contentContextSeparator.hidden = visibleItemsCount == 0;
307 | },
308 |
309 | onCommand : function(aEvent)
310 | {
311 | switch (aEvent.target.id)
312 | {
313 | case 'context_toggleTabSuspended':
314 | case 'contentContext_suspend':
315 | return this.onToggleSuspendedCommand(aEvent);
316 |
317 | case 'context_toggleTabSuspendException':
318 | case 'contentContext_toggleTabSuspendException':
319 | return this.onToggleExceptionCommand(aEvent);
320 |
321 | case 'context_suspendOthers':
322 | case 'contentContext_suspendOthers':
323 | return this.onSuspendOthersCommand(aEvent);
324 |
325 | default:
326 | return;
327 | }
328 | },
329 |
330 | onToggleSuspendedCommand : function(aEvent)
331 | {
332 | var tab = this.browser.mContextTab || this.browser.selectedTab;
333 | var TST = this.browser.treeStyleTab;
334 | if (this.isSuspended(tab)) {
335 | let resumed = this.resume(tab);
336 |
337 | if (TST && TST.isSubtreeCollapsed(tab)) {
338 | TST.getDescendantTabs(tab).forEach(function(aTab) {
339 | resumed = resumed && this.resume(aTab);
340 | }, this);
341 | }
342 |
343 | if (!resumed)
344 | return;
345 | }
346 | else {
347 | if (this.debug)
348 | dump('\n');
349 | let suspended = this.suspend(tab);
350 |
351 | if (TST && TST.isSubtreeCollapsed(tab)) {
352 | TST.getDescendantTabs(tab).forEach(function(aTab) {
353 | if (suspended && this.debug)
354 | dump(' \n');
355 | suspended = suspended && this.suspend(aTab);
356 | }, this);
357 | }
358 |
359 | if (!suspended)
360 | return;
361 | }
362 | },
363 | getNextFocusedTab : function(aTab)
364 | {
365 | var tabs = this.browser.visibleTabs;
366 | if (tabs.length == 1 && tabs[0] == aTab)
367 | tabs = this.tabs;
368 |
369 | if (!Array.isArray(tabs))
370 | tabs = [...tabs];
371 |
372 | var focusableTabs = tabs.filter(this.isTabFocusable, this);
373 | if (focusableTabs.length > 0)
374 | tabs = focusableTabs;
375 |
376 | var index = tabs.indexOf(aTab);
377 | switch (prefs.getPref(this.domain + 'autoSuspend.nextFocus')) {
378 | default:
379 | let TST = this.browser.treeStyleTab;
380 | if (TST) {
381 | let nextFocused = !TST.isSubtreeCollapsed(aTab) && TST.getFirstChildTab(aTab);
382 | nextFocused = nextFocused || TST.getNextSiblingTab(aTab) || TST.getPreviousSiblingTab(aTab);
383 | if (nextFocused && this.isTabFocusable(nextFocused))
384 | return nextFocused;
385 |
386 | return tabs.length ? tabs[0] : null ;
387 | }
388 | case this.NEXT_FOCUS_FOLLOWING:
389 | index = index > -1 && index + 1 <= tabs.length - 1 ?
390 | index + 1 :
391 | 0 ;
392 | return tabs[index];
393 |
394 | case this.NEXT_FOCUS_PREVIOUSLY_FOCUSED:
395 | tabs = this.tabsFromOldToNew;
396 | index = tabs.indexOf(aTab);
397 | if (index == 0)
398 | return tabs[1];
399 | case this.NEXT_FOCUS_PRECEDING:
400 | index = index > 1 ?
401 | index - 1 :
402 | tabs.length - 1 ;
403 | return tabs[index];
404 |
405 | case this.NEXT_FOCUS_FIRST:
406 | return tabs[0];
407 |
408 | case this.NEXT_FOCUS_LAST:
409 | return tabs[tabs.length - 1];
410 | }
411 | },
412 | isTabFocusable : function(aTab)
413 | {
414 | return (
415 | !aTab.hidden &&
416 | !this.internal.isSuspended(aTab) &&
417 | !this.internal.isSuspending(aTab) &&
418 | !this.internal.getTabState(aTab)
419 | );
420 | },
421 |
422 | onToggleExceptionCommand : function(aEvent)
423 | {
424 | var tab = this.browser.mContextTab || this.browser.selectedTab;
425 | var uri = tab.linkedBrowser.currentURI;
426 |
427 | var list = prefs.getPref(this.domain + 'autoSuspend.blockList') || '';
428 | if (this.isBlocked(tab)) {
429 | list = list.split(/\s+/).filter(function(aRule) {
430 | aRule = this._generateRegExpFromRule(aRule);
431 | return !this.testBlockRule(aRule, uri);
432 | }, this).join(' ');
433 | }
434 | else {
435 | let matcher = uri.spec;
436 | try {
437 | matcher = uri.host;
438 | }
439 | catch(e) {
440 | }
441 | list = (list + ' ' + matcher).trim();
442 | }
443 | prefs.setPref(this.domain + 'autoSuspend.blockList', list);
444 | },
445 |
446 | onSuspendOthersCommand : function(aEvent)
447 | {
448 | var tab = this.browser.mContextTab || this.browser.selectedTab;
449 | for (let oneTab of this.tabs) {
450 | if (oneTab != tab) {
451 | if (this.debug)
452 | dump('\n');
453 | this.suspend(oneTab);
454 | }
455 | }
456 | },
457 |
458 | onTabOpen : function(aEvent)
459 | {
460 | var tab = aEvent.originalTarget;
461 | tab.__suspendtab__openedAt = Date.now();
462 | if (this.autoSuspendNewBackgroundTab) {
463 | if (!tab.selected &&
464 | this.autoSuspendNewBackgroundTabAfterLoad)
465 | tab.__suspendtab__suspendAfterLoad = true;
466 |
467 | setTimeout((function() {
468 | if (!tab.parentNode || tab.selected)
469 | return;
470 |
471 | if (!this.autoSuspendNewBackgroundTabAfterLoad) {
472 | if (this.debug)
473 | dump('\n');
474 | this.suspend(tab, { newTabNotLoadedYet : true });
475 | }
476 | }).bind(this), 0);
477 | }
478 | else {
479 | this.trySuspendBackgroundTabs();
480 | }
481 | },
482 |
483 | onTabSelect : function(aEvent)
484 | {
485 | var tab = aEvent.originalTarget;
486 | if (this.debug)
487 | dump('tab '+tab._tPos+' is selected.\n');
488 | this.cancelTimer(tab);
489 | this.resume(tab);
490 | this.trySuspendBackgroundTabs();
491 | tab.__suspendtab__lastFocused = Date.now();
492 | },
493 |
494 | onTabRestored : function(aEvent)
495 | {
496 | var tab = aEvent.originalTarget;
497 | return this.resume(tab);
498 | },
499 |
500 | /**
501 | * This addon handles "reload" of tabs for multiple purposes:
502 | * 1. When a suspended tab is reloaded, restore the tab.
503 | * 2. When a normal tab is reloaded, cancel (and reset)
504 | * the timer of "auto suspend".
505 | */
506 | handleReloadedTab : function(aTab)
507 | {
508 | var possiblySuspended = aTab.linkedBrowser.currentURI.spec == 'about:blank';
509 | if (!this.autoSuspendResetOnReload && !possiblySuspended)
510 | return;
511 |
512 | if (this.isSuspended(aTab)) {
513 | let options = this.internal.getTabOptions(aTab);
514 | if (
515 | possiblySuspended &&
516 | // The blank page is loaded when it is suspended too.
517 | // We have to handle only "reloading of already suspended" tab,
518 | // in other words, we must ignore "just now suspended" tab.
519 | aTab.hasAttribute('pending')
520 | ) {
521 | if (options && options.label)
522 | aTab.visibleLabel = aTab.label = options.label;
523 | if (!options || !options.newTabNotLoadedYet)
524 | this.resume(aTab);
525 | }
526 | }
527 | else {
528 | if (
529 | !aTab.pinned &&
530 | this.autoSuspendResetOnReload &&
531 | !aTab.selected
532 | )
533 | this.reserveSuspend(aTab);
534 | }
535 | },
536 |
537 | trySuspendBackgroundTabs : function(aReset)
538 | {
539 | var tabs = [...this.tabs];
540 | var tabsOnMemory = tabs.length;
541 | if (this.autoSuspendTooManyTabs) {
542 | tabs = this.tabsFromNewToOld;
543 | tabsOnMemory = this.maxTabsOnMemory;
544 | if (!this.browser.selectedTab.pinned)
545 | tabsOnMemory--; // decrement at first, for the current tab!
546 | }
547 | tabs.forEach(function(aTab) {
548 | if (!this.isSuspendable(aTab))
549 | return;
550 | if (this.isSuspended(aTab) && !aReset)
551 | return;
552 | if (
553 | !aTab.__suspendtab__timer ||
554 | aReset ||
555 | aTab.pinned
556 | ) {
557 | if (!aTab.pinned && this.autoSuspend)
558 | this.reserveSuspend(aTab);
559 | else if (aTab.pinned || aReset)
560 | this.cancelTimer(aTab);
561 | }
562 | if (!aTab.pinned && !aTab.selected) {
563 | tabsOnMemory--;
564 | if (tabsOnMemory < 0) {
565 | if (this.debug)
566 | dump('\n');
567 | this.suspend(aTab);
568 | }
569 | }
570 | }, this);
571 | },
572 |
573 | isSuspendable : function(aTab)
574 | {
575 | if (
576 | aTab.selected ||
577 | aTab.pinned ||
578 | aTab.hasAttribute('protected') || // protected tab, by Tab Mix Plus or others ||
579 | !this.internal.isSuspendable(aTab)
580 | )
581 | return false;
582 |
583 | return !this.isBlocked(aTab);
584 | },
585 | isBlocked : function(aTab)
586 | {
587 | if (!this.blockList)
588 | return false;
589 |
590 | var uri = aTab.linkedBrowser.currentURI;
591 | return this.blockList.some(function(aRule) {
592 | return this.testBlockRule(aRule, uri);
593 | }, this);
594 | },
595 | RULE_WITH_SCHEME : /^[^:]+:/,
596 | SCHEME_PART_MATCHER : /^[^:]+(?:\/\/)?/,
597 | testBlockRule : function(aRule, aURI)
598 | {
599 | if (this.RULE_WITH_SCHEME.test(aRule.source)) {
600 | return aRule.test(aURI.spec);
601 | }
602 | else {
603 | try {
604 | let specWithoutScheme = aURI.spec.replace(this.SCHEME_PART_MATCHER);
605 | return aRule.test(specWithoutScheme);
606 | }
607 | catch(e) {
608 | return false;
609 | }
610 | }
611 | },
612 |
613 | cancelTimers : function()
614 | {
615 | for (let tab of this.tabs)
616 | {
617 | this.cancelTimer(tab);
618 | }
619 | },
620 |
621 | cancelTimer : function(aTab)
622 | {
623 | if (aTab.__suspendtab__timer) {
624 | if (this.debug)
625 | dump(' cancel timer for '+aTab._tPos+'\n');
626 | clearTimeout(aTab.__suspendtab__timer);
627 | aTab.__suspendtab__timestamp = 0;
628 | aTab.__suspendtab__timer = null;
629 | this.updateTooltip(aTab);
630 | }
631 | },
632 |
633 | reserveSuspend : function(aTab)
634 | {
635 | var timestamp = aTab.__suspendtab__timestamp;
636 | this.cancelTimer(aTab);
637 |
638 | if (this.isSuspended(aTab) ||
639 | !this.isSuspendable(aTab))
640 | return;
641 |
642 | var now = Date.now();
643 | if (this.debug) {
644 | dump(' reserve suspend '+aTab._tPos+'\n');
645 | dump(' timestamp = '+timestamp+'\n');
646 | dump(' now = '+now+'\n');
647 | }
648 | if (timestamp && now - timestamp >= this.autoSuspendTimeout) {
649 | if (this.debug)
650 | dump('\n');
651 | return this.suspend(aTab);
652 | }
653 |
654 | aTab.__suspendtab__timestamp = timestamp || now;
655 | aTab.__suspendtab__timer = setTimeout((function() {
656 | if (!aTab.parentNode)
657 | return;
658 | aTab.__suspendtab__timestamp = 0;
659 | aTab.__suspendtab__timer = null;
660 | if (!aTab.selected && this.autoSuspend) {
661 | if (this.debug)
662 | dump('\n');
663 | this.suspend(aTab);
664 | }
665 | }).bind(this), this.autoSuspendTimeout)
666 |
667 | this.updateTooltip(aTab);
668 | },
669 |
670 | updateTooltip : function(aTab)
671 | {
672 | if (!this.debug || this.isSuspended(aTab) || aTab.selected) {
673 | if (aTab.getAttribute('tooltiptext') &&
674 | aTab.getAttribute('tooltiptext') == aTab.getAttribute('suspendtab-tooltiptext'))
675 | aTab.removeAttribute('tooltiptext');
676 | aTab.removeAttribute('suspendtab-tooltiptext');
677 | return;
678 | }
679 |
680 | var now = aTab.__suspendtab__timestamp || Date.now();
681 | var date = String(new Date(now + this.autoSuspendTimeout));
682 | var label = aTab.visibleLabel || aTab.label;
683 | label = bundle.getFormattedString('toBeSuspended.tooltip', [label, date]);
684 | aTab.setAttribute('tooltiptext', label);
685 | aTab.setAttribute('suspendtab-tooltiptext', label);
686 | dump(' => will be suspended at '+date+'\n');
687 | },
688 |
689 | get MutationObserver()
690 | {
691 | var w = this.window;
692 | return w.MutationObserver || w.MozMutationObserver;
693 | },
694 |
695 | onMutation : function(aMutations, aObserver)
696 | {
697 | aMutations.forEach(function(aMutation) {
698 | var target = aMutation.target;
699 | if (target.localName != 'tab')
700 | return;
701 | this.updateTooltip(target);
702 | }, this);
703 | },
704 |
705 | resumeAll : function(aRestoreOnlySuspendedByMe)
706 | {
707 | return Promise.all([...this.tabs].map(function(aTab) {
708 | this.cancelTimer(aTab);
709 | if (!aRestoreOnlySuspendedByMe ||
710 | aTab.getAttribute(this.SUSPENDED) == 'true')
711 | return this.resume(aTab);
712 | }, this));
713 | },
714 |
715 | reserveGC : function()
716 | {
717 | if (this.GCTimer) return;
718 | this.GCTimer = setTimeout((function() {
719 | this.GCTimer= null;
720 |
721 | Cu.forceGC();
722 | Services.obs.notifyObservers(null, 'child-gc-request', null);
723 |
724 | var utils = this.window
725 | .QueryInterface(Ci.nsIInterfaceRequestor)
726 | .getInterface(Ci.nsIDOMWindowUtils);
727 | if (utils.cycleCollect) {
728 | utils.cycleCollect();
729 | Services.obs.notifyObservers(null, 'child-cc-request', null);
730 | }
731 | }).bind(this), 0);
732 | },
733 |
734 | init : function(aWindow)
735 | {
736 | SuspendTab.instances.push(this);
737 |
738 | if (!SuspendTabInternal.isAvailable()) return;
739 |
740 | this.window = aWindow;
741 | this.internal = new SuspendTabInternal(aWindow);
742 |
743 | this.window.addEventListener('unload', this, false);
744 | this.window.addEventListener('TabOpen', this, false);
745 | this.window.addEventListener('TabSelect', this, true);
746 | this.window.addEventListener('SSTabRestoring', this, true);
747 | this.window.addEventListener('SSTabRestored', this, true);
748 | this.window.addEventListener(this.EVENT_TYPE_TAB_LOADED, this, true);
749 |
750 | this.trySuspendBackgroundTabs();
751 |
752 | prefs.addPrefListener(this);
753 |
754 | this.observer = new this.MutationObserver((function(aMutations, aObserver) {
755 | this.onMutation(aMutations, aObserver);
756 | }).bind(this));
757 | this.observer.observe(this.browser.tabContainer, {
758 | attributes : true,
759 | subtree : true,
760 | attributeFilter : [
761 | 'label',
762 | 'visibleLabel'
763 | ]
764 | });
765 |
766 | this.initMenuItems();
767 | },
768 |
769 | initMenuItems : function()
770 | {
771 | this.tabContextPopup.addEventListener('popupshowing', this, false);
772 | this.contentContextPopup.addEventListener('popupshowing', this, false);
773 |
774 | this.tabContextItem = this.document.createElement('menuitem');
775 | this.tabContextItem.setAttribute('id', 'context_toggleTabSuspended');
776 | this.tabContextItem.addEventListener('command', this, false);
777 |
778 | var undoCloseTabItem = this.document.getElementById('context_undoCloseTab');
779 | this.tabContextPopup.insertBefore(this.tabContextItem, undoCloseTabItem);
780 |
781 | this.contentContextSeparator = this.document.createElement('menuseparator');
782 | this.contentContextSeparator.setAttribute('id', 'contentContext_suspend_separator');
783 | this.contentContextPopup.appendChild(this.contentContextSeparator);
784 |
785 | this.contentContextItem = this.document.createElement('menuitem');
786 | this.contentContextItem.setAttribute('id', 'contentContext_suspend');
787 | this.contentContextItem.setAttribute('label', bundle.getString('tab.suspend.label'));
788 | this.contentContextItem.setAttribute('accesskey', bundle.getString('tab.suspend.accesskey'));
789 | this.contentContextItem.addEventListener('command', this, false);
790 | this.contentContextPopup.appendChild(this.contentContextItem);
791 |
792 |
793 | this.tabContextExtraMenuItems = [];
794 | this.contentContextExtraMenuItems = [];
795 |
796 | if ('TreeStyleTabService' in this.window) {
797 | let collectTreeTabs = here(/*
798 | var tab = gBrowser.mContextTab || gBrowser.selectedTab;
799 | var tabs = [tab].concat(gBrowser.treeStyleTab.getDescendantTabs(tab));
800 | */);
801 | {
802 | let item = this.document.createElement('menuitem');
803 | this.tabContextExtraMenuItems.push(item);
804 | item.setAttribute('id', 'context_suspendTree');
805 | item.setAttribute('label', bundle.getString('tab.suspendTree.label'));
806 | item.setAttribute('accesskey', bundle.getString('tab.suspendTree.accesskey'));
807 | item.setAttribute('oncommand', collectTreeTabs + here(/*
808 | tabs.forEach(function(aTab) {
809 | if (SuspendTab.debug)
810 | dump('\n');
811 | SuspendTab.suspend(aTab);
812 | });
813 | */));
814 | item.setAttribute(this.MENUITEM_ENABLED, collectTreeTabs + here(/*
815 | return tabs.some(function(aTab) {
816 | return !SuspendTab.isSuspended(aTab);
817 | });
818 | */));
819 | item.setAttribute(this.MENUITEM_AVAILABLE,
820 | 'return gBrowser.treeStyleTab.hasChildTabs(gBrowser.mContextTab || gBrowser.selectedTab);');
821 | this.tabContextPopup.insertBefore(item, undoCloseTabItem);
822 |
823 | let contentItem = item.cloneNode(true);
824 | this.contentContextExtraMenuItems.push(contentItem);
825 | contentItem.setAttribute('id', 'contentContext_suspendTree');
826 | this.contentContextPopup.appendChild(contentItem);
827 | }
828 | {
829 | let item = this.document.createElement('menuitem');
830 | this.tabContextExtraMenuItems.push(item);
831 | item.setAttribute('id', 'context_resumeTree');
832 | item.setAttribute('label', bundle.getString('tab.resumeTree.label'));
833 | item.setAttribute('accesskey', bundle.getString('tab.resumeTree.accesskey'));
834 | item.setAttribute('oncommand', collectTreeTabs + here(/*
835 | tabs.forEach(function(aTab) {
836 | SuspendTab.resume(aTab);
837 | });
838 | */));
839 | item.setAttribute(this.MENUITEM_ENABLED, collectTreeTabs + here(/*
840 | return tabs.some(function(aTab) {
841 | return SuspendTab.isSuspended(aTab);
842 | });
843 | */));
844 | item.setAttribute(this.MENUITEM_AVAILABLE,
845 | 'return gBrowser.treeStyleTab.hasChildTabs(gBrowser.mContextTab || gBrowser.selectedTab);');
846 | this.tabContextPopup.insertBefore(item, undoCloseTabItem);
847 |
848 | let contentItem = item.cloneNode(true);
849 | this.contentContextExtraMenuItems.push(contentItem);
850 | contentItem.setAttribute('id', 'contentContext_resumeTree');
851 | this.contentContextPopup.appendChild(contentItem);
852 | }
853 | }
854 |
855 | this.tabContextSuspendOthersItem = this.document.createElement('menuitem');
856 | this.tabContextSuspendOthersItem.setAttribute('id', 'context_suspendOthers');
857 | this.tabContextSuspendOthersItem.setAttribute('label', bundle.getString('tab.suspendOthers.label'));
858 | this.tabContextSuspendOthersItem.setAttribute('accesskey', bundle.getString('tab.suspendOthers.accesskey'));
859 | this.tabContextSuspendOthersItem.addEventListener('command', this, false);
860 | this.tabContextPopup.insertBefore(this.tabContextSuspendOthersItem, undoCloseTabItem);
861 |
862 | this.contentContextSuspendOthersItem = this.tabContextSuspendOthersItem.cloneNode(true);
863 | this.contentContextSuspendOthersItem.setAttribute('id', 'contentContext_suspendOthers');
864 | this.contentContextSuspendOthersItem.addEventListener('command', this, false);
865 | this.contentContextPopup.appendChild(this.contentContextSuspendOthersItem);
866 |
867 |
868 | this.tabContextAddDomainExceptionItem = this.document.createElement('menuitem');
869 | this.tabContextAddDomainExceptionItem.setAttribute('id', 'context_toggleTabSuspendException');
870 | this.tabContextAddDomainExceptionItem.setAttribute('label', bundle.getString('tab.exception.add.label'));
871 | this.tabContextAddDomainExceptionItem.setAttribute('accesskey', bundle.getString('tab.exception.add.accesskey'));
872 | this.tabContextAddDomainExceptionItem.setAttribute('type', 'checkbox');
873 | this.tabContextAddDomainExceptionItem.addEventListener('command', this, false);
874 | this.tabContextPopup.insertBefore(this.tabContextAddDomainExceptionItem, undoCloseTabItem);
875 |
876 | this.contentContextAddDomainExceptionItem = this.tabContextAddDomainExceptionItem.cloneNode(true);
877 | this.contentContextAddDomainExceptionItem.setAttribute('id', 'contentContext_toggleTabSuspendException');
878 | this.contentContextAddDomainExceptionItem.addEventListener('command', this, false);
879 | this.contentContextPopup.appendChild(this.contentContextAddDomainExceptionItem);
880 | },
881 |
882 | destroy : function()
883 | {
884 | if (this.window) {
885 | this.cancelTimers();
886 |
887 | this.destroyMenuItems();
888 |
889 | prefs.removePrefListener(this);
890 |
891 | this.observer.disconnect();
892 | delete this.observer;
893 |
894 | this.window.removeEventListener('unload', this, false);
895 | this.window.removeEventListener('TabOpen', this, false);
896 | this.window.removeEventListener('TabSelect', this, true);
897 | this.window.removeEventListener('SSTabRestoring', this, true);
898 | this.window.removeEventListener('SSTabRestored', this, true);
899 | this.window.removeEventListener(this.EVENT_TYPE_TAB_LOADED, this, true);
900 |
901 | delete this.window;
902 | }
903 |
904 | if (this.internal)
905 | delete this.internal;
906 |
907 | if (SuspendTab)
908 | SuspendTab.instances.splice(SuspendTab.instances.indexOf(this), 1);
909 | },
910 |
911 | destroyMenuItems : function()
912 | {
913 | this.tabContextPopup.removeEventListener('popupshowing', this, false);
914 | this.contentContextPopup.removeEventListener('popupshowing', this, false);
915 |
916 | [
917 | 'tabContextItem',
918 | 'contentContextItem',
919 | 'tabContextSuspendOthersItem',
920 | 'contentContextSuspendOthersItem',
921 | 'tabContextAddDomainExceptionItem',
922 | 'contentContextAddDomainExceptionItem'
923 | ].forEach(function(aKey) {
924 | this[aKey].removeEventListener('command', this, false);
925 | this[aKey].parentNode.removeChild(this[aKey]);
926 | delete this[aKey];
927 | }, this);
928 |
929 | [this.contentContextSeparator]
930 | .concat(this.tabContextExtraMenuItems)
931 | .concat(this.contentContextExtraMenuItems)
932 | .forEach(function(aItem) {
933 | aItem.parentNode.removeChild(aItem);
934 | });
935 | delete this.contentContextSeparator;
936 | delete this.tabContextExtraMenuItems;
937 | delete this.contentContextExtraMenuItems;
938 | },
939 |
940 |
941 | isSuspended : function(aTab)
942 | {
943 | return (
944 | this.internal &&
945 | !this.internal.destroyed &&
946 | this.internal.isSuspended(aTab)
947 | );
948 | },
949 |
950 | isSuspending : function(aTab)
951 | {
952 | return (
953 | this.internal &&
954 | !this.internal.destroyed &&
955 | this.internal.isSuspending(aTab)
956 | );
957 | },
958 |
959 | suspend : function(aTab, aOptions)
960 | {
961 | if (this.isSuspended(aTab))
962 | return true;
963 |
964 | if (!this.internal ||
965 | this.internal.destroyed)
966 | return false;
967 |
968 | return this.internal.suspend(aTab, aOptions)
969 | .then((function() {
970 | if (!this.window) // service already destroyed
971 | return;
972 | if (aTab.selected) {
973 | let nextFocused = this.getNextFocusedTab(aTab);
974 | if (nextFocused)
975 | this.browser.selectedTab = nextFocused;
976 | }
977 | this.reserveGC();
978 |
979 | return true;
980 | }).bind(this));
981 | },
982 |
983 | resume : function(aTabs)
984 | {
985 | return this.internal &&
986 | !this.internal.destroyed &&
987 | this.internal.resume(aTabs);
988 | }
989 | });
990 |
991 | SuspendTab.instances = [];
992 |
993 | SuspendTab.resumeAll = function(aRestoreOnlySuspendedByMe) {
994 | return Promise.all(this.instances.map(function(aInstance) {
995 | return aInstance.resumeAll(aRestoreOnlySuspendedByMe);
996 | }));
997 | };
998 |
999 | function shutdown(aReason)
1000 | {
1001 | if (aReason == 'ADDON_DISABLE')
1002 | return SuspendTab.resumeAll(true)
1003 | .then(shutdownPostProcess);
1004 | else
1005 | return shutdownPostProcess();
1006 | }
1007 | function shutdownPostProcess(aReason)
1008 | {
1009 | return Promise.all(SuspendTab.instances.map(function(aInstance) {
1010 | return aInstance.destroy(aReason == 'ADDON_DISABLE');
1011 | }))
1012 | .then(function() {
1013 | SuspendTab.instances = [];
1014 |
1015 | WindowManager = undefined;
1016 | setTimeout = clearTimeout = undefined;
1017 | bundle = undefined;
1018 | Services = undefined;
1019 |
1020 | SuspendTab.instances = undefined;
1021 | SuspendTab = undefined;
1022 | SuspendTabInternal = undefined;
1023 |
1024 | shutdown = undefined;
1025 | shutdownPostProcess = undefined;
1026 | });
1027 | }
1028 |
--------------------------------------------------------------------------------