@jbrowse/app-core 4.2.1 → 5.0.0-beta.1
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/AppRootModel.d.ts +30 -0
- package/esm/AppSession/AppRootModel.js +1 -0
- package/esm/AppSession/AppSessionMixin.d.ts +1598 -0
- package/esm/AppSession/AppSessionMixin.js +62 -0
- package/esm/AppSession/index.d.ts +2 -0
- package/esm/AppSession/index.js +1 -0
- package/esm/Assemblies/AssembliesMixin.d.ts +225 -0
- package/esm/Assemblies/AssembliesMixin.js +149 -0
- package/esm/Assemblies/index.d.ts +1 -2
- package/esm/Assemblies/index.js +1 -2
- package/esm/HistoryManagement/index.d.ts +19 -0
- package/esm/HistoryManagement/index.js +42 -5
- package/esm/JBrowseConfig/RootConfiguration.d.ts +167 -78
- package/esm/JBrowseConfig/RootConfiguration.js +85 -7
- package/esm/JBrowseConfig/index.d.ts +169 -84
- package/esm/JBrowseConfig/index.js +62 -2
- package/esm/JBrowseModel/index.d.ts +176 -95
- package/esm/JBrowseModel/index.js +122 -21
- package/esm/RootMenu/index.d.ts +77 -2
- package/esm/RootMenu/index.js +107 -61
- package/esm/SessionSpec/index.d.ts +6 -0
- package/esm/SessionSpec/index.js +3 -0
- package/esm/SessionSpec/lgvUrlInit.d.ts +79 -0
- package/esm/SessionSpec/lgvUrlInit.js +158 -0
- package/esm/SessionSpec/loadSessionSpec.d.ts +21 -0
- package/esm/SessionSpec/loadSessionSpec.js +311 -0
- package/esm/SessionSpec/parseSessionSpecUrl.d.ts +13 -0
- package/esm/SessionSpec/parseSessionSpecUrl.js +196 -0
- package/esm/SessionSpec/types.d.ts +33 -0
- package/esm/SessionSpec/types.js +1 -0
- package/esm/WorkspaceLayout/LayoutRenderer.d.ts +24 -0
- package/esm/WorkspaceLayout/LayoutRenderer.js +185 -0
- package/esm/WorkspaceLayout/PanelView.d.ts +13 -0
- package/esm/WorkspaceLayout/PanelView.js +99 -0
- package/esm/WorkspaceLayout/TabStrip.d.ts +14 -0
- package/esm/WorkspaceLayout/TabStrip.js +226 -0
- package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +16 -0
- package/esm/WorkspaceLayout/WorkspaceContainer.js +105 -0
- package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +24 -0
- package/esm/WorkspaceLayout/WorkspacePanelActions.js +120 -0
- package/esm/WorkspaceLayout/WorkspaceTab.d.ts +27 -0
- package/esm/WorkspaceLayout/WorkspaceTab.js +109 -0
- package/esm/WorkspaceLayout/dockviewTheme.d.ts +69 -0
- package/esm/WorkspaceLayout/dockviewTheme.js +82 -0
- package/esm/WorkspaceLayout/dropZone.d.ts +105 -0
- package/esm/WorkspaceLayout/dropZone.js +113 -0
- package/esm/WorkspaceLayout/index.d.ts +8 -0
- package/esm/WorkspaceLayout/index.js +4 -0
- package/esm/WorkspaceLayout/model.d.ts +240 -0
- package/esm/WorkspaceLayout/model.js +527 -0
- package/esm/WorkspaceLayout/panelChrome.d.ts +19 -0
- package/esm/WorkspaceLayout/panelChrome.js +4 -0
- package/esm/WorkspaceLayout/spec.d.ts +58 -0
- package/esm/WorkspaceLayout/spec.js +153 -0
- package/esm/WorkspaceLayout/splitter.d.ts +25 -0
- package/esm/WorkspaceLayout/splitter.js +37 -0
- package/esm/WorkspaceLayout/tree.d.ts +158 -0
- package/esm/WorkspaceLayout/tree.js +379 -0
- package/esm/WorkspaceLayout/useLayoutDrag.d.ts +34 -0
- package/esm/WorkspaceLayout/useLayoutDrag.js +232 -0
- package/esm/index.d.ts +16 -9
- package/esm/index.js +13 -9
- package/esm/menus.d.ts +63 -62
- package/esm/menus.js +131 -84
- package/esm/queryParams.d.ts +9 -0
- package/esm/queryParams.js +92 -0
- package/esm/sessionModelFactory.d.ts +18 -0
- package/esm/sessionModelFactory.js +1 -0
- package/esm/ui/App/App.d.ts +3 -14
- package/esm/ui/App/App.js +30 -17
- package/esm/ui/App/AppFab.d.ts +12 -3
- package/esm/ui/App/AppFab.js +15 -8
- package/esm/ui/App/AppReadyMarker.d.ts +34 -0
- package/esm/ui/App/AppReadyMarker.js +56 -0
- package/esm/ui/App/AppToolbar.d.ts +2 -14
- package/esm/ui/App/AppToolbar.js +23 -24
- package/esm/ui/App/ClassicViewsContainer.d.ts +3 -9
- package/esm/ui/App/ClassicViewsContainer.js +9 -8
- package/esm/ui/App/DialogQueue.d.ts +17 -1
- package/esm/ui/App/DialogQueue.js +30 -1
- package/esm/ui/App/Drawer.d.ts +1 -1
- package/esm/ui/App/Drawer.js +10 -39
- package/esm/ui/App/DrawerControls.d.ts +1 -5
- package/esm/ui/App/DrawerControls.js +1 -25
- package/esm/ui/App/DrawerHeader.d.ts +2 -3
- package/esm/ui/App/DrawerHeader.js +24 -10
- package/esm/ui/App/DrawerHeaderHelpButton.d.ts +1 -1
- package/esm/ui/App/DrawerHeaderHelpButton.js +3 -3
- package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +2 -2
- package/esm/ui/App/DrawerHeaderHelpDialog.js +3 -4
- package/esm/ui/App/DrawerWidget.d.ts +1 -1
- package/esm/ui/App/DrawerWidget.js +9 -21
- package/esm/ui/App/JBrowseTabMenu.d.ts +2 -3
- package/esm/ui/App/JBrowseTabMenu.js +24 -5
- package/esm/ui/App/ViewButtons.d.ts +2 -4
- package/esm/ui/App/ViewButtons.js +15 -3
- package/esm/ui/App/ViewContainer.d.ts +3 -2
- package/esm/ui/App/ViewContainer.js +144 -24
- package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
- package/esm/ui/App/ViewContainerTitle.js +9 -2
- package/esm/ui/App/ViewErrorFallback.d.ts +18 -0
- package/esm/ui/App/ViewErrorFallback.js +42 -0
- package/esm/ui/App/ViewHeader.d.ts +3 -4
- package/esm/ui/App/ViewHeader.js +28 -17
- package/esm/ui/App/ViewLauncher.d.ts +2 -8
- package/esm/ui/App/ViewLauncher.js +7 -5
- package/esm/ui/App/ViewMenu.d.ts +3 -5
- package/esm/ui/App/ViewMenu.js +87 -92
- package/esm/ui/App/ViewStack.d.ts +7 -0
- package/esm/ui/App/ViewStack.js +42 -0
- package/esm/ui/App/ViewWrapper.d.ts +15 -1
- package/esm/ui/App/ViewWrapper.js +26 -10
- package/esm/ui/App/ViewsContainer.d.ts +3 -8
- package/esm/ui/App/ViewsContainer.js +9 -6
- package/esm/ui/App/copyView.js +1 -1
- package/esm/ui/App/index.d.ts +2 -1
- package/esm/ui/App/index.js +3 -2
- package/esm/ui/App/lazyChunk.d.ts +13 -0
- package/esm/ui/App/lazyChunk.js +91 -0
- package/esm/ui/App/types.d.ts +12 -3
- package/esm/ui/App/types.js +1 -1
- package/esm/ui/App/useViewVisibility.d.ts +34 -0
- package/esm/ui/App/useViewVisibility.js +87 -0
- package/esm/ui/App/viewTitle.d.ts +5 -0
- package/esm/ui/App/viewTitle.js +9 -0
- package/esm/ui/index.d.ts +1 -1
- package/esm/ui/index.js +1 -1
- package/package.json +12 -17
- package/esm/AppFocus/index.d.ts +0 -5
- package/esm/AppFocus/index.js +0 -12
- package/esm/Assemblies/SessionAssembliesMixin.d.ts +0 -92
- package/esm/Assemblies/SessionAssembliesMixin.js +0 -53
- package/esm/Assemblies/TemporaryAssembliesMixin.d.ts +0 -10
- package/esm/Assemblies/TemporaryAssembliesMixin.js +0 -37
- package/esm/DockviewLayout/index.d.ts +0 -35
- package/esm/DockviewLayout/index.js +0 -111
- package/esm/ui/App/DockviewContext.d.ts +0 -22
- package/esm/ui/App/DockviewContext.js +0 -29
- package/esm/ui/App/DockviewLeftHeaderActions.d.ts +0 -2
- package/esm/ui/App/DockviewLeftHeaderActions.js +0 -58
- package/esm/ui/App/DockviewRightHeaderActions.d.ts +0 -2
- package/esm/ui/App/DockviewRightHeaderActions.js +0 -100
- package/esm/ui/App/DrawerWidgetSelector.d.ts +0 -5
- package/esm/ui/App/DrawerWidgetSelector.js +0 -47
- package/esm/ui/App/JBrowseViewPanel.d.ts +0 -8
- package/esm/ui/App/JBrowseViewPanel.js +0 -40
- package/esm/ui/App/JBrowseViewTab.d.ts +0 -8
- package/esm/ui/App/JBrowseViewTab.js +0 -97
- package/esm/ui/App/ModalWidget.d.ts +0 -6
- package/esm/ui/App/ModalWidget.js +0 -48
- package/esm/ui/App/TiledViewsContainer.d.ts +0 -6
- package/esm/ui/App/TiledViewsContainer.js +0 -366
- package/esm/ui/App/dockviewUtils.d.ts +0 -46
- package/esm/ui/App/dockviewUtils.js +0 -55
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import { createElementId } from '@jbrowse/core/util/types/mst';
|
|
2
|
+
import { cast, getSnapshot, types } from '@jbrowse/mobx-state-tree';
|
|
3
|
+
import { 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';
|
|
5
|
+
/**
|
|
6
|
+
* #stateModel WorkspaceLayoutMixin
|
|
7
|
+
*
|
|
8
|
+
* The whole workspace, in one MST tree. There is no second owner, so there is
|
|
9
|
+
* nothing to reconcile, no event to echo, and no window during which the two
|
|
10
|
+
* disagree — which is the entire content of `useDockviewController`.
|
|
11
|
+
*
|
|
12
|
+
* Four levels, matching what the workspace actually has and what a generic
|
|
13
|
+
* window manager cannot quite express:
|
|
14
|
+
*
|
|
15
|
+
* branch (a split) > panel (a grid cell) > tab > views (stacked)
|
|
16
|
+
*
|
|
17
|
+
* dockview models the first three as branch/group/panel and stops there; the
|
|
18
|
+
* vertical stack of views inside a tab is ours, which is why
|
|
19
|
+
* `panelViewAssignments` had to exist alongside dockview's own serialized grid.
|
|
20
|
+
* Here it is one tree, and a tab simply *contains* its views.
|
|
21
|
+
*
|
|
22
|
+
* Every action is `tree -> tree` through the pure functions in `tree.ts`, so
|
|
23
|
+
* undo is `applySnapshot` on this node and nothing else has to be told.
|
|
24
|
+
*/
|
|
25
|
+
const LayoutTab = types.model('LayoutTab', {
|
|
26
|
+
id: types.identifier,
|
|
27
|
+
viewIds: types.array(types.string),
|
|
28
|
+
/** set only by an explicit rename; otherwise the name is derived from views */
|
|
29
|
+
title: types.maybe(types.string),
|
|
30
|
+
});
|
|
31
|
+
const LayoutPanel = types.model('LayoutPanel', {
|
|
32
|
+
id: types.identifier,
|
|
33
|
+
size: types.optional(types.number, 1),
|
|
34
|
+
tabs: types.array(LayoutTab),
|
|
35
|
+
activeTabId: types.maybe(types.string),
|
|
36
|
+
});
|
|
37
|
+
const LayoutBranch = types.model('LayoutBranch', {
|
|
38
|
+
id: types.identifier,
|
|
39
|
+
size: types.optional(types.number, 1),
|
|
40
|
+
direction: types.enumeration('LayoutDirection', ['row', 'column']),
|
|
41
|
+
children: types.array(types.late(() => LayoutNode)),
|
|
42
|
+
});
|
|
43
|
+
// `children` is a union of both node kinds, resolved late because a branch
|
|
44
|
+
// contains branches. The dispatcher keys off `direction`, which only a branch
|
|
45
|
+
// has, so an ambiguous snapshot is impossible rather than merely unlikely.
|
|
46
|
+
const LayoutNode = types.union({
|
|
47
|
+
dispatcher: (snapshot) => snapshot.direction ? LayoutBranch : LayoutPanel,
|
|
48
|
+
}, LayoutBranch, LayoutPanel);
|
|
49
|
+
/**
|
|
50
|
+
* Panel and tab ids.
|
|
51
|
+
*
|
|
52
|
+
* Random, NOT a counter. These are `types.identifier`, so they must be unique
|
|
53
|
+
* within the tree — and a counter is reset by every page load while the restored
|
|
54
|
+
* snapshot still holds `panel-1`, `tab-1`, .... The first tab a returning user
|
|
55
|
+
* opened would mint an id the tree already had.
|
|
56
|
+
*/
|
|
57
|
+
function nextId(kind) {
|
|
58
|
+
return `${kind}-${createElementId()}`;
|
|
59
|
+
}
|
|
60
|
+
function emptyPanel() {
|
|
61
|
+
const tabId = nextId('tab');
|
|
62
|
+
return {
|
|
63
|
+
id: nextId('panel'),
|
|
64
|
+
size: 1,
|
|
65
|
+
tabs: [{ id: tabId, viewIds: [] }],
|
|
66
|
+
activeTabId: tabId,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function WorkspaceLayoutMixin() {
|
|
70
|
+
return (types
|
|
71
|
+
.model({
|
|
72
|
+
layout: types.optional(LayoutNode, emptyPanel),
|
|
73
|
+
activePanelId: types.maybe(types.string),
|
|
74
|
+
/**
|
|
75
|
+
* Show only this cell, at the size of the whole workspace.
|
|
76
|
+
*
|
|
77
|
+
* Deliberately HERE and not a `maximized` flag on `PanelNode`. On the
|
|
78
|
+
* node it would be inside `tree.ts`, the half that carries the risk and
|
|
79
|
+
* is proven by a randomised operation sequence asserting canonical form
|
|
80
|
+
* after every step — and every operation would then have to say what it
|
|
81
|
+
* does to the flag: a split of a maximized panel, a drag of its last tab
|
|
82
|
+
* out, a normalize that collapses it into its parent. Beside
|
|
83
|
+
* `activePanelId` it is the same class of thing as `activePanelId`,
|
|
84
|
+
* including its failure mode, which `livePanelIds` already repairs.
|
|
85
|
+
*/
|
|
86
|
+
maximizedPanelId: types.maybe(types.string),
|
|
87
|
+
})
|
|
88
|
+
.views(self => ({
|
|
89
|
+
/**
|
|
90
|
+
* The plain tree the pure functions take.
|
|
91
|
+
*
|
|
92
|
+
* `getSnapshot` is a `keepAlive` computed, so this is cached and
|
|
93
|
+
* referentially stable — which also lets MST's reconcile short-circuit on
|
|
94
|
+
* identity when `apply` writes an untouched subtree back.
|
|
95
|
+
*
|
|
96
|
+
* Uncast on purpose: the models below and the interfaces in `tree.ts` are
|
|
97
|
+
* two spellings of one shape, and this assignment is the only thing that
|
|
98
|
+
* checks they agree.
|
|
99
|
+
*/
|
|
100
|
+
get tree() {
|
|
101
|
+
return getSnapshot(self.layout);
|
|
102
|
+
},
|
|
103
|
+
}))
|
|
104
|
+
.views(self => ({
|
|
105
|
+
get panels() {
|
|
106
|
+
return panels(self.tree);
|
|
107
|
+
},
|
|
108
|
+
get tabs() {
|
|
109
|
+
return tabs(self.tree);
|
|
110
|
+
},
|
|
111
|
+
hasPanel(panelId) {
|
|
112
|
+
return panels(self.tree).some(p => p.id === panelId);
|
|
113
|
+
},
|
|
114
|
+
findTab(tabId) {
|
|
115
|
+
return findTab(self.tree, tabId);
|
|
116
|
+
},
|
|
117
|
+
tabContainingView(viewId) {
|
|
118
|
+
return tabContainingView(self.tree, viewId);
|
|
119
|
+
},
|
|
120
|
+
panelContainingView(viewId) {
|
|
121
|
+
return panelContainingView(self.tree, viewId);
|
|
122
|
+
},
|
|
123
|
+
/** The views a tab renders: its members, in `session.views` order. */
|
|
124
|
+
viewIdsForTab(tabId, order) {
|
|
125
|
+
const members = new Set(findTab(self.tree, tabId)?.tab.viewIds ?? []);
|
|
126
|
+
return order.filter(id => members.has(id));
|
|
127
|
+
},
|
|
128
|
+
/** The tab a panel is showing, or its first. */
|
|
129
|
+
activeTabOf(panelId) {
|
|
130
|
+
const panel = panels(self.tree).find(p => p.id === panelId);
|
|
131
|
+
return panel ? activeTabIn(panel) : undefined;
|
|
132
|
+
},
|
|
133
|
+
/**
|
|
134
|
+
* What to render: the maximized cell alone, or the whole tree.
|
|
135
|
+
*
|
|
136
|
+
* Sized to 1 rather than handed over as it sits. A pane's `size` is its
|
|
137
|
+
* share of its siblings, and CSS distributes free space by grow factor
|
|
138
|
+
* only up to a total of 1 — so a cell that was a third of a row, alone
|
|
139
|
+
* in the workspace with `flexGrow: 0.33`, draws a third of the window
|
|
140
|
+
* and leaves the rest blank.
|
|
141
|
+
*/
|
|
142
|
+
get visibleTree() {
|
|
143
|
+
const maximized = panels(self.tree).find(p => p.id === self.maximizedPanelId);
|
|
144
|
+
return maximized ? { ...maximized, size: 1 } : self.tree;
|
|
145
|
+
},
|
|
146
|
+
}))
|
|
147
|
+
.actions(self => {
|
|
148
|
+
/**
|
|
149
|
+
* Both panel ids this model holds outside the tree must name a cell
|
|
150
|
+
* that exists, whatever just stopped existing. A dangling
|
|
151
|
+
* `activePanelId` puts homed views in a cell nobody draws; a dangling
|
|
152
|
+
* `maximizedPanelId` draws nothing at all.
|
|
153
|
+
*
|
|
154
|
+
* Stated as the invariant rather than as "the panel I just closed",
|
|
155
|
+
* because a removal collapses branches on the way out and the cell that
|
|
156
|
+
* disappears is not always the one that was named.
|
|
157
|
+
*
|
|
158
|
+
* They fall back differently, and the difference is the whole reason
|
|
159
|
+
* this is two lines rather than a loop. A workspace always shows some
|
|
160
|
+
* cell, so `activePanelId` takes the first one; maximize is a mode the
|
|
161
|
+
* user is IN, so losing its cell leaves the mode rather than picking an
|
|
162
|
+
* arbitrary cell to hold the user in it.
|
|
163
|
+
*/
|
|
164
|
+
function livePanelIds() {
|
|
165
|
+
if (self.activePanelId !== undefined &&
|
|
166
|
+
!self.hasPanel(self.activePanelId)) {
|
|
167
|
+
self.activePanelId = panels(self.tree)[0]?.id;
|
|
168
|
+
}
|
|
169
|
+
if (self.maximizedPanelId !== undefined &&
|
|
170
|
+
!self.hasPanel(self.maximizedPanelId)) {
|
|
171
|
+
self.maximizedPanelId = undefined;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Every write to the tree, and therefore the one place the invariant
|
|
176
|
+
* above is repaired.
|
|
177
|
+
*
|
|
178
|
+
* It used to be a `keepActivePanel()` the two closing actions called,
|
|
179
|
+
* which was right while a closing action was the only way to retire a
|
|
180
|
+
* cell. `maximizedPanelId` is not reached that way: BOTH drop gestures
|
|
181
|
+
* prune their emptied source panel, and `applyLayoutSpec` replaces every
|
|
182
|
+
* id in the tree at once. That is five call sites for one rule, which is
|
|
183
|
+
* the shape that ends with one of them missing.
|
|
184
|
+
*/
|
|
185
|
+
function apply(next) {
|
|
186
|
+
const before = panels(self.tree).length;
|
|
187
|
+
self.layout = cast(normalize(next));
|
|
188
|
+
// A cell appearing where it cannot be seen is the one thing maximize
|
|
189
|
+
// must not do, so gaining one leaves the mode. Stated as the count
|
|
190
|
+
// rather than at the three actions that split (`splitPanel`,
|
|
191
|
+
// `dropTabInNewSplit`, `moveViewToSplitRight`) for the reason above —
|
|
192
|
+
// and a fourth, `applyLayoutSpec`, arrives at it from the other side:
|
|
193
|
+
// it replaces every id, so `livePanelIds` was going to clear the mode
|
|
194
|
+
// regardless. Losing a cell needs nothing here; that IS `livePanelIds`.
|
|
195
|
+
if (panels(self.tree).length > before) {
|
|
196
|
+
self.maximizedPanelId = undefined;
|
|
197
|
+
}
|
|
198
|
+
livePanelIds();
|
|
199
|
+
}
|
|
200
|
+
function home(tree, viewIds) {
|
|
201
|
+
return homeViews(tree, viewIds, self.activePanelId, () => nextId('tab'));
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The shape ViewMenu's two "give this view a home of its own" moves share.
|
|
205
|
+
*
|
|
206
|
+
* Homing first is what removed the old fork here — the live dockview api
|
|
207
|
+
* when the workspace was up, an `init` when it was not — because from the
|
|
208
|
+
* classic stack nothing has been assigned to a tab yet.
|
|
209
|
+
*
|
|
210
|
+
* Returns where the view came from, or `undefined` if it is not in the
|
|
211
|
+
* session, in which case nothing is applied.
|
|
212
|
+
*/
|
|
213
|
+
function rehomeView(viewId, allViewIds, place) {
|
|
214
|
+
const tree = home(self.tree, allViewIds);
|
|
215
|
+
const from = tabContainingView(tree, viewId);
|
|
216
|
+
if (!from) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
const placed = place(removeView(tree, viewId), from);
|
|
220
|
+
apply(pruneEmptyTabIn(placed, from.panel.id, from.tab.id));
|
|
221
|
+
return from;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The panel a dropped tab is leaving, or `undefined` if the drop cannot
|
|
225
|
+
* happen — which BOTH drop gestures have to establish before they touch
|
|
226
|
+
* the tree, and for two different reasons. A missing tab leaves an
|
|
227
|
+
* edge-drop's new cell behind empty; a missing target splits nothing while
|
|
228
|
+
* the gesture goes on to point `activePanelId` at a cell nobody draws, and
|
|
229
|
+
* makes `moveTabToPanel`'s remove-then-insert a deletion.
|
|
230
|
+
*
|
|
231
|
+
* One place, so the next drop gesture inherits the rule rather than
|
|
232
|
+
* restating it — the two of them restating it is how one came to be
|
|
233
|
+
* missing half of it.
|
|
234
|
+
*/
|
|
235
|
+
function dropSource(tabId, targetPanelId) {
|
|
236
|
+
const source = findTab(self.tree, tabId)?.panel;
|
|
237
|
+
return source && self.hasPanel(targetPanelId) ? source : undefined;
|
|
238
|
+
}
|
|
239
|
+
// `apply` is deliberately not returned: it takes a whole tree, so as an
|
|
240
|
+
// action it is a public "set the layout to this" on the session.
|
|
241
|
+
return {
|
|
242
|
+
setActivePanelId(panelId) {
|
|
243
|
+
self.activePanelId = panelId;
|
|
244
|
+
},
|
|
245
|
+
/**
|
|
246
|
+
* Show one cell at the size of the workspace, or go back.
|
|
247
|
+
*
|
|
248
|
+
* A toggle rather than a pair, because the gesture is a toggle: the
|
|
249
|
+
* strip's double-click and the cell menu's one item both mean "this
|
|
250
|
+
* cell, or not any more". Maximizing a DIFFERENT cell while one is
|
|
251
|
+
* already maximized moves the mode rather than restoring, which is
|
|
252
|
+
* what the menu item on another cell's strip is asking for.
|
|
253
|
+
*
|
|
254
|
+
* Mounts no views that were not mounted — it is the same cell showing
|
|
255
|
+
* the same tab — and unmounts every other cell's, so the WebGL2
|
|
256
|
+
* context ceiling (`agent-docs/reference/GPU_CONTEXT_BUDGET.md`) can
|
|
257
|
+
* only go down. That is the reason it is this and not a `display:
|
|
258
|
+
* none` over a still-mounted workspace.
|
|
259
|
+
*/
|
|
260
|
+
toggleMaximizedPanel(panelId) {
|
|
261
|
+
if (!self.hasPanel(panelId)) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
self.maximizedPanelId =
|
|
265
|
+
self.maximizedPanelId === panelId ? undefined : panelId;
|
|
266
|
+
if (self.maximizedPanelId !== undefined) {
|
|
267
|
+
self.activePanelId = panelId;
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
restorePanels() {
|
|
271
|
+
self.maximizedPanelId = undefined;
|
|
272
|
+
},
|
|
273
|
+
setActiveTab(panelId, tabId) {
|
|
274
|
+
apply(setActiveTab(self.tree, panelId, tabId));
|
|
275
|
+
self.activePanelId = panelId;
|
|
276
|
+
},
|
|
277
|
+
renameTab(tabId, title) {
|
|
278
|
+
apply(renameTab(self.tree, tabId, title));
|
|
279
|
+
},
|
|
280
|
+
/** Split a grid cell; the new cell gets one empty tab. */
|
|
281
|
+
splitPanel(panelId, direction, before = false) {
|
|
282
|
+
const panel = emptyPanel();
|
|
283
|
+
apply(splitPanel(self.tree, panelId, direction, panel, before));
|
|
284
|
+
// A split of a cell that is not there inserts nothing, so claiming
|
|
285
|
+
// the id anyway would leave activePanelId naming a cell nobody draws
|
|
286
|
+
// — and homing falls back on activePanelId.
|
|
287
|
+
if (!self.hasPanel(panel.id)) {
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
self.activePanelId = panel.id;
|
|
291
|
+
return panel;
|
|
292
|
+
},
|
|
293
|
+
closePanel(panelId) {
|
|
294
|
+
apply(removePanel(self.tree, panelId));
|
|
295
|
+
},
|
|
296
|
+
/** "New empty tab": a tab in an existing cell, showing the launcher. */
|
|
297
|
+
addTab(panelId, viewIds = []) {
|
|
298
|
+
if (!self.hasPanel(panelId)) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
const tab = { id: nextId('tab'), viewIds };
|
|
302
|
+
apply(addTab(self.tree, panelId, tab));
|
|
303
|
+
self.activePanelId = panelId;
|
|
304
|
+
return tab;
|
|
305
|
+
},
|
|
306
|
+
/**
|
|
307
|
+
* Close a tab, and the cell with it if that was its last.
|
|
308
|
+
*
|
|
309
|
+
* A cell whose tabs are all gone is the state `pruneEmptyPanel` was
|
|
310
|
+
* written for — "dragging the last tab out of a split and leaving a
|
|
311
|
+
* blank half is the one place an empty panel is clearly not what was
|
|
312
|
+
* meant" — and closing that tab arrives at the identical half by a
|
|
313
|
+
* different gesture. It rendered nothing at all, not even the launcher
|
|
314
|
+
* an empty TAB shows, so the only way back out of it was the `+`.
|
|
315
|
+
*
|
|
316
|
+
* `pruneEmptyPanel` carries both guards already: a cell with tabs left
|
|
317
|
+
* stays, and the last cell in the workspace stays whatever happens to
|
|
318
|
+
* it, since there is nowhere for the tree to collapse to.
|
|
319
|
+
*/
|
|
320
|
+
closeTab(tabId) {
|
|
321
|
+
const panelId = findTab(self.tree, tabId)?.panel.id;
|
|
322
|
+
if (panelId === undefined) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
apply(pruneEmptyPanel(removeTab(self.tree, tabId), panelId));
|
|
326
|
+
},
|
|
327
|
+
addViewToTab(tabId, viewId) {
|
|
328
|
+
apply(addViewToTab(self.tree, tabId, viewId));
|
|
329
|
+
},
|
|
330
|
+
// NO `removeView` HERE. This mixin is composed into the session, where
|
|
331
|
+
// `removeView(view)` is already the action that takes a view out of
|
|
332
|
+
// `session.views` — part of AbstractSessionModel, and what every close
|
|
333
|
+
// button and every host calls. A same-named action here does not extend
|
|
334
|
+
// it, it *replaces* it (types.compose merges, last one wins), so the
|
|
335
|
+
// session action stopped being reachable at all: closing a view pruned
|
|
336
|
+
// the layout tree and left the view in the session forever, and
|
|
337
|
+
// `session.removeView(view)` passed a model where this wanted an id, so
|
|
338
|
+
// it matched nothing and even the pruning was a no-op.
|
|
339
|
+
//
|
|
340
|
+
// Nothing needs one either way: `home` below drops any tab entry whose
|
|
341
|
+
// view is no longer in `session.views`, so removing the view is the
|
|
342
|
+
// whole operation and the tree follows.
|
|
343
|
+
/**
|
|
344
|
+
* Drop a dragged tab into an existing panel, as a tab.
|
|
345
|
+
*
|
|
346
|
+
* One action, so the tree never exists in a state where the tab is in
|
|
347
|
+
* both panels or neither. The imperative bridge needed an explicit
|
|
348
|
+
* `runInAction` around the unassign+reassign pair for exactly this, and
|
|
349
|
+
* a comment explaining that without it the reconcile autorun would
|
|
350
|
+
* observe the gap and re-home the view.
|
|
351
|
+
*/
|
|
352
|
+
dropTabInPanel(tabId, targetPanelId, index) {
|
|
353
|
+
const source = dropSource(tabId, targetPanelId);
|
|
354
|
+
if (!source) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
// A drop on the BODY of the cell the tab is already in asks for
|
|
358
|
+
// nothing. There is no gap under the pointer to state a position, and
|
|
359
|
+
// the indicator washes the whole cell — which says "be a tab of this
|
|
360
|
+
// cell", which it already is. Appending reordered the strip to say
|
|
361
|
+
// something the gesture never said, and sent the tab to the end.
|
|
362
|
+
// dockview declines a centre drop on the group a tab came from too.
|
|
363
|
+
//
|
|
364
|
+
// The rule is the gesture's, not `moveTabToPanel`'s: no index there
|
|
365
|
+
// still means append, which is the only reading a total function has.
|
|
366
|
+
if (source.id === targetPanelId && index === undefined) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
let next = moveTabToPanel(self.tree, tabId, targetPanelId, index);
|
|
370
|
+
if (source.id !== targetPanelId) {
|
|
371
|
+
next = pruneEmptyPanel(next, source.id);
|
|
372
|
+
}
|
|
373
|
+
apply(next);
|
|
374
|
+
self.activePanelId = targetPanelId;
|
|
375
|
+
},
|
|
376
|
+
/** Drop a dragged tab on a panel edge: split, and land in the new half. */
|
|
377
|
+
dropTabInNewSplit(tabId, targetPanelId, direction, before) {
|
|
378
|
+
const source = dropSource(tabId, targetPanelId);
|
|
379
|
+
if (!source) {
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
const panel = {
|
|
383
|
+
id: nextId('panel'),
|
|
384
|
+
size: 1,
|
|
385
|
+
tabs: [],
|
|
386
|
+
};
|
|
387
|
+
let next = splitPanel(self.tree, targetPanelId, direction, panel, before);
|
|
388
|
+
next = moveTabToPanel(next, tabId, panel.id);
|
|
389
|
+
// Dragging a panel's only tab onto that same panel's edge prunes the
|
|
390
|
+
// now-empty source, which collapses the split — the gesture undoes
|
|
391
|
+
// itself rather than leaving a blank half, without needing a case.
|
|
392
|
+
next = pruneEmptyPanel(next, source.id);
|
|
393
|
+
apply(next);
|
|
394
|
+
self.activePanelId = panel.id;
|
|
395
|
+
return panel.id;
|
|
396
|
+
},
|
|
397
|
+
setSizes(branchId, sizes) {
|
|
398
|
+
apply(setSizes(self.tree, branchId, sizes));
|
|
399
|
+
},
|
|
400
|
+
/**
|
|
401
|
+
* Arrange the workspace as a spec states.
|
|
402
|
+
*
|
|
403
|
+
* There is no `init` property and no standing request: the spec is
|
|
404
|
+
* converted and *becomes* the layout, here and now. `init` existed only
|
|
405
|
+
* because dockview had to be told, could not be told before it mounted,
|
|
406
|
+
* and had to be told again afterwards — three problems that all came
|
|
407
|
+
* from the layout living somewhere this action could not reach.
|
|
408
|
+
*/
|
|
409
|
+
applyLayoutSpec(spec) {
|
|
410
|
+
apply(treeFromSpec(spec, nextId));
|
|
411
|
+
self.activePanelId = panels(self.tree)[0]?.id;
|
|
412
|
+
return viewIdsInSpec(spec);
|
|
413
|
+
},
|
|
414
|
+
/**
|
|
415
|
+
* ViewMenu's "move to new tab": the view leaves its tab for a new one.
|
|
416
|
+
*
|
|
417
|
+
* `allViewIds` is EVERY view in the session, and is required for that
|
|
418
|
+
* reason — homing drops any view the list does not name, so the
|
|
419
|
+
* `[viewId]` default this used to carry unhomed all the others.
|
|
420
|
+
*/
|
|
421
|
+
moveViewToNewTab(viewId, allViewIds) {
|
|
422
|
+
const tab = { id: nextId('tab'), viewIds: [viewId] };
|
|
423
|
+
const from = rehomeView(viewId, allViewIds, (tree, at) => addTab(tree, at.panel.id, tab));
|
|
424
|
+
if (!from) {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
self.activePanelId = from.panel.id;
|
|
428
|
+
return tab.id;
|
|
429
|
+
},
|
|
430
|
+
/**
|
|
431
|
+
* ViewMenu's "move to split view": the view leaves for a new cell.
|
|
432
|
+
* `allViewIds` is every view in the session — see `moveViewToNewTab`.
|
|
433
|
+
*/
|
|
434
|
+
moveViewToSplitRight(viewId, allViewIds) {
|
|
435
|
+
const tabId = nextId('tab');
|
|
436
|
+
const panel = {
|
|
437
|
+
id: nextId('panel'),
|
|
438
|
+
size: 1,
|
|
439
|
+
tabs: [{ id: tabId, viewIds: [viewId] }],
|
|
440
|
+
activeTabId: tabId,
|
|
441
|
+
};
|
|
442
|
+
const from = rehomeView(viewId, allViewIds, (tree, at) => splitPanel(tree, at.panel.id, 'row', panel));
|
|
443
|
+
if (!from) {
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
self.activePanelId = panel.id;
|
|
447
|
+
return panel.id;
|
|
448
|
+
},
|
|
449
|
+
homeUnassignedViews(viewIds) {
|
|
450
|
+
apply(home(self.tree, viewIds));
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
})
|
|
454
|
+
/**
|
|
455
|
+
* The two sugars, in their own block so they can reach the actions they are
|
|
456
|
+
* sugar FOR through `self`.
|
|
457
|
+
*
|
|
458
|
+
* `this.applyLayoutSpec()` reaches sideways into the action literal the
|
|
459
|
+
* block returned — the fork's `instantiateActions` does `fn.bind(actions)`
|
|
460
|
+
* — so it is pinned to that block's implementation for good. A later block,
|
|
461
|
+
* or a plugin's `extendStateModel`, replaces `self.applyLayoutSpec` and the
|
|
462
|
+
* sideways hop goes on calling the one it replaced, with no error and no
|
|
463
|
+
* type complaint. An extra layer costs a line.
|
|
464
|
+
*/
|
|
465
|
+
.actions(self => ({
|
|
466
|
+
/**
|
|
467
|
+
* Move one view relative to the others. PUBLIC API: an external plugin
|
|
468
|
+
* calls this behind a `'setPendingMove' in session` guard
|
|
469
|
+
* (jbrowse-plugin-protein3d, putting a protein view beside its genome
|
|
470
|
+
* view). It survived the last storage change by being kept as sugar, and
|
|
471
|
+
* it survives this one the same way — a capability-detecting caller
|
|
472
|
+
* cannot tell you it lost a capability.
|
|
473
|
+
*
|
|
474
|
+
* **`allViewIds` is therefore OPTIONAL, and has to stay that way.** The
|
|
475
|
+
* plugin passes the move alone, because that was the whole signature when
|
|
476
|
+
* its call site was written; requiring the second argument threw
|
|
477
|
+
* `undefined.filter` out of a launch the plugin does not wrap, and the
|
|
478
|
+
* figure was again the only thing that noticed. Keeping the NAME is half
|
|
479
|
+
* of not breaking a runtime lookup — the call has to keep working as it
|
|
480
|
+
* is spelled.
|
|
481
|
+
*/
|
|
482
|
+
setPendingMove(move, allViewIds) {
|
|
483
|
+
// The session's whole set of views, for the one entry point that cannot
|
|
484
|
+
// be handed it. Every other one takes it as an argument on purpose —
|
|
485
|
+
// this mixin owns the tree and knows nothing else — and this would too,
|
|
486
|
+
// if its caller were ours. Duck-typed because `views` is
|
|
487
|
+
// MultipleViewsSessionMixin's and composition is the only thing that
|
|
488
|
+
// puts the two together.
|
|
489
|
+
const { views } = self;
|
|
490
|
+
// Not `?? []`: homing drops every view the list does not name, so an
|
|
491
|
+
// empty one would answer "put this view beside nothing" and unhome the
|
|
492
|
+
// rest. Nothing to say is nothing to do.
|
|
493
|
+
const ids = allViewIds ?? views?.map(v => v.id);
|
|
494
|
+
if (!move || !ids) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
self.applyLayoutSpec(specForPendingMove(move, ids));
|
|
498
|
+
// Show where the view went. A spec states an arrangement and not a
|
|
499
|
+
// selection, so `treeFromSpec` shows each cell's FIRST tab — and
|
|
500
|
+
// `newTab` puts the moved view in a tab beside the others, which makes
|
|
501
|
+
// it the one tab nobody can see. The plugin asking for this is asking
|
|
502
|
+
// for its view to be on screen; `moveViewToNewTab`, the same gesture
|
|
503
|
+
// from the View menu, has always ended with it there.
|
|
504
|
+
const home = tabContainingView(self.tree, move.viewId);
|
|
505
|
+
if (home) {
|
|
506
|
+
self.setActiveTab(home.panel.id, home.tab.id);
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
/**
|
|
510
|
+
* The whole-workspace re-arrange: every view one cell, in one of four
|
|
511
|
+
* shapes. Restored from the dockview header's four "Global:" commands,
|
|
512
|
+
* which went with that component and were not reimplemented.
|
|
513
|
+
*
|
|
514
|
+
* `allViewIds` is passed in rather than read off the session for the same
|
|
515
|
+
* reason `moveViewToNewTab` takes it: this mixin owns the tree and has no
|
|
516
|
+
* view list of its own. Passing `session.views` order means the
|
|
517
|
+
* arrangement it states is already the order views render in, so unlike a
|
|
518
|
+
* session spec's layout there is nothing for `orderViews` to apply.
|
|
519
|
+
*/
|
|
520
|
+
tileViews(mode, allViewIds) {
|
|
521
|
+
self.applyLayoutSpec(tileLayoutSpec(allViewIds, mode));
|
|
522
|
+
},
|
|
523
|
+
})));
|
|
524
|
+
}
|
|
525
|
+
export function isSessionWithWorkspaceLayout(session) {
|
|
526
|
+
return 'layout' in session && 'splitPanel' in session;
|
|
527
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PanelNode, TabNode } from './tree.ts';
|
|
2
|
+
import type { TabDragHandlers } from './useLayoutDrag.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The app's half of a panel — what a tab is *called* and what it *contains*.
|
|
5
|
+
*
|
|
6
|
+
* One object rather than five props because every one is forwarded unchanged
|
|
7
|
+
* down the whole recursion, and because one object can be memoised: it reaches
|
|
8
|
+
* every panel, so a fresh one per render re-renders the entire workspace.
|
|
9
|
+
*/
|
|
10
|
+
export interface PanelChrome {
|
|
11
|
+
renderTabLabel: (tab: TabNode) => React.ReactNode;
|
|
12
|
+
renderTabContent: (tab: TabNode) => React.ReactNode;
|
|
13
|
+
renderPanelActions?: (panel: PanelNode) => React.ReactNode;
|
|
14
|
+
dragHandlers: TabDragHandlers;
|
|
15
|
+
/** middle-click; the caller pairs it with closing that tab's views */
|
|
16
|
+
onTabClose?: (tabId: string) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare const tabDomId: (tabId: string) => string;
|
|
19
|
+
export declare const tabPanelDomId: (panelId: string) => string;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// The tablist wires `aria-controls`/`aria-labelledby` with these, and the strip
|
|
2
|
+
// mints one end of each pair while the tabpanel mints the other.
|
|
3
|
+
export const tabDomId = (tabId) => `jbrowse-tab-${tabId}`;
|
|
4
|
+
export const tabPanelDomId = (panelId) => `jbrowse-tabpanel-${panelId}`;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { LayoutTree, NodeKind } from './tree.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The `layout` a session spec / URL param states, in the vocabulary users
|
|
4
|
+
* write it in. **This shape is public** — it is documented as a URL parameter —
|
|
5
|
+
* so it keeps `horizontal`/`vertical`/`tabs` and percentage `size` rather than
|
|
6
|
+
* being renamed to match the internal tree.
|
|
7
|
+
*/
|
|
8
|
+
export interface LayoutSpecNode {
|
|
9
|
+
/** a leaf: the views to stack vertically in one tab */
|
|
10
|
+
viewIds?: string[];
|
|
11
|
+
/** how children divide the space; `tabs` puts them in one cell as tabs */
|
|
12
|
+
direction?: 'horizontal' | 'vertical' | 'tabs';
|
|
13
|
+
children?: LayoutSpecNode[];
|
|
14
|
+
/** share of the parent, as a percentage */
|
|
15
|
+
size?: number;
|
|
16
|
+
}
|
|
17
|
+
/** A request to move one view relative to the others. Public plugin API. */
|
|
18
|
+
export interface PendingMove {
|
|
19
|
+
type: 'newTab' | 'splitRight';
|
|
20
|
+
viewId: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build a layout tree from a spec.
|
|
24
|
+
*
|
|
25
|
+
* The old converter had to answer "which dockview group does a nested split
|
|
26
|
+
* address?" and could not, because dockview forces orientation to alternate by
|
|
27
|
+
* depth — which is why `size` was honoured only on the top-level split, and
|
|
28
|
+
* only if every panel there carried one. Here the spec's nesting is the tree's
|
|
29
|
+
* nesting, so `size` applies wherever it is written and there is no all-or-
|
|
30
|
+
* nothing pass.
|
|
31
|
+
*/
|
|
32
|
+
export declare function treeFromSpec(spec: LayoutSpecNode, nextId: (kind: NodeKind) => string): LayoutTree;
|
|
33
|
+
/** Every viewId a spec names, depth-first — the order it states. */
|
|
34
|
+
export declare function viewIdsInSpec(spec: LayoutSpecNode): string[];
|
|
35
|
+
/** How "arrange everything" lays the whole session out. */
|
|
36
|
+
export type TileMode = 'tabs' | 'horizontal' | 'vertical' | 'grid';
|
|
37
|
+
/**
|
|
38
|
+
* Re-arrange every view at once, one view per cell.
|
|
39
|
+
*
|
|
40
|
+
* The four whole-workspace commands, as a spec. dockview had these as imperative
|
|
41
|
+
* re-tiling — walk every panel and re-add it relative to the first, in an order
|
|
42
|
+
* chosen so the grid came out right — and the grid case had to compute its own
|
|
43
|
+
* row/column arithmetic against `api.panels` because there was no way to state
|
|
44
|
+
* the shape. Here the shape IS the statement, so a tiling is a spec and goes
|
|
45
|
+
* through the same `applyLayoutSpec` a session spec does.
|
|
46
|
+
*
|
|
47
|
+
* `grid` fills row-major at ceil(sqrt(n)) columns, which is what the dockview
|
|
48
|
+
* version computed; a trailing row shorter than the rest keeps its cells at full
|
|
49
|
+
* width, since `resolveSizes` reads bare siblings as an even share of their own
|
|
50
|
+
* branch rather than of the grid.
|
|
51
|
+
*/
|
|
52
|
+
export declare function tileLayoutSpec(viewIds: string[], mode: TileMode): LayoutSpecNode;
|
|
53
|
+
/**
|
|
54
|
+
* `setPendingMove` as a spec: everything else keeps its side, the named view
|
|
55
|
+
* takes the other. With nothing else on screen there is nothing to split from,
|
|
56
|
+
* so the view just takes the space.
|
|
57
|
+
*/
|
|
58
|
+
export declare function specForPendingMove(move: PendingMove, allViewIds: string[]): LayoutSpecNode;
|