@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.
Files changed (78) hide show
  1. package/esm/AppSession/AppSessionMixin.d.ts +110 -92
  2. package/esm/Assemblies/AssembliesMixin.d.ts +30 -0
  3. package/esm/HistoryManagement/index.d.ts +2 -0
  4. package/esm/HistoryManagement/index.js +2 -0
  5. package/esm/JBrowseConfig/RootConfiguration.d.ts +4 -10
  6. package/esm/JBrowseConfig/RootConfiguration.js +5 -8
  7. package/esm/JBrowseConfig/index.d.ts +3 -11
  8. package/esm/JBrowseConfig/index.js +2 -3
  9. package/esm/JBrowseModel/index.d.ts +10 -9
  10. package/esm/JBrowseModel/index.js +8 -7
  11. package/esm/JbApi/jbApi.d.ts +2 -2
  12. package/esm/JbApi/jbApi.js +16 -11
  13. package/esm/RootMenu/index.d.ts +2 -0
  14. package/esm/RootMenu/index.js +2 -0
  15. package/esm/SessionSpec/index.d.ts +2 -2
  16. package/esm/SessionSpec/index.js +1 -1
  17. package/esm/SessionSpec/loadSessionSpec.js +2 -12
  18. package/esm/SessionSpec/parseSessionSpecUrl.d.ts +19 -0
  19. package/esm/SessionSpec/parseSessionSpecUrl.js +29 -0
  20. package/esm/WorkspaceLayout/LayoutRenderer.d.ts +0 -10
  21. package/esm/WorkspaceLayout/LayoutRenderer.js +34 -101
  22. package/esm/WorkspaceLayout/PanelView.js +19 -45
  23. package/esm/WorkspaceLayout/TabStrip.d.ts +2 -2
  24. package/esm/WorkspaceLayout/TabStrip.js +36 -104
  25. package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +0 -9
  26. package/esm/WorkspaceLayout/WorkspaceContainer.js +9 -42
  27. package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +1 -16
  28. package/esm/WorkspaceLayout/WorkspacePanelActions.js +46 -51
  29. package/esm/WorkspaceLayout/WorkspaceTab.d.ts +3 -19
  30. package/esm/WorkspaceLayout/WorkspaceTab.js +12 -48
  31. package/esm/WorkspaceLayout/dropZone.d.ts +7 -37
  32. package/esm/WorkspaceLayout/dropZone.js +7 -44
  33. package/esm/WorkspaceLayout/model.d.ts +45 -120
  34. package/esm/WorkspaceLayout/model.js +145 -303
  35. package/esm/WorkspaceLayout/panelChrome.d.ts +4 -7
  36. package/esm/WorkspaceLayout/panelChrome.js +0 -2
  37. package/esm/WorkspaceLayout/spec.d.ts +10 -51
  38. package/esm/WorkspaceLayout/spec.js +12 -80
  39. package/esm/WorkspaceLayout/splitter.d.ts +3 -19
  40. package/esm/WorkspaceLayout/splitter.js +5 -21
  41. package/esm/WorkspaceLayout/tabName.d.ts +8 -0
  42. package/esm/WorkspaceLayout/tabName.js +14 -0
  43. package/esm/WorkspaceLayout/tree.d.ts +19 -79
  44. package/esm/WorkspaceLayout/tree.js +34 -131
  45. package/esm/WorkspaceLayout/useLayoutDrag.d.ts +4 -22
  46. package/esm/WorkspaceLayout/useLayoutDrag.js +55 -177
  47. package/esm/WorkspaceLayout/usePointerGesture.d.ts +24 -0
  48. package/esm/WorkspaceLayout/usePointerGesture.js +83 -0
  49. package/esm/WorkspaceLayout/workspaceTheme.d.ts +27 -0
  50. package/esm/WorkspaceLayout/workspaceTheme.js +37 -0
  51. package/esm/index.d.ts +5 -3
  52. package/esm/index.js +3 -2
  53. package/esm/queryParams.d.ts +2 -0
  54. package/esm/queryParams.js +16 -5
  55. package/esm/sessionSummary.d.ts +16 -0
  56. package/esm/sessionSummary.js +57 -0
  57. package/esm/ui/App/App.js +3 -0
  58. package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +3 -2
  59. package/esm/ui/App/DrawerHeaderHelpDialog.js +4 -2
  60. package/esm/ui/App/JBrowseTabMenu.d.ts +3 -2
  61. package/esm/ui/App/JBrowseTabMenu.js +5 -3
  62. package/esm/ui/App/ThemeModeButton.d.ts +4 -5
  63. package/esm/ui/App/ThemeModeButton.js +22 -14
  64. package/esm/ui/App/ViewContainer.js +7 -1
  65. package/esm/ui/App/ViewMenu.js +69 -42
  66. package/esm/ui/App/ViewWrapper.js +2 -2
  67. package/esm/ui/App/ViewsContainer.js +3 -4
  68. package/esm/ui/App/lazyParts.d.ts +0 -5
  69. package/esm/ui/App/lazyParts.js +0 -1
  70. package/esm/ui/App/preloadAppShell.js +3 -6
  71. package/esm/ui/App/types.d.ts +3 -2
  72. package/esm/ui/App/viewTitle.d.ts +6 -1
  73. package/esm/ui/App/viewTitle.js +7 -2
  74. package/package.json +7 -7
  75. package/esm/WorkspaceLayout/dockviewTheme.d.ts +0 -69
  76. package/esm/WorkspaceLayout/dockviewTheme.js +0 -82
  77. package/esm/ui/App/ClassicViewsContainer.d.ts +0 -6
  78. 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 { resolveLayoutSpec, specForPendingMove, tileLayoutSpec, treeFromSpec, viewIdsInSpec, } from './spec.js';
