flexdesk 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -3
- package/dist/wm.js +180 -36
- package/dist/wm.js.map +3 -3
- package/package.json +1 -1
- package/src/tiling/command_palette.js +15 -3
- package/src/tiling/desktops.js +71 -2
- package/src/tiling/shell.js +62 -16
- package/src/tiling/wm.js +52 -12
package/README.md
CHANGED
|
@@ -130,7 +130,36 @@ await createShell({
|
|
|
130
130
|
});
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
-
###
|
|
133
|
+
### Your desktops beside the app's (0.10.0)
|
|
134
|
+
|
|
135
|
+
Pass `chrome.desktops` and the shell paints a desktop bar: a chip per desktop
|
|
136
|
+
and a `+`. An app that keeps desktops of its own (one per page, say) marks them
|
|
137
|
+
on the desktop objects themselves:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
const shell = await createShell({ /* … */ eventBus,
|
|
141
|
+
chrome: { desktops: barEl }, desktopBar: { switchOnAdd: true } });
|
|
142
|
+
shell.wm.desktops.ensureCount(pages.length);
|
|
143
|
+
shell.wm.desktops.desktops.slice(0, pages.length)
|
|
144
|
+
.forEach((d) => { d.group = 'Workbench'; d.fixed = true; });
|
|
145
|
+
eventBus.emit('wm:changed', {}); // repaint the bar
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Desktops that share a **`group`** are one chip, labelled by the group, and one
|
|
149
|
+
stop for Ctrl+Left/Right; the chip goes back to the member that was on screen
|
|
150
|
+
last (`wm.desktops.chips()` says what the bar draws). A **`fixed`** desktop is
|
|
151
|
+
the app's: the bar offers no rename and no delete, no menu offers it as a
|
|
152
|
+
*Move to desktop* target, and `removeDesktop`, `moveFocusedToDesktop` and
|
|
153
|
+
`moveWindowToDesktop` refuse it. Both are persisted. A desktop the user adds is
|
|
154
|
+
numbered among the ungrouped ones, and `desktopBar.switchOnAdd` puts it on
|
|
155
|
+
screen. With neither set anywhere, the bar is what it always was.
|
|
156
|
+
|
|
157
|
+
*Move to desktop* now carries every tab of the pane and adds them to the target's
|
|
158
|
+
main pane; it used to replace that pane with the active tab alone. A target still
|
|
159
|
+
holding only its seed is replaced, as before. `dispose()` removes the bar, so a
|
|
160
|
+
rebuilt shell paints a fresh one.
|
|
161
|
+
|
|
162
|
+
### The palette that finds anything (0.9.0; matching tightened in 0.9.1)
|
|
134
163
|
|
|
135
164
|
Ctrl+K's grouped layout. Every option is off unless you pass it, so a shell that
|
|
136
165
|
passes none of them keeps the chip strip, the panel toggles and the flat list:
|
|
@@ -158,8 +187,10 @@ A row may carry `context` (drawn, not searched), `keywords` (searched, not
|
|
|
158
187
|
drawn), `modes` (which of `split`/`window` its keys offer), `drillLabel` (the Tab
|
|
159
188
|
hint), `boost`, `group`/`groupLabel`, `kindLabel`, `shortcut` and `verb`. A group's
|
|
160
189
|
header is the taxonomy's `plural` for the kind, else its `label`. Matching ranks
|
|
161
|
-
exact, prefix, word starts (`ord li` finds *Order lines*), substring, then
|
|
162
|
-
|
|
190
|
+
exact, prefix, word starts (`ord li` finds *Order lines*), substring, then an
|
|
191
|
+
abbreviation: it starts at a word, and the letters it skips outside word starts are
|
|
192
|
+
at most half the query (`cstmr` finds *customer*; `cour` does not find *Scroll up
|
|
193
|
+
to*). A drill's `search` is debounced, and a stale answer is aborted and
|
|
163
194
|
never drawn. `palette.invalidate()` drops the cache.
|
|
164
195
|
|
|
165
196
|
**`chrome.zoom`** paints a − / track / + / readout control into the element you
|
package/dist/wm.js
CHANGED
|
@@ -698,6 +698,7 @@ function _match(text, query) {
|
|
|
698
698
|
const sub = t.indexOf(q);
|
|
699
699
|
if (sub >= 0) return { s: 60, hits: span(sub, q.length) };
|
|
700
700
|
const flat = q.replace(/\s+/g, "");
|
|
701
|
+
const startsWord = (i) => i === 0 || _WORD_BREAK.test(t[i - 1]);
|
|
701
702
|
const hits = [];
|
|
702
703
|
let j = 0;
|
|
703
704
|
for (let i = 0; i < t.length && j < flat.length; i++) {
|
|
@@ -706,9 +707,13 @@ function _match(text, query) {
|
|
|
706
707
|
j++;
|
|
707
708
|
}
|
|
708
709
|
}
|
|
709
|
-
if (j < flat.length) return null;
|
|
710
|
-
|
|
711
|
-
|
|
710
|
+
if (j < flat.length || !startsWord(hits[0])) return null;
|
|
711
|
+
let spread = 0;
|
|
712
|
+
for (let k = 1; k < hits.length; k++) {
|
|
713
|
+
const gap = hits[k] - hits[k - 1] - 1;
|
|
714
|
+
if (gap > 0 && !startsWord(hits[k])) spread += gap;
|
|
715
|
+
}
|
|
716
|
+
if (spread > Math.floor(flat.length / 2)) return null;
|
|
712
717
|
return { s: Math.max(1, 30 - spread), hits };
|
|
713
718
|
}
|
|
714
719
|
function _mark(text, hits) {
|
|
@@ -1654,7 +1659,10 @@ function _makeDesktop(label, seed, panelDefaults = DEFAULT_PANEL_STATE) {
|
|
|
1654
1659
|
// Boot default: whatever the embedder asked for, left + right + bottom
|
|
1655
1660
|
// when it asked for nothing. Names are assigned by wm._canonicalize via
|
|
1656
1661
|
// PANEL_TITLES.
|
|
1657
|
-
panels: { ...panelDefaults }
|
|
1662
|
+
panels: { ...panelDefaults },
|
|
1663
|
+
// C40. Both unset is a desktop exactly as it always was.
|
|
1664
|
+
group: null,
|
|
1665
|
+
fixed: false
|
|
1658
1666
|
};
|
|
1659
1667
|
}
|
|
1660
1668
|
var DesktopManager = class _DesktopManager {
|
|
@@ -1673,6 +1681,7 @@ var DesktopManager = class _DesktopManager {
|
|
|
1673
1681
|
this.panelDefaults = { ...DEFAULT_PANEL_STATE, ...panelDefaults || {} };
|
|
1674
1682
|
this.desktops = [_makeDesktop("1", seed, this.panelDefaults)];
|
|
1675
1683
|
this.activeIdx = 0;
|
|
1684
|
+
this.lastInGroup = /* @__PURE__ */ new Map();
|
|
1676
1685
|
}
|
|
1677
1686
|
active() {
|
|
1678
1687
|
return this.desktops[this.activeIdx];
|
|
@@ -1681,8 +1690,76 @@ var DesktopManager = class _DesktopManager {
|
|
|
1681
1690
|
if (idx < 0 || idx >= this.desktops.length) return false;
|
|
1682
1691
|
if (idx === this.activeIdx) return false;
|
|
1683
1692
|
this.activeIdx = idx;
|
|
1693
|
+
const d = this.desktops[idx];
|
|
1694
|
+
if (d.group) this.lastInGroup.set(d.group, d.id);
|
|
1684
1695
|
return true;
|
|
1685
1696
|
}
|
|
1697
|
+
/**
|
|
1698
|
+
* ══ C40. WHAT THE DESKTOP BAR DRAWS — ONE CHIP PER GROUP ═══════════════
|
|
1699
|
+
*
|
|
1700
|
+
* An embedder may give desktops a `group` and mark them `fixed`. Desktops
|
|
1701
|
+
* that share a group are ONE stop for the user — one chip in the bar, one
|
|
1702
|
+
* step of Ctrl+Left/Right — and the chip goes back to whichever member was
|
|
1703
|
+
* last on screen; which member that is, is the embedder's business (Tables
|
|
1704
|
+
* keeps one desktop per page under a "Workbench" group, and the rail picks
|
|
1705
|
+
* among them). A `fixed` desktop is the embedder's own: the bar offers no
|
|
1706
|
+
* rename and no delete for it, and no menu offers it as a "Move to"
|
|
1707
|
+
* destination.
|
|
1708
|
+
*
|
|
1709
|
+
* With no group set anywhere this is one chip per desktop, in order — the
|
|
1710
|
+
* bar every existing embedder has.
|
|
1711
|
+
*
|
|
1712
|
+
* @returns {Array<{ idx: number, label: string, group: string|null,
|
|
1713
|
+
* fixed: boolean, active: boolean, members: number[] }>} `idx` is the
|
|
1714
|
+
* desktop the chip switches to.
|
|
1715
|
+
*/
|
|
1716
|
+
chips() {
|
|
1717
|
+
const out = [];
|
|
1718
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
1719
|
+
this.desktops.forEach((d, i) => {
|
|
1720
|
+
if (!d.group) {
|
|
1721
|
+
out.push({
|
|
1722
|
+
idx: i,
|
|
1723
|
+
label: d.label,
|
|
1724
|
+
group: null,
|
|
1725
|
+
fixed: !!d.fixed,
|
|
1726
|
+
active: i === this.activeIdx,
|
|
1727
|
+
members: [i]
|
|
1728
|
+
});
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
let chip = byGroup.get(d.group);
|
|
1732
|
+
if (!chip) {
|
|
1733
|
+
chip = {
|
|
1734
|
+
idx: i,
|
|
1735
|
+
label: d.group,
|
|
1736
|
+
group: d.group,
|
|
1737
|
+
fixed: true,
|
|
1738
|
+
active: false,
|
|
1739
|
+
members: []
|
|
1740
|
+
};
|
|
1741
|
+
byGroup.set(d.group, chip);
|
|
1742
|
+
out.push(chip);
|
|
1743
|
+
}
|
|
1744
|
+
chip.members.push(i);
|
|
1745
|
+
if (i === this.activeIdx) chip.active = true;
|
|
1746
|
+
});
|
|
1747
|
+
for (const chip of byGroup.values()) {
|
|
1748
|
+
if (chip.active) {
|
|
1749
|
+
chip.idx = this.activeIdx;
|
|
1750
|
+
continue;
|
|
1751
|
+
}
|
|
1752
|
+
const last = this.lastInGroup.get(chip.group);
|
|
1753
|
+
const at = last ? this.desktops.findIndex((d) => d.id === last) : -1;
|
|
1754
|
+
if (at >= 0 && this.desktops[at].group === chip.group) chip.idx = at;
|
|
1755
|
+
}
|
|
1756
|
+
return out;
|
|
1757
|
+
}
|
|
1758
|
+
/** C40. Whether any desktop belongs to a group — the bar's cue to stop
|
|
1759
|
+
* promising "Alt+N" on its chips, which then no longer count desktops. */
|
|
1760
|
+
hasGroups() {
|
|
1761
|
+
return this.desktops.some((d) => !!d.group);
|
|
1762
|
+
}
|
|
1686
1763
|
ensureCount(n) {
|
|
1687
1764
|
while (this.desktops.length < n) {
|
|
1688
1765
|
this.desktops.push(_makeDesktop(
|
|
@@ -1693,11 +1770,8 @@ var DesktopManager = class _DesktopManager {
|
|
|
1693
1770
|
}
|
|
1694
1771
|
}
|
|
1695
1772
|
addDesktop(label = null) {
|
|
1696
|
-
const
|
|
1697
|
-
|
|
1698
|
-
this.seed,
|
|
1699
|
-
this.panelDefaults
|
|
1700
|
-
);
|
|
1773
|
+
const n = this.desktops.filter((d2) => !d2.group).length + 1;
|
|
1774
|
+
const d = _makeDesktop(label || String(n), this.seed, this.panelDefaults);
|
|
1701
1775
|
this.desktops.push(d);
|
|
1702
1776
|
return d;
|
|
1703
1777
|
}
|
|
@@ -1708,7 +1782,11 @@ var DesktopManager = class _DesktopManager {
|
|
|
1708
1782
|
id: d.id,
|
|
1709
1783
|
label: d.label,
|
|
1710
1784
|
tree: d.tree.serialize(),
|
|
1711
|
-
panels: { ...d.panels }
|
|
1785
|
+
panels: { ...d.panels },
|
|
1786
|
+
// C40. Written only when set, so a blob without them is
|
|
1787
|
+
// byte-for-byte the blob it always was.
|
|
1788
|
+
...d.group ? { group: d.group } : {},
|
|
1789
|
+
...d.fixed ? { fixed: true } : {}
|
|
1712
1790
|
// windows serialized at WM level since we don't own
|
|
1713
1791
|
// managed-window state in this module.
|
|
1714
1792
|
}))
|
|
@@ -1726,12 +1804,16 @@ var DesktopManager = class _DesktopManager {
|
|
|
1726
1804
|
// closed that panel, and re-opening it on every reload is the bug
|
|
1727
1805
|
// this merge order avoids. The default only fills a key the stored
|
|
1728
1806
|
// blob predates.
|
|
1729
|
-
panels: { ...m.panelDefaults, ...raw.panels || {} }
|
|
1807
|
+
panels: { ...m.panelDefaults, ...raw.panels || {} },
|
|
1808
|
+
group: raw.group || null,
|
|
1809
|
+
fixed: !!raw.fixed
|
|
1730
1810
|
}));
|
|
1731
1811
|
for (const d of m.desktops) {
|
|
1732
1812
|
if (!d.tree.rootId) d.tree.setRoot(makeLeaf(seed()));
|
|
1733
1813
|
}
|
|
1734
1814
|
m.activeIdx = Math.min(Math.max(0, blob.activeIdx | 0), m.desktops.length - 1);
|
|
1815
|
+
const active = m.desktops[m.activeIdx];
|
|
1816
|
+
if (active.group) m.lastInGroup.set(active.group, active.id);
|
|
1735
1817
|
return m;
|
|
1736
1818
|
}
|
|
1737
1819
|
};
|
|
@@ -6681,6 +6763,7 @@ var WindowManager = class _WindowManager {
|
|
|
6681
6763
|
const rec = this._windowToLeaf.get(windowId);
|
|
6682
6764
|
if (!rec) return;
|
|
6683
6765
|
if (rec.desktopIdx === targetIdx) return;
|
|
6766
|
+
if (this.desktops.desktops[targetIdx]?.fixed) return;
|
|
6684
6767
|
this.desktops.ensureCount(targetIdx + 1);
|
|
6685
6768
|
rec.desktopIdx = targetIdx;
|
|
6686
6769
|
if (!rec.homeContainer && rec.homeLeafId) {
|
|
@@ -6811,10 +6894,10 @@ var WindowManager = class _WindowManager {
|
|
|
6811
6894
|
const items = [
|
|
6812
6895
|
{ label: "Back to tile", icon: "close_fullscreen", action: "back" }
|
|
6813
6896
|
];
|
|
6814
|
-
|
|
6897
|
+
const targets = [...this.desktops.desktops.entries()].filter(([i, d]) => i !== rec.desktopIdx && !d.fixed);
|
|
6898
|
+
if (targets.length) {
|
|
6815
6899
|
items.push({ separator: true });
|
|
6816
|
-
for (const [i, d] of
|
|
6817
|
-
if (i === rec.desktopIdx) continue;
|
|
6900
|
+
for (const [i, d] of targets) {
|
|
6818
6901
|
items.push({
|
|
6819
6902
|
label: `Move to desktop ${d.label}`,
|
|
6820
6903
|
icon: "sweep",
|
|
@@ -7108,20 +7191,34 @@ var WindowManager = class _WindowManager {
|
|
|
7108
7191
|
this._persist();
|
|
7109
7192
|
this._notifyChange();
|
|
7110
7193
|
}
|
|
7111
|
-
|
|
7194
|
+
/** Add a desktop. C40: `{ switchTo: true }` also puts it on screen, which
|
|
7195
|
+
* is what the bar's `+` does under `createShell({ desktopBar: {
|
|
7196
|
+
* switchOnAdd: true } })`. Answers the new desktop's index. */
|
|
7197
|
+
addDesktop({ switchTo = false } = {}) {
|
|
7112
7198
|
this.desktops.addDesktop();
|
|
7199
|
+
const idx = this.desktops.desktops.length - 1;
|
|
7200
|
+
if (switchTo) {
|
|
7201
|
+
this.switchDesktop(idx);
|
|
7202
|
+
return idx;
|
|
7203
|
+
}
|
|
7113
7204
|
this._notifyChange();
|
|
7114
7205
|
this._persist();
|
|
7206
|
+
return idx;
|
|
7115
7207
|
}
|
|
7116
7208
|
/** Step to the previous (-1) or next (+1) existing desktop, wrapping
|
|
7117
7209
|
* around. No-op with a single desktop. Iterates only desktops that
|
|
7118
|
-
* already exist — use switchDesktop(idx) to create-on-demand.
|
|
7210
|
+
* already exist — use switchDesktop(idx) to create-on-demand.
|
|
7211
|
+
*
|
|
7212
|
+
* C40. It steps between the bar's CHIPS, so a group is one stop and
|
|
7213
|
+
* lands on its member last on screen. Ungrouped, a chip is a desktop
|
|
7214
|
+
* and this is the step it always was. */
|
|
7119
7215
|
cycleDesktop(dir) {
|
|
7120
|
-
const
|
|
7121
|
-
const n =
|
|
7216
|
+
const chips = this.desktops.chips();
|
|
7217
|
+
const n = chips.length;
|
|
7122
7218
|
if (n <= 1) return;
|
|
7123
|
-
const
|
|
7124
|
-
|
|
7219
|
+
const at = chips.findIndex((c) => c.active);
|
|
7220
|
+
const next = chips[(at + (dir < 0 ? -1 : 1) + n) % n];
|
|
7221
|
+
this.switchDesktop(next.idx);
|
|
7125
7222
|
}
|
|
7126
7223
|
/** Cycle the focused tile's main-panel tabs (Ctrl+Tab / Ctrl+Shift+Tab).
|
|
7127
7224
|
* Wraps; no-op when the focused leaf has 0 or 1 tabs. */
|
|
@@ -7158,6 +7255,7 @@ var WindowManager = class _WindowManager {
|
|
|
7158
7255
|
const m = this.desktops;
|
|
7159
7256
|
if (m.desktops.length <= 1) return false;
|
|
7160
7257
|
if (idx < 0 || idx >= m.desktops.length) return false;
|
|
7258
|
+
if (m.desktops[idx].fixed) return false;
|
|
7161
7259
|
m.desktops.splice(idx, 1);
|
|
7162
7260
|
if (idx < m.activeIdx) m.activeIdx -= 1;
|
|
7163
7261
|
if (m.activeIdx >= m.desktops.length) m.activeIdx = m.desktops.length - 1;
|
|
@@ -7183,12 +7281,31 @@ var WindowManager = class _WindowManager {
|
|
|
7183
7281
|
if (!focused || !focused.content) return;
|
|
7184
7282
|
const closeChrome = this.renderer?.leafChrome?.(focused.id)?.close;
|
|
7185
7283
|
if (closeChrome === false || closeChrome?.disabled === true) return;
|
|
7186
|
-
|
|
7284
|
+
if (this.desktops.desktops[idx]?.fixed) return;
|
|
7187
7285
|
this.desktops.ensureCount(idx + 1);
|
|
7188
7286
|
const target = this.desktops.desktops[idx];
|
|
7287
|
+
const tabs = Array.isArray(focused.tabs) && focused.tabs.length ? focused.tabs : [{ kind: focused.content.kind, props: focused.content.props, title: focused.title }];
|
|
7288
|
+
const activeTab = Math.max(0, Math.min(tabs.length - 1, focused.activeTabIdx || 0));
|
|
7189
7289
|
const targetTree = target.tree;
|
|
7190
7290
|
const primary = targetTree.primaryLeafId();
|
|
7191
|
-
if (primary)
|
|
7291
|
+
if (primary) {
|
|
7292
|
+
const dest = targetTree.get(primary);
|
|
7293
|
+
const seedKind = this._rootLeaf()?.content?.kind;
|
|
7294
|
+
const blank = (dest.tabs || []).length <= 1 && dest.content?.kind === seedKind;
|
|
7295
|
+
let first = -1;
|
|
7296
|
+
tabs.forEach((tab, i) => {
|
|
7297
|
+
const content = { kind: tab.kind, props: tab.props || {} };
|
|
7298
|
+
const title = tab.title || tab.kind || "";
|
|
7299
|
+
if (i === 0 && blank) {
|
|
7300
|
+
targetTree.setLeafContent(primary, content, title);
|
|
7301
|
+
first = 0;
|
|
7302
|
+
return;
|
|
7303
|
+
}
|
|
7304
|
+
const at = targetTree.appendLeafTab(primary, content, title);
|
|
7305
|
+
if (first < 0) first = at;
|
|
7306
|
+
});
|
|
7307
|
+
if (first >= 0) targetTree.setActiveLeafTab(primary, first + activeTab);
|
|
7308
|
+
}
|
|
7192
7309
|
tree.close(focused.id);
|
|
7193
7310
|
if (!tree.rootId) tree.setRoot(makeLeaf(this._rootLeaf()));
|
|
7194
7311
|
if (!tree.leaves().some((l) => !String(l.content?.kind || "").startsWith("panel:"))) {
|
|
@@ -8158,6 +8275,7 @@ async function createShell({
|
|
|
8158
8275
|
floatActiveTab = false,
|
|
8159
8276
|
tabLayout = null,
|
|
8160
8277
|
newTabButton = true,
|
|
8278
|
+
desktopBar = {},
|
|
8161
8279
|
chrome = {},
|
|
8162
8280
|
// An embedder that moved its sections out of the top bar — into an icon
|
|
8163
8281
|
// rail, say — passes the selector its own buttons match, and F1..F8 keep
|
|
@@ -8229,7 +8347,7 @@ async function createShell({
|
|
|
8229
8347
|
const disposeKeymap = installKeymap({ wm, palette, navSelector });
|
|
8230
8348
|
const paletteBtn = mountPaletteButton(chrome.paletteButton, palette);
|
|
8231
8349
|
topNavEl = mountTopNav(chrome.topNav, taxonomy, wm);
|
|
8232
|
-
desktopsEl = mountDesktopBar(chrome.desktops, wm);
|
|
8350
|
+
desktopsEl = mountDesktopBar(chrome.desktops, wm, desktopBar || {});
|
|
8233
8351
|
const zoom = mountZoomControl(chrome.zoom, { root, host });
|
|
8234
8352
|
if (zoom) await zoom.ready;
|
|
8235
8353
|
toggles = bindPanelToggles(chrome.panelToggles, wm);
|
|
@@ -8280,6 +8398,10 @@ async function createShell({
|
|
|
8280
8398
|
zoom?.dispose();
|
|
8281
8399
|
} catch {
|
|
8282
8400
|
}
|
|
8401
|
+
try {
|
|
8402
|
+
desktopsEl?.remove();
|
|
8403
|
+
} catch {
|
|
8404
|
+
}
|
|
8283
8405
|
}
|
|
8284
8406
|
});
|
|
8285
8407
|
}
|
|
@@ -8354,7 +8476,7 @@ function syncPanelToggles(map, wm) {
|
|
|
8354
8476
|
btn.classList.toggle("twm-panel-toggle-btn--on", wm.isPanelOpen(side));
|
|
8355
8477
|
}
|
|
8356
8478
|
}
|
|
8357
|
-
function mountDesktopBar(hostEl, wm) {
|
|
8479
|
+
function mountDesktopBar(hostEl, wm, { switchOnAdd = false } = {}) {
|
|
8358
8480
|
if (!hostEl) return null;
|
|
8359
8481
|
let el = hostEl.querySelector("#twm-desktops");
|
|
8360
8482
|
if (!el) {
|
|
@@ -8368,15 +8490,16 @@ function mountDesktopBar(hostEl, wm) {
|
|
|
8368
8490
|
if (!btn) return;
|
|
8369
8491
|
const v = btn.dataset.desktop;
|
|
8370
8492
|
if (v === "+") {
|
|
8371
|
-
wm.addDesktop();
|
|
8493
|
+
wm.addDesktop({ switchTo: switchOnAdd });
|
|
8372
8494
|
syncDesktopBar(el, wm);
|
|
8373
8495
|
return;
|
|
8374
8496
|
}
|
|
8375
8497
|
wm.switchDesktop(Number(v));
|
|
8376
8498
|
});
|
|
8499
|
+
const isOwn = (btn) => btn.dataset.desktopFixed !== "1";
|
|
8377
8500
|
el.addEventListener("dblclick", async (e) => {
|
|
8378
8501
|
const btn = e.target.closest("[data-desktop]");
|
|
8379
|
-
if (!btn || btn.dataset.desktop === "+") return;
|
|
8502
|
+
if (!btn || btn.dataset.desktop === "+" || !isOwn(btn)) return;
|
|
8380
8503
|
e.preventDefault();
|
|
8381
8504
|
const idx = Number(btn.dataset.desktop);
|
|
8382
8505
|
const d = wm.desktops.desktops[idx];
|
|
@@ -8405,25 +8528,28 @@ function mountDesktopBar(hostEl, wm) {
|
|
|
8405
8528
|
const idx = Number(btn.dataset.desktop);
|
|
8406
8529
|
const m = wm.desktops;
|
|
8407
8530
|
const d = m.desktops[idx];
|
|
8408
|
-
const
|
|
8409
|
-
|
|
8531
|
+
const own = isOwn(btn);
|
|
8532
|
+
const canDelete = own && m.desktops.length > 1;
|
|
8533
|
+
const name = btn.dataset.desktopLabel || d.label;
|
|
8534
|
+
const items = [
|
|
8410
8535
|
{
|
|
8411
|
-
label: `Switch to ${
|
|
8536
|
+
label: `Switch to ${name}`,
|
|
8412
8537
|
icon: "desktop_windows",
|
|
8413
8538
|
action: "switch",
|
|
8414
8539
|
disabled: idx === m.activeIdx
|
|
8415
8540
|
},
|
|
8416
|
-
{ label: "Rename\u2026", icon: "edit", action: "rename" },
|
|
8541
|
+
...own ? [{ label: "Rename\u2026", icon: "edit", action: "rename" }] : [],
|
|
8417
8542
|
{ separator: true },
|
|
8418
8543
|
{ label: "New desktop", icon: "add", action: "add" },
|
|
8419
|
-
{
|
|
8544
|
+
...own ? [{
|
|
8420
8545
|
label: "Delete desktop",
|
|
8421
8546
|
icon: "delete",
|
|
8422
8547
|
action: "delete",
|
|
8423
8548
|
danger: true,
|
|
8424
8549
|
disabled: !canDelete
|
|
8425
|
-
}
|
|
8426
|
-
]
|
|
8550
|
+
}] : []
|
|
8551
|
+
];
|
|
8552
|
+
showContextMenu(e.clientX, e.clientY, items, async (action) => {
|
|
8427
8553
|
if (action === "switch") wm.switchDesktop(idx);
|
|
8428
8554
|
else if (action === "rename") {
|
|
8429
8555
|
const result = await openForm({
|
|
@@ -8437,7 +8563,7 @@ function mountDesktopBar(hostEl, wm) {
|
|
|
8437
8563
|
syncDesktopBar(el, wm);
|
|
8438
8564
|
wm._persist();
|
|
8439
8565
|
} else if (action === "add") {
|
|
8440
|
-
wm.addDesktop();
|
|
8566
|
+
wm.addDesktop({ switchTo: switchOnAdd });
|
|
8441
8567
|
syncDesktopBar(el, wm);
|
|
8442
8568
|
} else if (action === "delete") {
|
|
8443
8569
|
wm.removeDesktop(idx);
|
|
@@ -8450,13 +8576,31 @@ function mountDesktopBar(hostEl, wm) {
|
|
|
8450
8576
|
function syncDesktopBar(el, wm) {
|
|
8451
8577
|
if (!el) return;
|
|
8452
8578
|
const m = wm.desktops;
|
|
8453
|
-
|
|
8579
|
+
if (!m.hasGroups()) {
|
|
8580
|
+
el.innerHTML = m.desktops.map((d, i) => `
|
|
8454
8581
|
<button class="twm-desk__btn${i === m.activeIdx ? " twm-desk__btn--on" : ""}"
|
|
8455
8582
|
data-desktop="${i}"
|
|
8456
8583
|
title="Desktop ${d.label} (Alt+${i + 1}) \xB7 right-click to delete">
|
|
8457
8584
|
${d.label}
|
|
8458
8585
|
</button>`).join("") + `<button class="twm-desk__btn twm-desk__btn--add"
|
|
8459
8586
|
data-desktop="+" title="Add desktop">+</button>`;
|
|
8587
|
+
return;
|
|
8588
|
+
}
|
|
8589
|
+
el.innerHTML = m.chips().map((c) => {
|
|
8590
|
+
const label = _escapeHtml(c.label);
|
|
8591
|
+
const title = c.fixed ? label : `${label} \xB7 double-click to rename \xB7 right-click for more`;
|
|
8592
|
+
return `
|
|
8593
|
+
<button class="twm-desk__btn${c.active ? " twm-desk__btn--on" : ""}"
|
|
8594
|
+
data-desktop="${c.idx}"
|
|
8595
|
+
data-desktop-label="${label}"${c.fixed ? ' data-desktop-fixed="1"' : ""}
|
|
8596
|
+
title="${title}">
|
|
8597
|
+
${label}
|
|
8598
|
+
</button>`;
|
|
8599
|
+
}).join("") + `<button class="twm-desk__btn twm-desk__btn--add"
|
|
8600
|
+
data-desktop="+" title="New desktop">+</button>`;
|
|
8601
|
+
}
|
|
8602
|
+
function _escapeHtml(s) {
|
|
8603
|
+
return String(s ?? "").replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ch]);
|
|
8460
8604
|
}
|
|
8461
8605
|
function _tileTabMenu(wm, leafId, x, y) {
|
|
8462
8606
|
const tree = wm.desktops.active().tree;
|
|
@@ -8534,7 +8678,7 @@ function _tileContextMenu(wm, leafId, x, y) {
|
|
|
8534
8678
|
];
|
|
8535
8679
|
if (wm.desktops.desktops.length > 1 && !isPanel) {
|
|
8536
8680
|
for (const [i, d] of wm.desktops.desktops.entries()) {
|
|
8537
|
-
if (i === wm.desktops.activeIdx) continue;
|
|
8681
|
+
if (i === wm.desktops.activeIdx || d.fixed) continue;
|
|
8538
8682
|
items.push({
|
|
8539
8683
|
label: `Move to desktop ${d.label}`,
|
|
8540
8684
|
icon: "sweep",
|