mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-30 05:35:31 +00:00
176 lines
5.4 KiB
JavaScript
176 lines
5.4 KiB
JavaScript
/* 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/. */
|
|
|
|
const { Cu } = require("chrome");
|
|
|
|
const EventEmitter = require("devtools/toolkit/event-emitter");
|
|
const { Connection } = require("devtools/client/connection-manager");
|
|
const { Promise: promise } =
|
|
Cu.import("resource://gre/modules/Promise.jsm", {});
|
|
const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
|
|
const { devtools } =
|
|
Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
|
|
|
|
const _knownTabStores = new WeakMap();
|
|
|
|
let TabStore;
|
|
|
|
module.exports = TabStore = function(connection) {
|
|
// If we already know about this connection,
|
|
// let's re-use the existing store.
|
|
if (_knownTabStores.has(connection)) {
|
|
return _knownTabStores.get(connection);
|
|
}
|
|
|
|
_knownTabStores.set(connection, this);
|
|
|
|
EventEmitter.decorate(this);
|
|
|
|
this._resetStore();
|
|
|
|
this.destroy = this.destroy.bind(this);
|
|
this._onStatusChanged = this._onStatusChanged.bind(this);
|
|
|
|
this._connection = connection;
|
|
this._connection.once(Connection.Events.DESTROYED, this.destroy);
|
|
this._connection.on(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
|
|
this._onTabListChanged = this._onTabListChanged.bind(this);
|
|
this._onTabNavigated = this._onTabNavigated.bind(this);
|
|
this._onStatusChanged();
|
|
return this;
|
|
};
|
|
|
|
TabStore.prototype = {
|
|
|
|
destroy: function() {
|
|
if (this._connection) {
|
|
// While this.destroy is bound using .once() above, that event may not
|
|
// have occurred when the TabStore client calls destroy, so we
|
|
// manually remove it here.
|
|
this._connection.off(Connection.Events.DESTROYED, this.destroy);
|
|
this._connection.off(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
|
|
_knownTabStores.delete(this._connection);
|
|
this._connection = null;
|
|
}
|
|
},
|
|
|
|
_resetStore: function() {
|
|
this.response = null;
|
|
this.tabs = [];
|
|
this._selectedTab = null;
|
|
this._selectedTabTargetPromise = null;
|
|
},
|
|
|
|
_onStatusChanged: function() {
|
|
if (this._connection.status == Connection.Status.CONNECTED) {
|
|
// Watch for changes to remote browser tabs
|
|
this._connection.client.addListener("tabListChanged",
|
|
this._onTabListChanged);
|
|
this._connection.client.addListener("tabNavigated",
|
|
this._onTabNavigated);
|
|
this.listTabs();
|
|
} else {
|
|
if (this._connection.client) {
|
|
this._connection.client.removeListener("tabListChanged",
|
|
this._onTabListChanged);
|
|
this._connection.client.removeListener("tabNavigated",
|
|
this._onTabNavigated);
|
|
}
|
|
this._resetStore();
|
|
}
|
|
},
|
|
|
|
_onTabListChanged: function() {
|
|
this.listTabs();
|
|
},
|
|
|
|
_onTabNavigated: function(e, { from, title, url }) {
|
|
if (!this._selectedTab || from !== this._selectedTab.actor) {
|
|
return;
|
|
}
|
|
this._selectedTab.url = url;
|
|
this._selectedTab.title = title;
|
|
this.emit("navigate");
|
|
},
|
|
|
|
listTabs: function() {
|
|
if (!this._connection || !this._connection.client) {
|
|
return promise.reject(new Error("Can't listTabs, not connected."));
|
|
}
|
|
let deferred = promise.defer();
|
|
this._connection.client.listTabs(response => {
|
|
if (response.error) {
|
|
this._connection.disconnect();
|
|
deferred.reject(response.error);
|
|
return;
|
|
}
|
|
this.response = response;
|
|
this.tabs = response.tabs;
|
|
this._checkSelectedTab();
|
|
deferred.resolve(response);
|
|
});
|
|
return deferred.promise;
|
|
},
|
|
|
|
// TODO: Tab "selection" should really take place by creating a TabProject
|
|
// which is the selected project. This should be done as part of the
|
|
// project-agnostic work.
|
|
_selectedTab: null,
|
|
_selectedTabTargetPromise: null,
|
|
get selectedTab() {
|
|
return this._selectedTab;
|
|
},
|
|
set selectedTab(tab) {
|
|
if (this._selectedTab === tab) {
|
|
return;
|
|
}
|
|
this._selectedTab = tab;
|
|
this._selectedTabTargetPromise = null;
|
|
// Attach to the tab to follow navigation events
|
|
if (this._selectedTab) {
|
|
this.getTargetForTab();
|
|
}
|
|
},
|
|
|
|
_checkSelectedTab: function() {
|
|
if (!this._selectedTab) {
|
|
return;
|
|
}
|
|
let alive = this.tabs.some(tab => {
|
|
return tab.actor === this._selectedTab.actor;
|
|
});
|
|
if (!alive) {
|
|
this._selectedTab = null;
|
|
this._selectedTabTargetPromise = null;
|
|
this.emit("closed");
|
|
}
|
|
},
|
|
|
|
getTargetForTab: function() {
|
|
if (this._selectedTabTargetPromise) {
|
|
return this._selectedTabTargetPromise;
|
|
}
|
|
let store = this;
|
|
this._selectedTabTargetPromise = Task.spawn(function*() {
|
|
// If you connect to a tab, then detach from it, the root actor may have
|
|
// de-listed the actors that belong to the tab. This breaks the toolbox
|
|
// if you try to connect to the same tab again. To work around this
|
|
// issue, we force a "listTabs" request before connecting to a tab.
|
|
yield store.listTabs();
|
|
return devtools.TargetFactory.forRemoteTab({
|
|
form: store._selectedTab,
|
|
client: store._connection.client,
|
|
chrome: false
|
|
});
|
|
});
|
|
this._selectedTabTargetPromise.then(target => {
|
|
target.once("close", () => {
|
|
this._selectedTabTargetPromise = null;
|
|
});
|
|
});
|
|
return this._selectedTabTargetPromise;
|
|
},
|
|
|
|
};
|