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);