Mercurial > addons > firefox-addons > tab-mover
view background.js @ 11:5d7914307782
Allow reopening tabs from normal windows in incognito windows
Moving a tab from a normal window to a window in incognito mode does not work,
it result in a new empty tab. Thus, allow reopening instead of moving such
tabs.
author | Guido Berhoerster <guido+tab-mover@berhoerster.name> |
---|---|
date | Mon, 20 Feb 2017 17:40:31 +0100 |
parents | 2a87d7a3863f |
children | e32b90567f39 |
line wrap: on
line source
/* * 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) { 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; } 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) { this.focusedWindowId = this.windows.has(id) ? id : browser.windows.WINDOW_ID_NONE; this.notifyObservers('window-focus-changed', 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) { // no window is focused so there is no need to update the menu 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) { // no window is focused so there is no need to update the menu return; } browser.contextMenus.update(String(windowId), {title}).catch(error => { console.log('Error:', error); }); } onWindowFocusChanged(eventName, 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);