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