4
- import { activeTabIn, addTab, addViewToTab, findTab, homeViews, moveTabToPanel, normalize, panelContainingView, panels, pruneEmptyPanel, pruneEmptyTabIn, removePanel, removeTab, removeView, renameTab, setActiveTab, setSizes, splitPanel, tabContainingView, tabs, } from './tree.js';
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, in one MST tree. There is no second owner, so there is
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 and nothing else has to be told.
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
- // `children` is a union of both node kinds, resolved late because a branch
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
- * Panel and tab ids.
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
- * Deliberately HERE and not a `maximized` flag on `PanelNode`. On the
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
- * The plain tree the pure functions take.
111
- *
112
- * `getSnapshot` is a `keepAlive` computed, so this is cached and
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
- * What to render: the maximized cell alone, or the whole tree.
155
- *
156
- * Sized to 1 rather than handed over as it sits. A pane's `size` is its
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 this model holds outside the tree must name a cell
170
- * that exists, whatever just stopped existing. A dangling
171
- * `activePanelId` puts homed views in a cell nobody draws; a dangling
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
- * Every write to the tree, and therefore the one place the invariant
196
- * above is repaired.
197
- *
198
- * It used to be a `keepActivePanel()` the two closing actions called,
199
- * which was right while a closing action was the only way to retire a
200
- * cell. `maximizedPanelId` is not reached that way: BOTH drop gestures
201
- * prune their emptied source panel, and `applyLayoutSpec` replaces every
202
- * id in the tree at once. That is five call sites for one rule, which is
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
- // A cell appearing where it cannot be seen is the one thing maximize
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
- * The shape ViewMenu's two "give this view a home of its own" moves share.
225
- *
226
- * Homing first is what removed the old fork here — the live dockview api
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 everyViewId(action, allViewIds) {
234
- const ids = allViewIds ?? sessionViewIds(self);
235
- if (!ids) {
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(pruneEmptyTabIn(placed, from.panel.id, from.tab.id));
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 drop cannot
252
- * happen — which BOTH drop gestures have to establish before they touch
253
- * the tree, and for two different reasons. A missing tab leaves an
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` is deliberately not returned: it takes a whole tree, so as an
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
- self.activePanelId = panelId;
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
- * A toggle rather than a pair, because the gesture is a toggle: the
276
- * strip's double-click and the cell menu's one item both mean "this
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
- self.activePanelId = panelId;
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
- // A split of a cell that is not there inserts nothing, so claiming
312
- // the id anyway would leave activePanelId naming a cell nobody draws
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
- self.activePanelId = panel.id;
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
- self.activePanelId = panelId;
229
+ activate(panelId);
331
230
  return tab;
332
231
  },
333
232
  /**
334
- * Close a tab, and the cell with it if that was its last.
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` HERE. This mixin is composed into the session, where
358
- // `removeView(view)` is already the action that takes a view out of
359
- // `session.views` — part of AbstractSessionModel, and what every close
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
- // A drop on the BODY of the cell the tab is already in asks for
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
- self.activePanelId = targetPanelId;
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
- // Dragging a panel's only tab onto that same panel's edge prunes the
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
- self.activePanelId = panel.id;
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
- * The spec is the same shape a session spec's `layout` takes: a leaf's
431
- * `views` names views by index into `session.views` or by id, so
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, allViewIds) {
302
+ moveViewToNewTab(viewId) {
458
303
  const tab = { id: nextId('tab'), viewIds: [viewId] };
459
- const from = rehomeView(viewId, everyViewId('moveViewToNewTab', allViewIds), (tree, at) => addTab(tree, at.panel.id, tab));
304
+ const from = rehomeView(viewId, (tree, at) => addTab(tree, at.panel.id, tab));
460
305
  if (!from) {
461
306
  return undefined;
462
307
  }
463
- self.activePanelId = from.panel.id;
308
+ activate(from.panel.id);
464
309
  return tab.id;
465
310
  },
466
311
  /**
467
- * ViewMenu's "move to split view": the view leaves for a new cell.
468
- * `allViewIds` is every view in the session — see `moveViewToNewTab`.
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
- moveViewToSplitRight(viewId, allViewIds) {
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, everyViewId('moveViewToSplitRight', allViewIds), (tree, at) => splitPanel(tree, at.panel.id, 'row', panel));
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
- self.activePanelId = panel.id;
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
- * The two sugars, in their own block so they can reach the actions they are
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 relative to the others. PUBLIC API: an external plugin
504
- * calls this behind a `'setPendingMove' in session` guard
505
- * (jbrowse-plugin-protein3d, putting a protein view beside its genome
506
- * view). It survived the last storage change by being kept as sugar, and
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, allViewIds) {
519
- // Not `?? []`: homing drops every view the list does not name, so an
520
- // empty one would answer "put this view beside nothing" and unhome the
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
- const known = liveViewIds(self, ids);
527
- // Nothing to move: the view named is not in the session, so there is
528
- // no arrangement to state about it.
529
- if (sessionViewIds(self) && !known.includes(move.viewId)) {
530
- return;
365
+ if (ids.length === 1) {
366
+ self.homeUnassignedViews(ids);
367
+ }
368
+ else if (move.type === 'newTab') {
369
+ self.moveViewToNewTab(move.viewId);
531
370
  }
532
- self.applyLayoutSpec(specForPendingMove(move, known));
533
- // Show where the view went. A spec states an arrangement and not a
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
- * The whole-workspace re-arrange: every view one cell, in one of four
546
- * shapes. Restored from the dockview header's four "Global:" commands,
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, allViewIds) {
556
- self.applyLayoutSpec(tileLayoutSpec(liveViewIds(self, allViewIds), mode));
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. Calls `applyLayoutSpec`,
561
- * turns workspaces mode on for this session (a layout renders nowhere
562
- * else), and orders `session.views` to the spec's top-to-bottom
563
- * order, which tabs read their order from. Without the last two steps
564
- * the layout applies but does not display, or the tabs appear in the
565
- * wrong order. Leaves take view ids or indexes into `session.views`; a
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.setUseWorkspaces || !host.orderViews) {
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
- host.setUseWorkspaces(true);
575
- const ids = self.applyLayoutSpec(spec);
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;