-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.js
More file actions
365 lines (320 loc) · 12.1 KB
/
main.js
File metadata and controls
365 lines (320 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
/* 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/. */
/* Move Tab extension: lets you move a tab from its context menu for when you
* want to move it so far that dragging it would be a drag. */
'use strict';
const MID_TOP = 'movetab_menu';
const MID_NEW_WINDOW = 'newwindow';
const MID_MARK = 'toggle-mark';
const MID_PREFIX_WINDOW = 'window:';
const MID_PREFIX_TAB = 'tab:';
const DEFAULT_PREFS = {
move: 'after',
active: 'keep',
};
// Marked tab IDs (marked as "this is a teleport target").
let marks = new Set();
// options.js reads prefs from here (so as to get the same defaults).
var prefs = DEFAULT_PREFS;
browser.storage.sync.get().then(loaded => Object.assign(prefs, loaded));
browser.storage.onChanged.addListener(changes => {
for (let key of Object.keys(changes)) {
prefs[key] = changes[key].newValue;
}
});
let menu_state = '';
makeMenuBase();
// Clear the menu and fill it with the static part that's always there. This
// is the state when the menu is not being shown yet, so that if the
// dynamically updated part is late, the user still sees the submenu arrow.
function makeMenuBase() {
if (menu_state === 'building') {
menu_state = 'want-base';
return;
}
browser.menus.removeAll();
browser.menus.create({
id: MID_TOP,
title: browser.i18n.getMessage("menu@label"),
contexts: ['tab'],
});
for (let end of ['left', 'right']) {
browser.menus.create({
id: end,
parentId: MID_TOP,
title: browser.i18n.getMessage(`${end}@label`),
icons: {
"16": `${end}.svg`,
},
});
}
menu_state = 'base';
}
// Build the rest of the menu, assuming we start from makeMenuBase.
async function makeMenuRest(contextTab) {
if (menu_state === 'building') {
menu_state = 'want-full';
return;
}
if (menu_state !== 'base') makeMenuBase();
menu_state = 'building';
let markedTabs = await Promise.all(Array.from(marks.values())
.map(id => browser.tabs.get(id)));
markedTabs.sort((a, b) => a.index - b.index);
const windows = await browser.windows.getAll({windowTypes: ['normal']});
const currentWin = windows.find(win => win.focused);
if (!currentWin) return; // Eh, can't right-click until there's a window.
for (let tab of markedTabs) if (tab.windowId === currentWin.id) {
browser.menus.create({
id: MID_PREFIX_TAB + JSON.stringify(tab.id),
parentId: MID_TOP,
title: menuLabelToTab(tab.title),
enabled: contextTab.id !== tab.id,
});
}
browser.menus.create({
parentId: MID_TOP,
type: "separator",
});
for (let win of windows) {
if (win.focused) continue;
if (win.incognito !== currentWin.incognito) continue;
browser.menus.create({
id: MID_PREFIX_WINDOW + JSON.stringify(win.id),
parentId: MID_TOP,
title: browser.i18n.getMessage('to_window@pattern', win.title),
icons: {
"16": 'photon-window-16.svg',
},
});
for (let tab of markedTabs) if (tab.windowId === win.id) {
browser.menus.create({
id: MID_PREFIX_TAB + JSON.stringify(tab.id),
parentId: MID_TOP,
title: menuLabelToTab(tab.title),
});
}
}
browser.menus.create({
id: MID_NEW_WINDOW,
parentId: MID_TOP,
title: browser.i18n.getMessage('new_window@label'),
icons: {
"16": 'photon-window-new-16.svg',
},
});
browser.menus.create({
parentId: MID_TOP,
type: "separator",
});
let title, icons;
if (marks.has(contextTab.id)) {
title = browser.i18n.getMessage('mark_unset@label');
icons = {'16': 'unmark.svg'};
} else {
title = browser.i18n.getMessage('mark_set@label');
icons = {'16': 'mark.svg'};
}
browser.menus.create({
id: MID_MARK,
parentId: MID_TOP,
title,
icons,
});
// Note bug 1414566 - menus.update can't update the icon.
if (menu_state === 'want-base') {
makeMenuBase();
} else if (menu_state !== 'building') {
makeMenuRest(contextTab);
} else {
menu_state = 'full';
}
}
function menuLabelToTab(title) {
// The actual maximum length depends on the display width, which we don't
// know. Instead, a reasonable guess (at least for English) seems to be
// somewhere around 50-80 characters. Firefox does this too in
// gMenuBuilder.customizeElement.
const lenMax = 64;
const ellipsis = '\u2026'; // Ideally we'd get the pref intl.ellipsis.
let to_tab_pattern = `${prefs.move}_tab@pattern`;
let lenFixed = browser.i18n.getMessage(to_tab_pattern, '').length;
let lenAvailable = lenMax - lenFixed;
if (title.length <= lenAvailable) {
return browser.i18n.getMessage(to_tab_pattern, title);
}
return browser.i18n.getMessage(to_tab_pattern,
title.substr(0, lenAvailable - ellipsis.length) + ellipsis);
}
browser.menus.onShown.addListener(async function (info, tab) {
await makeMenuRest(tab);
browser.menus.refresh();
});
// When the menu is hidden, clear it back to a stub ready for the next onShown.
browser.menus.onHidden.addListener(makeMenuBase);
// When tabs are closed, remove them from our cache of marks. The mark can be
// restored if the tab is reopened.
browser.tabs.onRemoved.addListener(function (tabId) {
if (marks.has(tabId)) {
marks.delete(tabId);
}
});
// Listen for reopened tabs that were marked.
browser.tabs.onCreated.addListener(async function (tab) {
const isTarget = await browser.sessions.getTabValue(tab.id, 'target');
if (isTarget) {
setMark(tab);
}
});
// Since marking tabs happens in content (as a hack around the WebExtensions
// API not providing a real way to do it), we have to re-do it sometimes:
// - when the tab's title changes (e.g. the page updated itself, or the user
// navigated somewhere)
// - when the tab is restored after being unloaded (because if the tab was
// marked while unloaded, the script won't have run)
// Running addMark.js is idempotent, so it's okay if we are over-eager with it.
// Also the menu item needs to be updated, of course.
browser.tabs.onUpdated.addListener(function (tabId, updates, tab) {
// When loading an unloaded tab, the sequence of events is strange. The
// 'url' reported is still 'about:blank' when 'discarded' changes to false,
// and we can't run content scripts yet. So listen for 'url' changes too.
if (marks.has(tabId) && ('title' in updates
|| 'discarded' in updates
|| 'url' in updates)) {
browser.tabs.executeScript(tab.id,
{file: 'addMark.js', runAt: 'document_start'});
browser.menus.update(MID_PREFIX_TAB + JSON.stringify(tab.id),
{title: menuLabelToTab(tab.title)});
}
});
// When this extension is upgraded, refresh the cache of marked tabs.
browser.runtime.onInstalled.addListener(async function (details) {
if (details.reason == 'update') {
let tabs = await browser.tabs.query({});
let tabs_targets = await Promise.all(tabs.map(async tab =>
[tab, await browser.sessions.getTabValue(tab.id, 'target')]));
for (let [tab, isTarget] of tabs_targets) {
if (isTarget) {
setMark(tab);
}
}
}
});
browser.menus.onClicked.addListener(async function (info, tab) {
if (info.menuItemId === MID_MARK) {
// Toggle whether this tab is marked.
if (marks.has(tab.id)) {
clearMark(tab);
} else {
setMark(tab);
}
} else if (info.menuItemId === MID_NEW_WINDOW) {
browser.windows.create({tabId: tab.id});
} else if (info.menuItemId === 'right') {
if (tab.pinned) {
await browser.tabs.update(tab.id, {pinned: false});
}
moveTab(tab, tab.windowId, -1);
} else if (info.menuItemId === 'left') {
if (tab.pinned) {
await browser.tabs.update(tab.id, {pinned: false});
}
let index = await pinnedTabCount(tab.windowId);
moveTab(tab, tab.windowId, index);
} else if (info.menuItemId.startsWith(MID_PREFIX_WINDOW)) {
const windowId = JSON.parse(
info.menuItemId.slice(MID_PREFIX_WINDOW.length));
let index = -1;
if (tab.pinned) {
index = await pinnedTabCount(windowId);
}
moveTab(tab, windowId, index);
} else if (info.menuItemId.startsWith(MID_PREFIX_TAB)) {
const destTabId = JSON.parse(
info.menuItemId.slice(MID_PREFIX_TAB.length));
const destTab = await browser.tabs.get(destTabId);
if (tab.pinned != destTab.pinned) {
await browser.tabs.update(tab.id, {pinned: destTab.pinned});
}
// We know tab is not destTab because the menu item's disabled then.
let offset = prefs.move === 'after' ? 1 : 0;
if (tab.windowId === destTab.windowId
&& tab.index < destTab.index) {
offset -= 1;
}
moveTab(tab, destTab.windowId, destTab.index + offset);
}
});
// Since pinned tabs must all be at the start, this is also the index of the
// first non-pinned tab.
async function pinnedTabCount(windowId) {
let tabs = await browser.tabs.query({windowId, pinned: true});
return tabs.length;
}
async function moveTab(tab, destWindowId, destIndex) {
if (tab.active && prefs.active === 'next') {
// Activate the next tab before doing the move to avoid unnecessary
// tabbox scrolling.
let nextTab = await pickNextTab(tab);
await browser.tabs.update(nextTab.id, {active: true});
}
await browser.tabs.move(tab.id, {
windowId: destWindowId,
index: destIndex
});
if (tab.active && prefs.active === 'keep') {
browser.tabs.update(tab.id, {active: true});
browser.windows.update(destWindowId, {focused: true});
}
}
// Choose which tab to select next when prefs.active === 'next'.
// Return the tab object.
async function pickNextTab(tab) {
// Try the adjacent undiscarded tabs of the same pinnedness, preferring the
// one to the right.
let right = await findTab({
windowId: tab.windowId,
index: tab.index + 1,
pinned: tab.pinned,
});
if (right && !right.discarded) return right;
let left = await findTab({
windowId: tab.windowId,
index: tab.index - 1,
pinned: tab.pinned,
});
if (left && !left.discarded) return left;
// Give up on the 'undiscarded' criterion, again preferring the right.
if (right) return right;
if (left) return left;
// This tab is the last tab of the same pinnedness in its window, so go for
// the adjacent tab of the other pinnedness. If this is the only
// pinned tab, then it must be index 0 and the adjacent non-pinned tab is
// index 1. If this is the only non-pinned tab, the adjacent pinned tab is
// the rightmost pinned tab, which comes immediately before this tab.
let next = await findTab({
windowId: tab.windowId,
index: tab.pinned ? 1 : tab.index - 1,
});
if (next) return next;
// I guess that means this is the only tab in the window, so just stick
// with the orignal tab.
return tab;
}
async function findTab(queryInfo) {
let result = await browser.tabs.query(queryInfo);
return result.length ? result[0] : null;
}
function setMark(tab) {
marks.add(tab.id);
browser.sessions.setTabValue(tab.id, 'target', true);
browser.tabs.executeScript(tab.id,
{file: 'addMark.js', runAt: 'document_start'});
}
function clearMark(tab) {
marks.delete(tab.id);
browser.sessions.removeTabValue(tab.id, 'target');
browser.tabs.executeScript(tab.id,
{file: 'removeMark.js', runAt: 'document_start'});
}