Mercurial > addons > firefox-addons > tab-mover
diff background.js @ 0:480f8e4f4500
Initial revision
author | Guido Berhoerster <guido+tab-mover@berhoerster.name> |
---|---|
date | Sun, 19 Feb 2017 00:20:26 +0100 |
parents | |
children | 2a87d7a3863f |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/background.js Sun Feb 19 00:20:26 2017 +0100 @@ -0,0 +1,398 @@ +/* + * Copyright (C) 2017 Guido Berhoerster <guido+tab-mover@berhoerster.name> + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +'use strict'; + +function createContextMenuItem(createProperties) { + return new Promise((resolve, reject) => { + browser.contextMenus.create(createProperties, () => { + if (browser.runtime.lastError) { + reject(browser.runtime.lastError); + } else { + resolve(); + } + }); + }); +} + +const Observable = (superclass) => class extends superclass { + constructor(...args) { + super(...args); + + this._observers = new Map(); + } + + addObserver(eventName, observer, thisArg) { + if (!this._observers.has(eventName)) { + this._observers.set(eventName, new Set()); + } + + this._observers.get(eventName).add(observer); + } + + deleteObserver(eventName, observer) { + if (this._observers.has(eventName)) { + this._observers.get(eventName).delete(observer); + } + } + + notifyObservers(eventName, ...args) { + if (!this._observers.has(eventName)) { + return; + } + + for (let observer of this._observers.get(eventName)) { + observer(eventName, ...args); + } + } +} + +class WindowsModel extends Observable(Object) { + constructor() { + super(); + + this.windows = new Map(); + this.focusedWindowId = browser.windows.WINDOW_ID_NONE; + } + + hasWindow(id) { + return this.windows.has(id); + } + + getWindow(id) { + return this.windows.get(id); + } + + getAllWindows() { + return this.windows.values(); + } + + getfocusedWindowId() { + return this.focusedWindowId; + } + + openWindow(id, incognito = false) { + this.windows.set(id, { + id, + title: browser.i18n.getMessage(incognito ? + 'defaultIncognitoWindowTitle' : 'defaultWindowTitle', id), + incognito + }); + + this.notifyObservers('window-opened', id); + } + + updateWindowTitle(id, title) { + if (!this.windows.has(id)) { + return; + } + + let windowInfo = this.windows.get(id) + windowInfo.title = browser.i18n.getMessage(windowInfo.incognito ? + 'incognitoWindowTitle' : 'windowTitle', title); + + this.notifyObservers('window-title-updated', id, title); + } + + focusWindow(id) { + let oldId = this.focusedWindowId; + this.focusedWindowId = this.windows.has(id) ? id : + browser.windows.WINDOW_ID_NONE; + + this.notifyObservers('window-focus-changed', oldId, id); + } + + closeWindow(id) { + if (!this.windows.has(id)) { + return; + } + + this.windows.delete(id); + + if (id === this.focusedWindowId) { + this.focusedWindowId = browser.windows.WINDOW_ID_NONE; + } + + this.notifyObservers('window-closed', id); + } +} + +class MenuView { + constructor(model) { + this.model = model; + this.moveMenuIds = new Set(); + this.reopenMenuIds = new Set(); + this.menuContexts = ['tab']; + + browser.runtime.getBrowserInfo().then(browserInfo => { + // Firefox before version 53 does not support tab context menus + let majorVersion = browserInfo.version.match(/^\d+/); + if (majorVersion !== null && majorVersion < 53) { + this.menuContexts = ['all']; + } + + return Promise.all([ + // create submenus + createContextMenuItem({ + id: 'move-menu', + title: browser.i18n.getMessage('moveToWindowMenu'), + enabled: false, + contexts: this.menuContexts + }), + createContextMenuItem({ + id: 'reopen-menu', + title: browser.i18n.getMessage('reopenInWindowMenu'), + enabled: false, + contexts: this.menuContexts + }) + ]); + }).then(values => { + this.model.addObserver('window-opened', + this.onWindowOpened.bind(this)); + this.model.addObserver('window-title-updated', + this.onWindowTitleUpdated.bind(this)); + this.model.addObserver('window-focus-changed', + this.onWindowFocusChanged.bind(this)); + this.model.addObserver('window-closed', + this.onWindowClosed.bind(this)); + }).catch(error => { + console.log('Error:', error); + }); + } + + enableMenus() { + return Promise.all([ + browser.contextMenus.update('move-menu', { + enabled: this.moveMenuIds.size > 0 + }), + browser.contextMenus.update('reopen-menu', { + enabled: this.reopenMenuIds.size > 0 + }) + ]); + } + + onWindowOpened(eventName, windowId) { + let focusedWindowId = this.model.getfocusedWindowId(); + if (focusedWindowId === browser.windows.WINDOW_ID_NONE) { + return; + } + + let menuId = String(windowId); + let windowInfo = this.model.getWindow(windowId); + let incognito = this.model.getWindow(focusedWindowId).incognito; + + if (incognito && !windowInfo.incognito) { + this.reopenMenuIds.add(menuId); + } else { + this.moveMenuIds.add(menuId); + } + + createContextMenuItem({ + id: menuId, + title: windowInfo.title, + contexts: this.menuContexts, + parentId: (incognito && !windowInfo.incognito) ? + 'reopen-menu' : 'move-menu' + }).then(() => { + return this.enableMenus(); + }).catch(error => { + console.log('Error:', error); + }); + } + + onWindowTitleUpdated(eventName, windowId, title) { + if (this.model.getfocusedWindowId() === + browser.windows.WINDOW_ID_NONE) { + return; + } + + browser.contextMenus.update(String(windowId), {title}).catch(error => { + console.log('Error:', error); + }); + } + + onWindowFocusChanged(eventName, oldWindowId, newWindowId) { + let promises = [ + // disable submenus + browser.contextMenus.update('move-menu', { + enabled: false + }), + browser.contextMenus.update('reopen-menu', { + enabled: false + }) + ]; + + if (newWindowId === browser.windows.WINDOW_ID_NONE) { + // just disable the submenus if focus moved to a window not tracked + Promise.all(promises).catch(error => { + console.log('Error:', error); + }); + return; + } + + Promise.all(promises).then(values => { + // remove all submenu items + let promises = new Array(...this.moveMenuIds, + ...this.reopenMenuIds).map(menuId => { + this.moveMenuIds.delete(menuId) || + this.reopenMenuIds.delete(menuId); + + return browser.contextMenus.remove(menuId); + }); + + return Promise.all(promises); + }).then(values => { + let incognito = this.model.getWindow(newWindowId).incognito; + + // rebuild submenus + let promises = []; + for (let windowInfo of this.model.getAllWindows()) { + if (windowInfo.id === newWindowId) { + // skip the currently focused window + continue; + } + + let menuId = String(windowInfo.id); + if (incognito && !windowInfo.incognito) { + this.reopenMenuIds.add(menuId); + } else { + this.moveMenuIds.add(menuId); + } + + // create menu item + promises.push(createContextMenuItem({ + id: menuId, + title: windowInfo.title, + contexts: this.menuContexts, + parentId: (incognito && !windowInfo.incognito) ? + 'reopen-menu' : 'move-menu' + })); + } + + return Promise.all(promises); + }).then(values => { + return this.enableMenus(); + }).catch(error => { + console.log('Error:', error); + }); + } + + onWindowClosed(eventName, windowId) { + if (this.model.getfocusedWindowId() === + browser.windows.WINDOW_ID_NONE) { + return; + } + + let menuId = String(windowId); + + this.moveMenuIds.delete(menuId) || this.reopenMenuIds.delete(menuId); + + browser.contextMenus.remove(menuId).then(() => { + return this.enableMenus(); + }).catch(error => { + console.log('Error:', error); + }); + } +} + +class Presenter { + constructor(model, view) { + this.model = model; + this.view = view; + + browser.windows.getAll({windowTypes: ['normal']}).then(windows => { + // populate model with existing windows + for (let windowInfo of windows) { + this.onWindowCreated(windowInfo); + + if (windowInfo.focused) { + this.onWindowFocusChanged(windowInfo.id); + } + } + + browser.windows.onCreated + .addListener(this.onWindowCreated.bind(this)); + browser.windows.onRemoved + .addListener(this.onWindowRemoved.bind(this)); + browser.windows.onFocusChanged + .addListener(this.onWindowFocusChanged.bind(this)); + browser.contextMenus.onClicked + .addListener(this.onMenuItemClicked.bind(this)); + }).catch(error => { + console.log('Error:', error); + }); + } + + onWindowCreated(windowInfo) { + // only track normal windows + if (windowInfo.type !== 'normal') { + return; + } + + this.model.openWindow(windowInfo.id, windowInfo.incognito); + + // get the window title and update the model + browser.tabs.query({ + active: true, + windowId: windowInfo.id + }).then(tabs => { + this.model.updateWindowTitle(tabs[0].windowId, tabs[0].title) + }).catch(error => { + console.log('Error:', error); + }); + } + + onWindowRemoved(windowId) { + this.model.closeWindow(windowId); + } + + onWindowFocusChanged(windowId) { + let prevFocusedWindowId = this.model.getfocusedWindowId(); + if (prevFocusedWindowId !== browser.windows.WINDOW_ID_NONE) { + // get title of the previously focused window and update the model + browser.tabs.query({ + active: true, + windowId: prevFocusedWindowId + }).then(tabs => { + this.model.updateWindowTitle(tabs[0].windowId, tabs[0].title) + }).catch(error => { + console.log('Error:', error); + }); + } + + this.model.focusWindow(windowId); + } + + onMenuItemClicked(info, tab) { + if (info.parentMenuItemId === 'move-menu') { + // move tab from the current window to the selected window + browser.tabs.move(tab.id, { + windowId: parseInt(info.menuItemId), + index: -1 + }).catch(error => { + console.log('Error:', error); + }); + } else { + // open the URL of the current tab in the selected window and close + // the current tab + browser.tabs.create({ + url: tab.url, + windowId: parseInt(info.menuItemId), + index: -1 + }).then(newTab => { + return browser.tabs.remove(tab.id); + }).catch(error => { + console.log('Error:', error); + }); + } + } +} + +let windowsModel = new WindowsModel(); +let menuView = new MenuView(windowsModel); +let presenter = new Presenter(windowsModel, menuView);