├── .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 | 100 | 101 | 102 | 104 | 105 | 106 | 108 | 114 | 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 | 181 | 182 | 185 | 188 | 191 | 194 | 197 | 200 | 203 | 206 | 209 | 212 | 213 | 214 | 215 | 217 | 219 | 221 | 222 | 224 | 226 | 227 | 228 | 229 | 230 | 232 | 234 | 236 | 237 | 239 | 241 | 242 | 243 | 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 | --------------------------------------------------------------------------------