@jbrowse/app-core 5.0.0-beta.11 → 5.0.0-beta.14
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/esm/AppSession/AppSessionMixin.d.ts +110 -92
- package/esm/Assemblies/AssembliesMixin.d.ts +30 -0
- package/esm/HistoryManagement/index.d.ts +2 -0
- package/esm/HistoryManagement/index.js +2 -0
- package/esm/JBrowseConfig/RootConfiguration.d.ts +4 -10
- package/esm/JBrowseConfig/RootConfiguration.js +5 -8
- package/esm/JBrowseConfig/index.d.ts +3 -11
- package/esm/JBrowseConfig/index.js +2 -3
- package/esm/JBrowseModel/index.d.ts +10 -9
- package/esm/JBrowseModel/index.js +8 -7
- package/esm/JbApi/jbApi.d.ts +2 -2
- package/esm/JbApi/jbApi.js +16 -11
- package/esm/RootMenu/index.d.ts +2 -0
- package/esm/RootMenu/index.js +2 -0
- package/esm/SessionSpec/index.d.ts +2 -2
- package/esm/SessionSpec/index.js +1 -1
- package/esm/SessionSpec/loadSessionSpec.js +2 -12
- package/esm/SessionSpec/parseSessionSpecUrl.d.ts +19 -0
- package/esm/SessionSpec/parseSessionSpecUrl.js +29 -0
- package/esm/WorkspaceLayout/LayoutRenderer.d.ts +0 -10
- package/esm/WorkspaceLayout/LayoutRenderer.js +34 -101
- package/esm/WorkspaceLayout/PanelView.js +19 -45
- package/esm/WorkspaceLayout/TabStrip.d.ts +2 -2
- package/esm/WorkspaceLayout/TabStrip.js +36 -104
- package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +0 -9
- package/esm/WorkspaceLayout/WorkspaceContainer.js +9 -42
- package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +1 -16
- package/esm/WorkspaceLayout/WorkspacePanelActions.js +46 -51
- package/esm/WorkspaceLayout/WorkspaceTab.d.ts +3 -19
- package/esm/WorkspaceLayout/WorkspaceTab.js +12 -48
- package/esm/WorkspaceLayout/dropZone.d.ts +7 -37
- package/esm/WorkspaceLayout/dropZone.js +7 -44
- package/esm/WorkspaceLayout/model.d.ts +45 -120
- package/esm/WorkspaceLayout/model.js +145 -303
- package/esm/WorkspaceLayout/panelChrome.d.ts +4 -7
- package/esm/WorkspaceLayout/panelChrome.js +0 -2
- package/esm/WorkspaceLayout/spec.d.ts +10 -51
- package/esm/WorkspaceLayout/spec.js +12 -80
- package/esm/WorkspaceLayout/splitter.d.ts +3 -19
- package/esm/WorkspaceLayout/splitter.js +5 -21
- package/esm/WorkspaceLayout/tabName.d.ts +8 -0
- package/esm/WorkspaceLayout/tabName.js +14 -0
- package/esm/WorkspaceLayout/tree.d.ts +19 -79
- package/esm/WorkspaceLayout/tree.js +34 -131
- package/esm/WorkspaceLayout/useLayoutDrag.d.ts +4 -22
- package/esm/WorkspaceLayout/useLayoutDrag.js +55 -177
- package/esm/WorkspaceLayout/usePointerGesture.d.ts +24 -0
- package/esm/WorkspaceLayout/usePointerGesture.js +83 -0
- package/esm/WorkspaceLayout/workspaceTheme.d.ts +27 -0
- package/esm/WorkspaceLayout/workspaceTheme.js +37 -0
- package/esm/index.d.ts +5 -3
- package/esm/index.js +3 -2
- package/esm/queryParams.d.ts +2 -0
- package/esm/queryParams.js +16 -5
- package/esm/sessionSummary.d.ts +16 -0
- package/esm/sessionSummary.js +57 -0
- package/esm/ui/App/App.js +3 -0
- package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +3 -2
- package/esm/ui/App/DrawerHeaderHelpDialog.js +4 -2
- package/esm/ui/App/JBrowseTabMenu.d.ts +3 -2
- package/esm/ui/App/JBrowseTabMenu.js +5 -3
- package/esm/ui/App/ThemeModeButton.d.ts +4 -5
- package/esm/ui/App/ThemeModeButton.js +22 -14
- package/esm/ui/App/ViewContainer.js +7 -1
- package/esm/ui/App/ViewMenu.js +69 -42
- package/esm/ui/App/ViewWrapper.js +2 -2
- package/esm/ui/App/ViewsContainer.js +3 -4
- package/esm/ui/App/lazyParts.d.ts +0 -5
- package/esm/ui/App/lazyParts.js +0 -1
- package/esm/ui/App/preloadAppShell.js +3 -6
- package/esm/ui/App/types.d.ts +3 -2
- package/esm/ui/App/viewTitle.d.ts +6 -1
- package/esm/ui/App/viewTitle.js +7 -2
- package/package.json +7 -7
- package/esm/WorkspaceLayout/dockviewTheme.d.ts +0 -69
- package/esm/WorkspaceLayout/dockviewTheme.js +0 -82
- package/esm/ui/App/ClassicViewsContainer.d.ts +0 -6
- package/esm/ui/App/ClassicViewsContainer.js +0 -22
|
@@ -1,46 +1,20 @@
|
|
|
1
1
|
import { createElementId } from '@jbrowse/core/util/types/mst';
|
|
2
|
-
import { cast, getSnapshot, types } from '@jbrowse/mobx-state-tree';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { addDisposer, cast, getSnapshot, types } from '@jbrowse/mobx-state-tree';
|
|
3
|
+
import { autorun } from 'mobx';
|
|
4
|
+
import { resolveLayoutSpec, tileLayoutSpec, treeFromSpec, viewIdsInSpec, } from './spec.js';
|
|
5
|
+
import { addTab, addViewToTab, findTab, homeViews, moveTabToPanel, normalize, panelContainingView, panels, pruneEmptyPanel, pruneEmptyTab, removePanel, removeTab, removeView, renameTab, setActiveTab, setSizes, splitPanel, tabContainingView, tabs, } from './tree.js';
|
|
5
6
|
function sessionViewIds(self) {
|
|
6
7
|
return self.views?.map(v => v.id);
|
|
7
8
|
}
|
|
8
|
-
/**
|
|
9
|
-
* A caller-supplied view list narrowed to the views the session actually has.
|
|
10
|
-
*
|
|
11
|
-
* The list is the caller's snapshot, and `applyLayoutSpec` refuses an id no
|
|
12
|
-
* view has — so a view added after the list was built, or removed between
|
|
13
|
-
* building it and the call, would throw out of a sugar action no plugin wraps.
|
|
14
|
-
* The arrangement a stale name asks for is still the one it means for every
|
|
15
|
-
* view that IS there, so the name is dropped rather than the action. A repeat
|
|
16
|
-
* is dropped for the same reason: it would seat one view in two cells, which
|
|
17
|
-
* the resolver also refuses.
|
|
18
|
-
*/
|
|
19
|
-
function liveViewIds(self, allViewIds) {
|
|
20
|
-
const live = sessionViewIds(self);
|
|
21
|
-
return [
|
|
22
|
-
...new Set(live ? allViewIds.filter(id => live.includes(id)) : allViewIds),
|
|
23
|
-
];
|
|
24
|
-
}
|
|
25
9
|
/**
|
|
26
10
|
* #stateModel WorkspaceLayoutMixin
|
|
27
11
|
*
|
|
28
|
-
* The whole workspace
|
|
29
|
-
* nothing to reconcile, no event to echo, and no window during which the two
|
|
30
|
-
* disagree — which is the entire content of `useDockviewController`.
|
|
31
|
-
*
|
|
32
|
-
* Four levels, matching what the workspace actually has and what a generic
|
|
33
|
-
* window manager cannot quite express:
|
|
12
|
+
* The whole workspace as one MST tree:
|
|
34
13
|
*
|
|
35
14
|
* branch (a split) > panel (a grid cell) > tab > views (stacked)
|
|
36
15
|
*
|
|
37
|
-
* dockview models the first three as branch/group/panel and stops there; the
|
|
38
|
-
* vertical stack of views inside a tab is ours, which is why
|
|
39
|
-
* `panelViewAssignments` had to exist alongside dockview's own serialized grid.
|
|
40
|
-
* Here it is one tree, and a tab simply *contains* its views.
|
|
41
|
-
*
|
|
42
16
|
* Every action is `tree -> tree` through the pure functions in `tree.ts`, so
|
|
43
|
-
* undo is `applySnapshot` on this node
|
|
17
|
+
* undo is `applySnapshot` on this node.
|
|
44
18
|
*/
|
|
45
19
|
const LayoutTab = types.model('LayoutTab', {
|
|
46
20
|
id: types.identifier,
|
|
@@ -60,19 +34,13 @@ const LayoutBranch = types.model('LayoutBranch', {
|
|
|
60
34
|
direction: types.enumeration('LayoutDirection', ['row', 'column']),
|
|
61
35
|
children: types.array(types.late(() => LayoutNode)),
|
|
62
36
|
});
|
|
63
|
-
//
|
|
64
|
-
// contains branches. The dispatcher keys off `direction`, which only a branch
|
|
65
|
-
// has, so an ambiguous snapshot is impossible rather than merely unlikely.
|
|
37
|
+
// only a branch has `direction`, so the dispatcher is never ambiguous
|
|
66
38
|
const LayoutNode = types.union({
|
|
67
39
|
dispatcher: (snapshot) => snapshot.direction ? LayoutBranch : LayoutPanel,
|
|
68
40
|
}, LayoutBranch, LayoutPanel);
|
|
69
41
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* Random, NOT a counter. These are `types.identifier`, so they must be unique
|
|
73
|
-
* within the tree — and a counter is reset by every page load while the restored
|
|
74
|
-
* snapshot still holds `panel-1`, `tab-1`, .... The first tab a returning user
|
|
75
|
-
* opened would mint an id the tree already had.
|
|
42
|
+
* Random, not a counter: a counter restarts on page load while a restored
|
|
43
|
+
* snapshot still holds its old ids, and these are `types.identifier`s.
|
|
76
44
|
*/
|
|
77
45
|
function nextId(kind) {
|
|
78
46
|
return `${kind}-${createElementId()}`;
|
|
@@ -92,30 +60,17 @@ export function WorkspaceLayoutMixin() {
|
|
|
92
60
|
layout: types.optional(LayoutNode, emptyPanel),
|
|
93
61
|
activePanelId: types.maybe(types.string),
|
|
94
62
|
/**
|
|
95
|
-
* Show only this cell, at the size of the whole workspace.
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* node it would be inside `tree.ts`, the half that carries the risk and
|
|
99
|
-
* is proven by a randomised operation sequence asserting canonical form
|
|
100
|
-
* after every step — and every operation would then have to say what it
|
|
101
|
-
* does to the flag: a split of a maximized panel, a drag of its last tab
|
|
102
|
-
* out, a normalize that collapses it into its parent. Beside
|
|
103
|
-
* `activePanelId` it is the same class of thing as `activePanelId`,
|
|
104
|
-
* including its failure mode, which `livePanelIds` already repairs.
|
|
63
|
+
* Show only this cell, at the size of the whole workspace. Kept here
|
|
64
|
+
* rather than as a flag on `PanelNode` so no `tree.ts` operation has to
|
|
65
|
+
* say what it does to it.
|
|
105
66
|
*/
|
|
106
67
|
maximizedPanelId: types.maybe(types.string),
|
|
107
68
|
})
|
|
108
69
|
.views(self => ({
|
|
109
70
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* referentially stable — which also lets MST's reconcile short-circuit on
|
|
114
|
-
* identity when `apply` writes an untouched subtree back.
|
|
115
|
-
*
|
|
116
|
-
* Uncast on purpose: the models below and the interfaces in `tree.ts` are
|
|
117
|
-
* two spellings of one shape, and this assignment is the only thing that
|
|
118
|
-
* checks they agree.
|
|
71
|
+
* Referentially stable, since `getSnapshot` is cached. Uncast on
|
|
72
|
+
* purpose: this assignment is what checks the models above agree with
|
|
73
|
+
* the interfaces in `tree.ts`.
|
|
119
74
|
*/
|
|
120
75
|
get tree() {
|
|
121
76
|
return getSnapshot(self.layout);
|
|
@@ -140,24 +95,10 @@ export function WorkspaceLayoutMixin() {
|
|
|
140
95
|
panelContainingView(viewId) {
|
|
141
96
|
return panelContainingView(self.tree, viewId);
|
|
142
97
|
},
|
|
143
|
-
/** The views a tab renders: its members, in `session.views` order. */
|
|
144
|
-
viewIdsForTab(tabId, order) {
|
|
145
|
-
const members = new Set(findTab(self.tree, tabId)?.tab.viewIds ?? []);
|
|
146
|
-
return order.filter(id => members.has(id));
|
|
147
|
-
},
|
|
148
|
-
/** The tab a panel is showing, or its first. */
|
|
149
|
-
activeTabOf(panelId) {
|
|
150
|
-
const panel = panels(self.tree).find(p => p.id === panelId);
|
|
151
|
-
return panel ? activeTabIn(panel) : undefined;
|
|
152
|
-
},
|
|
153
98
|
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* share of its siblings, and CSS distributes free space by grow factor
|
|
158
|
-
* only up to a total of 1 — so a cell that was a third of a row, alone
|
|
159
|
-
* in the workspace with `flexGrow: 0.33`, draws a third of the window
|
|
160
|
-
* and leaves the rest blank.
|
|
99
|
+
* The maximized cell alone, or the whole tree. The cell is resized to 1
|
|
100
|
+
* because CSS hands out free space by grow factor only up to a total of
|
|
101
|
+
* 1, so a third-of-a-row cell would draw a third of the window.
|
|
161
102
|
*/
|
|
162
103
|
get visibleTree() {
|
|
163
104
|
const maximized = panels(self.tree).find(p => p.id === self.maximizedPanelId);
|
|
@@ -166,20 +107,9 @@ export function WorkspaceLayoutMixin() {
|
|
|
166
107
|
}))
|
|
167
108
|
.actions(self => {
|
|
168
109
|
/**
|
|
169
|
-
* Both panel ids
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* `maximizedPanelId` draws nothing at all.
|
|
173
|
-
*
|
|
174
|
-
* Stated as the invariant rather than as "the panel I just closed",
|
|
175
|
-
* because a removal collapses branches on the way out and the cell that
|
|
176
|
-
* disappears is not always the one that was named.
|
|
177
|
-
*
|
|
178
|
-
* They fall back differently, and the difference is the whole reason
|
|
179
|
-
* this is two lines rather than a loop. A workspace always shows some
|
|
180
|
-
* cell, so `activePanelId` takes the first one; maximize is a mode the
|
|
181
|
-
* user is IN, so losing its cell leaves the mode rather than picking an
|
|
182
|
-
* arbitrary cell to hold the user in it.
|
|
110
|
+
* Both panel ids held outside the tree must name a live cell. A lost
|
|
111
|
+
* active cell falls back to the first; a lost maximized cell leaves the
|
|
112
|
+
* mode.
|
|
183
113
|
*/
|
|
184
114
|
function livePanelIds() {
|
|
185
115
|
if (self.activePanelId !== undefined &&
|
|
@@ -191,27 +121,19 @@ export function WorkspaceLayoutMixin() {
|
|
|
191
121
|
self.maximizedPanelId = undefined;
|
|
192
122
|
}
|
|
193
123
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
* the shape that ends with one of them missing.
|
|
204
|
-
*/
|
|
124
|
+
// a maximized cell is the active one, so activating any other leaves
|
|
125
|
+
// the mode rather than putting new views where nobody can see them
|
|
126
|
+
function activate(panelId) {
|
|
127
|
+
self.activePanelId = panelId;
|
|
128
|
+
if (self.maximizedPanelId !== panelId) {
|
|
129
|
+
self.maximizedPanelId = undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Every write to the tree, so the one place `livePanelIds` runs. */
|
|
205
133
|
function apply(next) {
|
|
206
134
|
const before = panels(self.tree).length;
|
|
207
135
|
self.layout = cast(normalize(next));
|
|
208
|
-
//
|
|
209
|
-
// must not do, so gaining one leaves the mode. Stated as the count
|
|
210
|
-
// rather than at the three actions that split (`splitPanel`,
|
|
211
|
-
// `dropTabInNewSplit`, `moveViewToSplitRight`) for the reason above —
|
|
212
|
-
// and a fourth, `applyLayoutSpec`, arrives at it from the other side:
|
|
213
|
-
// it replaces every id, so `livePanelIds` was going to clear the mode
|
|
214
|
-
// regardless. Losing a cell needs nothing here; that IS `livePanelIds`.
|
|
136
|
+
// a new cell would appear where maximize hides it
|
|
215
137
|
if (panels(self.tree).length > before) {
|
|
216
138
|
self.maximizedPanelId = undefined;
|
|
217
139
|
}
|
|
@@ -221,68 +143,46 @@ export function WorkspaceLayoutMixin() {
|
|
|
221
143
|
return homeViews(tree, viewIds, self.activePanelId, () => nextId('tab'));
|
|
222
144
|
}
|
|
223
145
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* when the workspace was up, an `init` when it was not — because from the
|
|
228
|
-
* classic stack nothing has been assigned to a tab yet.
|
|
229
|
-
*
|
|
230
|
-
* Returns where the view came from, or `undefined` if it is not in the
|
|
146
|
+
* Homes every view first: the homing autorun waits for the outermost
|
|
147
|
+
* action, so a view added in the same action has no tab yet. Returns
|
|
148
|
+
* where the view came from, or `undefined` if it is not in the
|
|
231
149
|
* session, in which case nothing is applied.
|
|
232
150
|
*/
|
|
233
|
-
function
|
|
234
|
-
const ids =
|
|
235
|
-
|
|
236
|
-
throw new Error(`${action}(viewId, allViewIds): allViewIds is every view id in the session, and this session has no view list to default it from`);
|
|
237
|
-
}
|
|
238
|
-
return ids;
|
|
239
|
-
}
|
|
240
|
-
function rehomeView(viewId, allViewIds, place) {
|
|
241
|
-
const tree = home(self.tree, allViewIds);
|
|
151
|
+
function rehomeView(viewId, place) {
|
|
152
|
+
const ids = sessionViewIds(self);
|
|
153
|
+
const tree = ids ? home(self.tree, ids) : self.tree;
|
|
242
154
|
const from = tabContainingView(tree, viewId);
|
|
243
155
|
if (!from) {
|
|
244
156
|
return undefined;
|
|
245
157
|
}
|
|
246
158
|
const placed = place(removeView(tree, viewId), from);
|
|
247
|
-
apply(
|
|
159
|
+
apply(pruneEmptyPanel(pruneEmptyTab(placed, from.tab.id), from.panel.id));
|
|
248
160
|
return from;
|
|
249
161
|
}
|
|
250
162
|
/**
|
|
251
|
-
* The panel a dropped tab is leaving, or `undefined` if the
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* edge-drop's new cell behind empty; a missing target splits nothing while
|
|
255
|
-
* the gesture goes on to point `activePanelId` at a cell nobody draws, and
|
|
256
|
-
* makes `moveTabToPanel`'s remove-then-insert a deletion.
|
|
257
|
-
*
|
|
258
|
-
* One place, so the next drop gesture inherits the rule rather than
|
|
259
|
-
* restating it — the two of them restating it is how one came to be
|
|
260
|
-
* missing half of it.
|
|
163
|
+
* The panel a dropped tab is leaving, or `undefined` if the tab or the
|
|
164
|
+
* target is missing. Every drop gesture checks this before touching the
|
|
165
|
+
* tree.
|
|
261
166
|
*/
|
|
262
167
|
function dropSource(tabId, targetPanelId) {
|
|
263
168
|
const source = findTab(self.tree, tabId)?.panel;
|
|
264
169
|
return source && self.hasPanel(targetPanelId) ? source : undefined;
|
|
265
170
|
}
|
|
266
|
-
// `apply`
|
|
267
|
-
// action it is a public "set the layout to this" on the session.
|
|
171
|
+
// `apply` stays private: as an action it would be a public "set layout"
|
|
268
172
|
return {
|
|
269
173
|
setActivePanelId(panelId) {
|
|
270
|
-
|
|
174
|
+
if (panelId === undefined) {
|
|
175
|
+
self.activePanelId = undefined;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
activate(panelId);
|
|
179
|
+
}
|
|
271
180
|
},
|
|
272
181
|
/**
|
|
273
|
-
* Show one cell at the size of the workspace, or go back
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
* cell, or not any more". Maximizing a DIFFERENT cell while one is
|
|
278
|
-
* already maximized moves the mode rather than restoring, which is
|
|
279
|
-
* what the menu item on another cell's strip is asking for.
|
|
280
|
-
*
|
|
281
|
-
* Mounts no views that were not mounted — it is the same cell showing
|
|
282
|
-
* the same tab — and unmounts every other cell's, so the WebGL2
|
|
283
|
-
* context ceiling (`agent-docs/reference/GPU_PORTABILITY.md`) can
|
|
284
|
-
* only go down. That is the reason it is this and not a `display:
|
|
285
|
-
* none` over a still-mounted workspace.
|
|
182
|
+
* Show one cell at the size of the workspace, or go back; naming a
|
|
183
|
+
* different cell moves the mode. Unmounts every other cell's views
|
|
184
|
+
* rather than hiding them, to stay under the WebGL2 context ceiling
|
|
185
|
+
* (`agent-docs/reference/GPU_PORTABILITY.md`).
|
|
286
186
|
*/
|
|
287
187
|
toggleMaximizedPanel(panelId) {
|
|
288
188
|
if (!self.hasPanel(panelId)) {
|
|
@@ -299,7 +199,7 @@ export function WorkspaceLayoutMixin() {
|
|
|
299
199
|
},
|
|
300
200
|
setActiveTab(panelId, tabId) {
|
|
301
201
|
apply(setActiveTab(self.tree, panelId, tabId));
|
|
302
|
-
|
|
202
|
+
activate(panelId);
|
|
303
203
|
},
|
|
304
204
|
renameTab(tabId, title) {
|
|
305
205
|
apply(renameTab(self.tree, tabId, title));
|
|
@@ -308,13 +208,12 @@ export function WorkspaceLayoutMixin() {
|
|
|
308
208
|
splitPanel(panelId, direction, before = false) {
|
|
309
209
|
const panel = emptyPanel();
|
|
310
210
|
apply(splitPanel(self.tree, panelId, direction, panel, before));
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
// — and homing falls back on activePanelId.
|
|
211
|
+
// a split of a missing cell inserts nothing, and homing falls back
|
|
212
|
+
// on activePanelId
|
|
314
213
|
if (!self.hasPanel(panel.id)) {
|
|
315
214
|
return undefined;
|
|
316
215
|
}
|
|
317
|
-
|
|
216
|
+
activate(panel.id);
|
|
318
217
|
return panel;
|
|
319
218
|
},
|
|
320
219
|
closePanel(panelId) {
|
|
@@ -327,22 +226,12 @@ export function WorkspaceLayoutMixin() {
|
|
|
327
226
|
}
|
|
328
227
|
const tab = { id: nextId('tab'), viewIds };
|
|
329
228
|
apply(addTab(self.tree, panelId, tab));
|
|
330
|
-
|
|
229
|
+
activate(panelId);
|
|
331
230
|
return tab;
|
|
332
231
|
},
|
|
333
232
|
/**
|
|
334
|
-
* Close a tab, and
|
|
335
|
-
*
|
|
336
|
-
* A cell whose tabs are all gone is the state `pruneEmptyPanel` was
|
|
337
|
-
* written for — "dragging the last tab out of a split and leaving a
|
|
338
|
-
* blank half is the one place an empty panel is clearly not what was
|
|
339
|
-
* meant" — and closing that tab arrives at the identical half by a
|
|
340
|
-
* different gesture. It rendered nothing at all, not even the launcher
|
|
341
|
-
* an empty TAB shows, so the only way back out of it was the `+`.
|
|
342
|
-
*
|
|
343
|
-
* `pruneEmptyPanel` carries both guards already: a cell with tabs left
|
|
344
|
-
* stays, and the last cell in the workspace stays whatever happens to
|
|
345
|
-
* it, since there is nowhere for the tree to collapse to.
|
|
233
|
+
* Close a tab, and its cell too if that was the last tab and not the
|
|
234
|
+
* workspace's last cell: a tabless cell renders nothing at all.
|
|
346
235
|
*/
|
|
347
236
|
closeTab(tabId) {
|
|
348
237
|
const panelId = findTab(self.tree, tabId)?.panel.id;
|
|
@@ -354,42 +243,15 @@ export function WorkspaceLayoutMixin() {
|
|
|
354
243
|
addViewToTab(tabId, viewId) {
|
|
355
244
|
apply(addViewToTab(self.tree, tabId, viewId));
|
|
356
245
|
},
|
|
357
|
-
// NO `removeView`
|
|
358
|
-
// `removeView
|
|
359
|
-
|
|
360
|
-
// button and every host calls. A same-named action here does not extend
|
|
361
|
-
// it, it *replaces* it (types.compose merges, last one wins), so the
|
|
362
|
-
// session action stopped being reachable at all: closing a view pruned
|
|
363
|
-
// the layout tree and left the view in the session forever, and
|
|
364
|
-
// `session.removeView(view)` passed a model where this wanted an id, so
|
|
365
|
-
// it matched nothing and even the pruning was a no-op.
|
|
366
|
-
//
|
|
367
|
-
// Nothing needs one either way: `home` below drops any tab entry whose
|
|
368
|
-
// view is no longer in `session.views`, so removing the view is the
|
|
369
|
-
// whole operation and the tree follows.
|
|
370
|
-
/**
|
|
371
|
-
* Drop a dragged tab into an existing panel, as a tab.
|
|
372
|
-
*
|
|
373
|
-
* One action, so the tree never exists in a state where the tab is in
|
|
374
|
-
* both panels or neither. The imperative bridge needed an explicit
|
|
375
|
-
* `runInAction` around the unassign+reassign pair for exactly this, and
|
|
376
|
-
* a comment explaining that without it the reconcile autorun would
|
|
377
|
-
* observe the gap and re-home the view.
|
|
378
|
-
*/
|
|
246
|
+
// NO `removeView` here: composed into the session it would replace
|
|
247
|
+
// `session.removeView`, and homing already drops a removed view
|
|
248
|
+
/** Drop a dragged tab into an existing panel, as a tab. */
|
|
379
249
|
dropTabInPanel(tabId, targetPanelId, index) {
|
|
380
250
|
const source = dropSource(tabId, targetPanelId);
|
|
381
251
|
if (!source) {
|
|
382
252
|
return;
|
|
383
253
|
}
|
|
384
|
-
//
|
|
385
|
-
// nothing. There is no gap under the pointer to state a position, and
|
|
386
|
-
// the indicator washes the whole cell — which says "be a tab of this
|
|
387
|
-
// cell", which it already is. Appending reordered the strip to say
|
|
388
|
-
// something the gesture never said, and sent the tab to the end.
|
|
389
|
-
// dockview declines a centre drop on the group a tab came from too.
|
|
390
|
-
//
|
|
391
|
-
// The rule is the gesture's, not `moveTabToPanel`'s: no index there
|
|
392
|
-
// still means append, which is the only reading a total function has.
|
|
254
|
+
// a drop on the body of the tab's own cell asks for nothing
|
|
393
255
|
if (source.id === targetPanelId && index === undefined) {
|
|
394
256
|
return;
|
|
395
257
|
}
|
|
@@ -398,7 +260,7 @@ export function WorkspaceLayoutMixin() {
|
|
|
398
260
|
next = pruneEmptyPanel(next, source.id);
|
|
399
261
|
}
|
|
400
262
|
apply(next);
|
|
401
|
-
|
|
263
|
+
activate(targetPanelId);
|
|
402
264
|
},
|
|
403
265
|
/** Drop a dragged tab on a panel edge: split, and land in the new half. */
|
|
404
266
|
dropTabInNewSplit(tabId, targetPanelId, direction, before) {
|
|
@@ -413,33 +275,20 @@ export function WorkspaceLayoutMixin() {
|
|
|
413
275
|
};
|
|
414
276
|
let next = splitPanel(self.tree, targetPanelId, direction, panel, before);
|
|
415
277
|
next = moveTabToPanel(next, tabId, panel.id);
|
|
416
|
-
//
|
|
417
|
-
// now-empty source, which collapses the split — the gesture undoes
|
|
418
|
-
// itself rather than leaving a blank half, without needing a case.
|
|
278
|
+
// a panel's only tab dropped on its own edge collapses the split
|
|
419
279
|
next = pruneEmptyPanel(next, source.id);
|
|
420
280
|
apply(next);
|
|
421
|
-
|
|
281
|
+
activate(panel.id);
|
|
422
282
|
return panel.id;
|
|
423
283
|
},
|
|
424
284
|
setSizes(branchId, sizes) {
|
|
425
285
|
apply(setSizes(self.tree, branchId, sizes));
|
|
426
286
|
},
|
|
427
287
|
/**
|
|
428
|
-
* Arrange the workspace as a spec states
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
* `{ direction: 'horizontal', children: [{ views: [0] }, { views: [1] }] }`
|
|
433
|
-
* means here what it means in a link. A leaf spelled any other way, an
|
|
434
|
-
* index past the end or an id no view has throws rather than arranging
|
|
435
|
-
* nothing — an untyped caller used to collapse the workspace into one
|
|
436
|
-
* blank tab that way.
|
|
437
|
-
*
|
|
438
|
-
* There is no `init` property and no standing request: the spec is
|
|
439
|
-
* converted and *becomes* the layout, here and now. `init` existed only
|
|
440
|
-
* because dockview had to be told, could not be told before it mounted,
|
|
441
|
-
* and had to be told again afterwards — three problems that all came
|
|
442
|
-
* from the layout living somewhere this action could not reach.
|
|
288
|
+
* Arrange the workspace as a spec states, in the shape a session
|
|
289
|
+
* spec's `layout` takes: a leaf's `views` names views by index into
|
|
290
|
+
* `session.views` or by id. A malformed leaf, an index past the end or
|
|
291
|
+
* an unknown id throws.
|
|
443
292
|
*/
|
|
444
293
|
applyLayoutSpec(spec) {
|
|
445
294
|
const resolved = resolveLayoutSpec(spec, sessionViewIds(self));
|
|
@@ -449,25 +298,21 @@ export function WorkspaceLayoutMixin() {
|
|
|
449
298
|
},
|
|
450
299
|
/**
|
|
451
300
|
* ViewMenu's "move to new tab": the view leaves its tab for a new one.
|
|
452
|
-
*
|
|
453
|
-
* `allViewIds` is EVERY view in the session, defaulting to the host's
|
|
454
|
-
* own list — homing drops any view the list does not name, so the
|
|
455
|
-
* `[viewId]` default this used to carry unhomed all the others.
|
|
456
301
|
*/
|
|
457
|
-
moveViewToNewTab(viewId
|
|
302
|
+
moveViewToNewTab(viewId) {
|
|
458
303
|
const tab = { id: nextId('tab'), viewIds: [viewId] };
|
|
459
|
-
const from = rehomeView(viewId,
|
|
304
|
+
const from = rehomeView(viewId, (tree, at) => addTab(tree, at.panel.id, tab));
|
|
460
305
|
if (!from) {
|
|
461
306
|
return undefined;
|
|
462
307
|
}
|
|
463
|
-
|
|
308
|
+
activate(from.panel.id);
|
|
464
309
|
return tab.id;
|
|
465
310
|
},
|
|
466
311
|
/**
|
|
467
|
-
* ViewMenu's "move to split
|
|
468
|
-
* `
|
|
312
|
+
* ViewMenu's "move to split": the view leaves for a new cell to the
|
|
313
|
+
* right (`row`) or below (`column`) of its own.
|
|
469
314
|
*/
|
|
470
|
-
|
|
315
|
+
moveViewToSplit(viewId, direction) {
|
|
471
316
|
const tabId = nextId('tab');
|
|
472
317
|
const panel = {
|
|
473
318
|
id: nextId('panel'),
|
|
@@ -475,111 +320,108 @@ export function WorkspaceLayoutMixin() {
|
|
|
475
320
|
tabs: [{ id: tabId, viewIds: [viewId] }],
|
|
476
321
|
activeTabId: tabId,
|
|
477
322
|
};
|
|
478
|
-
const from = rehomeView(viewId,
|
|
323
|
+
const from = rehomeView(viewId, (tree, at) => splitPanel(tree, at.panel.id, direction, panel));
|
|
479
324
|
if (!from) {
|
|
480
325
|
return undefined;
|
|
481
326
|
}
|
|
482
|
-
|
|
327
|
+
activate(panel.id);
|
|
483
328
|
return panel.id;
|
|
484
329
|
},
|
|
330
|
+
/**
|
|
331
|
+
* ViewMenu's "move to tab": the view joins an existing tab, which
|
|
332
|
+
* becomes the one shown.
|
|
333
|
+
*/
|
|
334
|
+
moveViewToTab(viewId, tabId) {
|
|
335
|
+
const target = findTab(self.tree, tabId);
|
|
336
|
+
if (!target || target.tab.viewIds.includes(viewId)) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const from = rehomeView(viewId, tree => addViewToTab(tree, tabId, viewId));
|
|
340
|
+
const landed = findTab(self.tree, tabId);
|
|
341
|
+
if (from && landed) {
|
|
342
|
+
apply(setActiveTab(self.tree, landed.panel.id, tabId));
|
|
343
|
+
activate(landed.panel.id);
|
|
344
|
+
}
|
|
345
|
+
},
|
|
485
346
|
homeUnassignedViews(viewIds) {
|
|
486
347
|
apply(home(self.tree, viewIds));
|
|
487
348
|
},
|
|
488
349
|
};
|
|
489
350
|
})
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
* sugar FOR through `self`.
|
|
493
|
-
*
|
|
494
|
-
* `this.applyLayoutSpec()` reaches sideways into the action literal the
|
|
495
|
-
* block returned — the fork's `instantiateActions` does `fn.bind(actions)`
|
|
496
|
-
* — so it is pinned to that block's implementation for good. A later block,
|
|
497
|
-
* or a plugin's `extendStateModel`, replaces `self.applyLayoutSpec` and the
|
|
498
|
-
* sideways hop goes on calling the one it replaced, with no error and no
|
|
499
|
-
* type complaint. An extra layer costs a line.
|
|
500
|
-
*/
|
|
351
|
+
// a separate block so these reach the actions they wrap through `self`,
|
|
352
|
+
// where a later override still gets called
|
|
501
353
|
.actions(self => ({
|
|
502
354
|
/**
|
|
503
|
-
* Move one view
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
* it survives this one the same way — a capability-detecting caller
|
|
508
|
-
* cannot tell you it lost a capability.
|
|
509
|
-
*
|
|
510
|
-
* **`allViewIds` is therefore OPTIONAL, and has to stay that way.** The
|
|
511
|
-
* plugin passes the move alone, because that was the whole signature when
|
|
512
|
-
* its call site was written; requiring the second argument threw
|
|
513
|
-
* `undefined.filter` out of a launch the plugin does not wrap, and the
|
|
514
|
-
* figure was again the only thing that noticed. Keeping the NAME is half
|
|
515
|
-
* of not breaking a runtime lookup — the call has to keep working as it
|
|
516
|
-
* is spelled.
|
|
355
|
+
* Move one view to a new tab or a split beside its cell, keeping the
|
|
356
|
+
* rest of the arrangement. PUBLIC API: protein3d and msaview call it
|
|
357
|
+
* with the move alone. The second argument is an old plugin signature;
|
|
358
|
+
* the session's own views are read instead.
|
|
517
359
|
*/
|
|
518
|
-
setPendingMove(move,
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
// rest. Nothing to say is nothing to do.
|
|
522
|
-
const ids = allViewIds ?? sessionViewIds(self);
|
|
523
|
-
if (!move || !ids) {
|
|
360
|
+
setPendingMove(move, _allViewIds) {
|
|
361
|
+
const ids = sessionViewIds(self);
|
|
362
|
+
if (!move || !ids?.includes(move.viewId)) {
|
|
524
363
|
return;
|
|
525
364
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
if (
|
|
530
|
-
|
|
365
|
+
if (ids.length === 1) {
|
|
366
|
+
self.homeUnassignedViews(ids);
|
|
367
|
+
}
|
|
368
|
+
else if (move.type === 'newTab') {
|
|
369
|
+
self.moveViewToNewTab(move.viewId);
|
|
531
370
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
// selection, so `treeFromSpec` shows each cell's FIRST tab — and
|
|
535
|
-
// `newTab` puts the moved view in a tab beside the others, which makes
|
|
536
|
-
// it the one tab nobody can see. The plugin asking for this is asking
|
|
537
|
-
// for its view to be on screen; `moveViewToNewTab`, the same gesture
|
|
538
|
-
// from the View menu, has always ended with it there.
|
|
539
|
-
const home = tabContainingView(self.tree, move.viewId);
|
|
540
|
-
if (home) {
|
|
541
|
-
self.setActiveTab(home.panel.id, home.tab.id);
|
|
371
|
+
else {
|
|
372
|
+
self.moveViewToSplit(move.viewId, 'row');
|
|
542
373
|
}
|
|
543
374
|
},
|
|
544
375
|
/**
|
|
545
|
-
*
|
|
546
|
-
* shapes
|
|
547
|
-
* which went with that component and were not reimplemented.
|
|
548
|
-
*
|
|
549
|
-
* `allViewIds` is passed in rather than read off the session for the same
|
|
550
|
-
* reason `moveViewToNewTab` takes it: this mixin owns the tree and has no
|
|
551
|
-
* view list of its own. Passing `session.views` order means the
|
|
552
|
-
* arrangement it states is already the order views render in, so unlike a
|
|
553
|
-
* session spec's layout there is nothing for `orderViews` to apply.
|
|
376
|
+
* Re-arrange the whole workspace, one view per cell, in one of four
|
|
377
|
+
* shapes, in `session.views` order.
|
|
554
378
|
*/
|
|
555
|
-
tileViews(mode
|
|
556
|
-
self.applyLayoutSpec(tileLayoutSpec(
|
|
379
|
+
tileViews(mode) {
|
|
380
|
+
self.applyLayoutSpec(tileLayoutSpec(sessionViewIds(self) ?? [], mode));
|
|
557
381
|
},
|
|
558
382
|
/**
|
|
559
383
|
* #action
|
|
560
|
-
* Arrange the session's views into panels
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
* layout that seats no view throws rather than leaving a blank tab. A
|
|
567
|
-
* session spec's `layout` and an agent's live re-layout both call it.
|
|
384
|
+
* Arrange the session's views into panels: `applyLayoutSpec`, then
|
|
385
|
+
* `session.views` ordered to the spec's top-to-bottom order, which tabs
|
|
386
|
+
* read their order from. Leaves take view ids or indexes into
|
|
387
|
+
* `session.views`; a layout that seats no view throws rather than
|
|
388
|
+
* leaving a blank tab. A session spec's `layout` and an agent's live
|
|
389
|
+
* re-layout both call it.
|
|
568
390
|
*/
|
|
569
391
|
layoutViews(spec) {
|
|
570
392
|
const host = self;
|
|
571
|
-
if (!host.
|
|
393
|
+
if (!host.orderViews) {
|
|
572
394
|
throw new Error('This session has no view list to arrange: layoutViews needs a host composed with MultipleViewsSessionMixin');
|
|
573
395
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (ids.length === 0) {
|
|
396
|
+
const seated = viewIdsInSpec(resolveLayoutSpec(spec, sessionViewIds(self)));
|
|
397
|
+
if (seated.length === 0) {
|
|
577
398
|
throw new Error('The layout seats no views: a leaf names its views with "views" (view ids, or indexes into session.views) and a container nests "children"');
|
|
578
399
|
}
|
|
400
|
+
const ids = self.applyLayoutSpec(spec);
|
|
579
401
|
host.orderViews(ids);
|
|
580
402
|
return ids;
|
|
581
403
|
},
|
|
582
|
-
}))
|
|
404
|
+
}))
|
|
405
|
+
.actions(self => ({
|
|
406
|
+
afterAttach() {
|
|
407
|
+
addDisposer(self, autorun(function homeUnassignedViewsAutorun() {
|
|
408
|
+
const host = self;
|
|
409
|
+
if (host.views) {
|
|
410
|
+
self.homeUnassignedViews(host.views.map(v => v.id));
|
|
411
|
+
}
|
|
412
|
+
}, { name: 'HomeUnassignedViews' }));
|
|
413
|
+
},
|
|
414
|
+
}))
|
|
415
|
+
// A session saved with workspaces off showed its views stacked whatever
|
|
416
|
+
// layout it carried, so it opens as one cell holding them all
|
|
417
|
+
.preProcessSnapshot(snap => {
|
|
418
|
+
if (snap?.useWorkspaces !==
|
|
419
|
+
false) {
|
|
420
|
+
return snap;
|
|
421
|
+
}
|
|
422
|
+
const { layout, activePanelId, maximizedPanelId, ...rest } = snap;
|
|
423
|
+
return rest;
|
|
424
|
+
}));
|
|
583
425
|
}
|
|
584
426
|
export function isSessionWithWorkspaceLayout(session) {
|
|
585
427
|
return 'layout' in session && 'splitPanel' in session;
|