@jbrowse/app-core 5.0.0-beta.11 → 5.0.0-beta.13
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/JBrowseConfig/RootConfiguration.d.ts +1 -8
- package/esm/JBrowseConfig/RootConfiguration.js +2 -6
- package/esm/JBrowseConfig/index.d.ts +1 -8
- package/esm/JBrowseModel/index.d.ts +1 -8
- package/esm/JbApi/jbApi.d.ts +2 -2
- package/esm/JbApi/jbApi.js +12 -10
- package/esm/SessionSpec/loadSessionSpec.js +2 -12
- 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 -34
- package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +1 -16
- package/esm/WorkspaceLayout/WorkspacePanelActions.js +45 -50
- package/esm/WorkspaceLayout/WorkspaceTab.d.ts +1 -16
- package/esm/WorkspaceLayout/WorkspaceTab.js +11 -47
- package/esm/WorkspaceLayout/dropZone.d.ts +7 -37
- package/esm/WorkspaceLayout/dropZone.js +7 -44
- package/esm/WorkspaceLayout/model.d.ts +42 -112
- package/esm/WorkspaceLayout/model.js +139 -268
- 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 -76
- package/esm/WorkspaceLayout/tree.js +29 -122
- package/esm/WorkspaceLayout/useLayoutDrag.d.ts +4 -22
- package/esm/WorkspaceLayout/useLayoutDrag.js +46 -174
- 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/ui/App/App.js +3 -0
- package/esm/ui/App/JBrowseTabMenu.js +1 -1
- package/esm/ui/App/ThemeModeButton.js +5 -4
- package/esm/ui/App/ViewMenu.js +47 -29
- 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/package.json +4 -4
- 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
|
@@ -2,37 +2,23 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
4
4
|
import { observer } from 'mobx-react';
|
|
5
|
-
import { dv, tabColors } from './dockviewTheme.js';
|
|
6
5
|
import { tabDomId, tabPanelDomId } from './panelChrome.js';
|
|
7
|
-
|
|
8
|
-
* A panel's tab strip: the tabs, and the panel's own buttons beside them.
|
|
9
|
-
*
|
|
10
|
-
* Everything with state in a cell lives here — the roving tabindex, the wheel
|
|
11
|
-
* translation, the scroll-into-view — which leaves `PanelView` a pure function
|
|
12
|
-
* of its node. The strip is dockview's dark theme, transcribed in
|
|
13
|
-
* `dockviewTheme.ts`; see there for why it is fixed rather than derived from
|
|
14
|
-
* the MUI theme.
|
|
15
|
-
*/
|
|
6
|
+
import { tabColors, workspaceTheme } from './workspaceTheme.js';
|
|
16
7
|
const useStyles = makeStyles()({
|
|
17
8
|
strip: {
|
|
18
9
|
display: 'flex',
|
|
19
10
|
flexShrink: 0,
|
|
20
11
|
boxSizing: 'border-box',
|
|
21
|
-
height:
|
|
22
|
-
fontSize:
|
|
23
|
-
background:
|
|
12
|
+
height: workspaceTheme.stripHeight,
|
|
13
|
+
fontSize: workspaceTheme.stripFontSize,
|
|
14
|
+
background: workspaceTheme.stripBackground,
|
|
24
15
|
},
|
|
25
16
|
tabs: {
|
|
26
17
|
display: 'flex',
|
|
27
18
|
overflowX: 'auto',
|
|
28
|
-
//
|
|
29
|
-
// so a tab list that takes the leftover space pushes the `+` to the far
|
|
30
|
-
// right edge, arbitrarily far from the tab it acts on. `0 1 auto` sizes the
|
|
31
|
-
// list to its tabs and leaves the `+` beside the last one, and the shrink
|
|
32
|
-
// half is what makes it scroll here rather than push the `+` off the strip.
|
|
19
|
+
// never grow, so the `+` after the list sits beside the last tab
|
|
33
20
|
flex: '0 1 auto',
|
|
34
21
|
minWidth: 0,
|
|
35
|
-
// the strip is chrome, so a scrollbar across it would be noise
|
|
36
22
|
scrollbarWidth: 'none',
|
|
37
23
|
'&::-webkit-scrollbar': { display: 'none' },
|
|
38
24
|
},
|
|
@@ -46,31 +32,16 @@ const useStyles = makeStyles()({
|
|
|
46
32
|
cursor: 'pointer',
|
|
47
33
|
userSelect: 'none',
|
|
48
34
|
touchAction: 'none',
|
|
49
|
-
borderRight: `1px solid ${
|
|
35
|
+
borderRight: `1px solid ${workspaceTheme.tabDivider}`,
|
|
50
36
|
'&:hover .jbrowse-tab-menu': { visibility: 'visible' },
|
|
51
|
-
// a keyboard user needs the ⋮ too, and hover is not a thing they can do
|
|
52
37
|
'&:focus-within .jbrowse-tab-menu': { visibility: 'visible' },
|
|
53
|
-
// the strip is dark in either theme, so the focus ring is the drop
|
|
54
|
-
// indicator's blue rather than the UA default black-on-dark
|
|
55
38
|
'&:focus-visible': {
|
|
56
|
-
outline: `2px solid ${
|
|
39
|
+
outline: `2px solid ${workspaceTheme.accent}`,
|
|
57
40
|
outlineOffset: -2,
|
|
58
41
|
},
|
|
59
42
|
},
|
|
60
43
|
});
|
|
61
|
-
|
|
62
|
-
* A wheel event's delta in PIXELS.
|
|
63
|
-
*
|
|
64
|
-
* `deltaY` is only pixels when `deltaMode` says so. Firefox reports whole lines
|
|
65
|
-
* for a mouse wheel — `deltaMode: 1`, `deltaY: ±3` — where Chrome reports
|
|
66
|
-
* `deltaMode: 0`, `deltaY: ±100`. Using the raw number moves the strip three
|
|
67
|
-
* pixels per notch there: scrolling, technically, and unusable in practice.
|
|
68
|
-
*
|
|
69
|
-
* The line height is nominal rather than measured. `getComputedStyle` per wheel
|
|
70
|
-
* event to resolve a `line-height: normal` that is itself font-dependent buys
|
|
71
|
-
* accuracy nobody can perceive in a scroll gesture, and the strip is a fixed
|
|
72
|
-
* 35px of chrome in one size.
|
|
73
|
-
*/
|
|
44
|
+
// Firefox reports a mouse wheel in lines (`deltaMode: 1`, `deltaY: ±3`)
|
|
74
45
|
const WHEEL_LINE_PX = 16;
|
|
75
46
|
function wheelDeltaPixels(event, pageSize) {
|
|
76
47
|
switch (event.deltaMode) {
|
|
@@ -82,40 +53,23 @@ function wheelDeltaPixels(event, pageSize) {
|
|
|
82
53
|
return event.deltaY;
|
|
83
54
|
}
|
|
84
55
|
}
|
|
85
|
-
export const TabStrip = observer(function TabStrip({ panel, layout, chrome, active,
|
|
56
|
+
export const TabStrip = observer(function TabStrip({ panel, layout, chrome, active, cellActive, }) {
|
|
86
57
|
const { classes } = useStyles();
|
|
87
58
|
const { renderPanelActions } = chrome;
|
|
88
|
-
//
|
|
89
|
-
// it. Focus is tracked separately from selection because activation here is
|
|
90
|
-
// manual (see `onKeyDown`), so the two genuinely differ while arrowing.
|
|
59
|
+
// roving tabindex; focus differs from selection because activation is manual
|
|
91
60
|
const stripRef = useRef(null);
|
|
92
61
|
const [focusedTabId, setFocusedTabId] = useState(undefined);
|
|
93
62
|
const roving = panel.tabs.find(t => t.id === focusedTabId)?.id ?? active?.id;
|
|
94
|
-
//
|
|
95
|
-
// nanoid output and would need CSS.escape, which jsdom does not have
|
|
63
|
+
// not an attribute selector: a nanoid needs CSS.escape, which jsdom lacks
|
|
96
64
|
function tabElement(tabId) {
|
|
97
65
|
return [...(stripRef.current?.children ?? [])].find(child => child.dataset.tabId === tabId);
|
|
98
66
|
}
|
|
99
67
|
function focusTab(tabId) {
|
|
100
68
|
setFocusedTabId(tabId);
|
|
101
|
-
// focusing scrolls it into view on its own, which is the whole reason the
|
|
102
|
-
// keyboard could always reach an overflowing strip when the mouse could not
|
|
103
69
|
tabElement(tabId)?.focus();
|
|
104
70
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
*
|
|
108
|
-
* The strip scrolls, so a tab can become current while sitting outside it —
|
|
109
|
-
* `+` on a full strip appends a tab, makes it active, and leaves the user
|
|
110
|
-
* looking at the tabs it scrolled past. Clicking never needs this (you
|
|
111
|
-
* clicked something visible) and arrowing gets it from `focus()`, so this is
|
|
112
|
-
* for the tab that becomes current without being touched.
|
|
113
|
-
*
|
|
114
|
-
* `block: 'nearest'` so it cannot scroll an ancestor vertically. jsdom has no
|
|
115
|
-
* scrollIntoView at all, but `config/jest/scrollIntoView.js` already no-ops it
|
|
116
|
-
* for the whole suite — don't add an optional call for that, it reads as a
|
|
117
|
-
* browser that might not have the method.
|
|
118
|
-
*/
|
|
71
|
+
// for a tab made current without being touched, e.g. by `+` on a full strip;
|
|
72
|
+
// `block: 'nearest'` keeps it from scrolling an ancestor vertically
|
|
119
73
|
useEffect(() => {
|
|
120
74
|
tabElement(active?.id)?.scrollIntoView({
|
|
121
75
|
block: 'nearest',
|
|
@@ -123,14 +77,9 @@ export const TabStrip = observer(function TabStrip({ panel, layout, chrome, acti
|
|
|
123
77
|
});
|
|
124
78
|
}, [active?.id]);
|
|
125
79
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* of the tabs pattern, and is wrong here for the reason WAI-ARIA names as its
|
|
130
|
-
* exception — showing a tab is expensive. Only the selected tab's views are
|
|
131
|
-
* mounted, and a JBrowse view costs a WebGL2 context per display against a
|
|
132
|
-
* ceiling of 16, so arrowing across five tabs would build and tear down five
|
|
133
|
-
* sets of them to pass through.
|
|
80
|
+
* Manual activation (arrows focus, Enter/Space selects), the WAI-ARIA
|
|
81
|
+
* exception for expensive tabs: showing one mounts views that each cost
|
|
82
|
+
* WebGL2 contexts.
|
|
134
83
|
*/
|
|
135
84
|
function onKeyDown(event, tabId) {
|
|
136
85
|
const ids = panel.tabs.map(t => t.id);
|
|
@@ -157,70 +106,53 @@ export const TabStrip = observer(function TabStrip({ panel, layout, chrome, acti
|
|
|
157
106
|
}
|
|
158
107
|
}
|
|
159
108
|
return (_jsxs("div", { "data-tab-strip": true, className: classes.strip,
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
// only other `onDoubleClick` is the one on a tab's own label, for rename.
|
|
163
|
-
//
|
|
164
|
-
// `target === currentTarget` is what keeps those two apart, and it is the
|
|
165
|
-
// test rather than a `stopPropagation` in `WorkspaceTab` because it holds
|
|
166
|
-
// for anything ever put on the strip: a rename double-click bubbles out
|
|
167
|
-
// of the label to here, and so would a double-click on the `+`.
|
|
109
|
+
// empty strip space only, so a tab's rename double-click does not bubble
|
|
110
|
+
// into a maximize
|
|
168
111
|
onDoubleClick: event => {
|
|
169
112
|
if (event.target === event.currentTarget) {
|
|
170
113
|
layout.toggleMaximizedPanel(panel.id);
|
|
171
114
|
}
|
|
172
|
-
}, children: [_jsx("div", { role: "tablist", ref: stripRef, className: classes.tabs,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
// `deltaX` is a trackpad's own horizontal axis and the browser has
|
|
180
|
-
// already applied it; taking the larger axis leaves that gesture alone
|
|
181
|
-
// rather than doubling it.
|
|
115
|
+
}, children: [_jsx("div", { role: "tablist", ref: stripRef, className: classes.tabs, onBlur: event => {
|
|
116
|
+
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
117
|
+
setFocusedTabId(undefined);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
// translate a vertical wheel to horizontal scroll; a trackpad's larger
|
|
121
|
+
// `deltaX` is already applied by the browser
|
|
182
122
|
onWheel: event => {
|
|
183
123
|
const el = stripRef.current;
|
|
184
124
|
if (!el || Math.abs(event.deltaX) > Math.abs(event.deltaY)) {
|
|
185
125
|
return;
|
|
186
126
|
}
|
|
187
127
|
el.scrollLeft += wheelDeltaPixels(event, el.clientWidth);
|
|
188
|
-
}, children: panel.tabs.map(tab => (_jsx(Tab, { tab: tab, panel: panel, layout: layout, chrome: chrome, className: classes.tab, selected: tab.id === active?.id,
|
|
128
|
+
}, children: panel.tabs.map(tab => (_jsx(Tab, { tab: tab, panel: panel, layout: layout, chrome: chrome, className: classes.tab, selected: tab.id === active?.id, cellActive: cellActive, tabIndex: tab.id === roving ? 0 : -1, onKeyDown: onKeyDown, onFocus: setFocusedTabId }, tab.id))) }), renderPanelActions?.(panel)] }));
|
|
189
129
|
});
|
|
190
|
-
const Tab = observer(function Tab({ tab, panel, layout, chrome, className, selected,
|
|
130
|
+
const Tab = observer(function Tab({ tab, panel, layout, chrome, className, selected, cellActive, tabIndex, onKeyDown, onFocus, }) {
|
|
191
131
|
const { renderTabLabel, dragHandlers, onTabClose } = chrome;
|
|
192
132
|
return (_jsx("div", { role: "tab", id: tabDomId(tab.id), "data-tab-id": tab.id, "aria-selected": selected, "aria-controls": selected ? tabPanelDomId(panel.id) : undefined, tabIndex: tabIndex, onKeyDown: event => {
|
|
193
|
-
|
|
133
|
+
// keys from a control inside the tab belong to that control
|
|
134
|
+
if (event.target === event.currentTarget) {
|
|
135
|
+
onKeyDown(event, tab.id);
|
|
136
|
+
}
|
|
194
137
|
}, onFocus: () => {
|
|
195
138
|
onFocus(tab.id);
|
|
196
139
|
}, onPointerDown: event => {
|
|
197
|
-
//
|
|
198
|
-
// the
|
|
199
|
-
// tab is middle-pressed.
|
|
200
|
-
//
|
|
201
|
-
// Cancelling pointerdown suppresses the compatibility mouse events
|
|
202
|
-
// (which is what stops the autoscroll) and NOT `click` / `auxclick`:
|
|
203
|
-
// Pointer Events L3 dispatches those two independently of that mapping.
|
|
204
|
-
// So the close below still fires, which is the thing this looks like it
|
|
205
|
-
// would break.
|
|
140
|
+
// Stops middle-button autoscroll; Pointer Events L3 still dispatches
|
|
141
|
+
// the `auxclick` that closes the tab
|
|
206
142
|
if (event.button === 1) {
|
|
207
143
|
event.preventDefault();
|
|
208
144
|
return;
|
|
209
145
|
}
|
|
210
|
-
//
|
|
211
|
-
// costing a WebGL2 context — so it is the LEFT button that asks for it,
|
|
212
|
-
// the same gate dockview puts on `_activateOnPointerDown`. A right-press
|
|
213
|
-
// is on its way to a context menu and is not a request to look at
|
|
214
|
-
// anything.
|
|
146
|
+
// only the left button activates: showing a tab mounts views
|
|
215
147
|
if (event.button !== 0) {
|
|
216
148
|
return;
|
|
217
149
|
}
|
|
218
150
|
layout.setActiveTab(panel.id, tab.id);
|
|
219
|
-
dragHandlers.
|
|
151
|
+
dragHandlers.onPointerDown(event);
|
|
220
152
|
}, onAuxClick: event => {
|
|
221
153
|
if (event.button === 1) {
|
|
222
154
|
event.preventDefault();
|
|
223
155
|
onTabClose?.(tab.id);
|
|
224
156
|
}
|
|
225
|
-
}, onPointerMove: dragHandlers.
|
|
157
|
+
}, onPointerMove: dragHandlers.onPointerMove, onPointerUp: dragHandlers.onPointerUp, onPointerCancel: dragHandlers.onPointerCancel, onLostPointerCapture: dragHandlers.onLostPointerCapture, className: className, style: tabColors(cellActive, selected), children: renderTabLabel(tab) }));
|
|
226
158
|
});
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import type { WorkspaceSessionType } from '../ui/App/types.ts';
|
|
2
2
|
import type { WorkspaceLayout } from './model.ts';
|
|
3
3
|
type WorkspaceSession = WorkspaceSessionType & WorkspaceLayout;
|
|
4
|
-
/**
|
|
5
|
-
* The workspace. Compare `TiledViewsContainer` + `useDockviewController`, which
|
|
6
|
-
* this replaces: there is no api to hold, no `onReady`, no event to subscribe
|
|
7
|
-
* to, and no reconciliation — the layout is session state and this renders it.
|
|
8
|
-
*
|
|
9
|
-
* The one reaction left is homing: `session.views` is owned by the session, so
|
|
10
|
-
* a view launched from a menu arrives belonging to no tab and has to land
|
|
11
|
-
* somewhere. That is one-directional and idempotent, and nothing reads back.
|
|
12
|
-
*/
|
|
13
4
|
export declare const WorkspaceContainer: ({ session, }: {
|
|
14
5
|
session: WorkspaceSession;
|
|
15
6
|
}) => import("react").JSX.Element;
|
|
@@ -17,11 +17,6 @@ const useStyles = makeStyles()(theme => ({
|
|
|
17
17
|
gridRow: 'components',
|
|
18
18
|
background: theme.palette.background.default,
|
|
19
19
|
},
|
|
20
|
-
stack: {
|
|
21
|
-
display: 'flex',
|
|
22
|
-
flexDirection: 'column',
|
|
23
|
-
minWidth: 0,
|
|
24
|
-
},
|
|
25
20
|
empty: {
|
|
26
21
|
display: 'flex',
|
|
27
22
|
alignItems: 'center',
|
|
@@ -29,30 +24,18 @@ const useStyles = makeStyles()(theme => ({
|
|
|
29
24
|
height: '100%',
|
|
30
25
|
},
|
|
31
26
|
}));
|
|
32
|
-
/**
|
|
33
|
-
* The workspace. Compare `TiledViewsContainer` + `useDockviewController`, which
|
|
34
|
-
* this replaces: there is no api to hold, no `onReady`, no event to subscribe
|
|
35
|
-
* to, and no reconciliation — the layout is session state and this renders it.
|
|
36
|
-
*
|
|
37
|
-
* The one reaction left is homing: `session.views` is owned by the session, so
|
|
38
|
-
* a view launched from a menu arrives belonging to no tab and has to land
|
|
39
|
-
* somewhere. That is one-directional and idempotent, and nothing reads back.
|
|
40
|
-
*/
|
|
41
27
|
export const WorkspaceContainer = observer(function WorkspaceContainer({ session, }) {
|
|
42
28
|
const { classes } = useStyles();
|
|
43
29
|
const { drag, handlers } = useLayoutDrag(session);
|
|
44
30
|
// #region autorunInEffect
|
|
45
31
|
useEffect(() => autorun(() => {
|
|
46
|
-
// reads session.views
|
|
47
|
-
//
|
|
32
|
+
// homes newly launched views; reads session.views here because an
|
|
33
|
+
// action's reads are untracked
|
|
48
34
|
session.homeUnassignedViews(session.views.map(v => v.id));
|
|
49
35
|
}), [session]);
|
|
50
36
|
// #endregion
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// chance to drop the node and leave its views in the session forever — and
|
|
54
|
-
// three gestures want it now: the tab's ⋮ menu, middle-clicking the tab, and
|
|
55
|
-
// the cell's ×.
|
|
37
|
+
// the layout does not own views, so every close gesture goes through here
|
|
38
|
+
// or leaks them
|
|
56
39
|
const closeViews = useCallback((viewIds) => {
|
|
57
40
|
for (const view of viewsOf(session, viewIds)) {
|
|
58
41
|
session.removeView(view);
|
|
@@ -74,15 +57,8 @@ export const WorkspaceContainer = observer(function WorkspaceContainer({ session
|
|
|
74
57
|
closeViews(panel.tabs.flatMap(t => t.viewIds));
|
|
75
58
|
session.closePanel(panelId);
|
|
76
59
|
}, [session, closeViews]);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
* defeats `observer`'s memo for all of them and each re-render rebuilds a
|
|
80
|
-
* `ViewStack`. `drag` is deliberately NOT in here — it goes down its own prop
|
|
81
|
-
* so it reaches only the cell it describes.
|
|
82
|
-
*
|
|
83
|
-
* Nothing does this for us: `observer(function(){})` is not compiled by the
|
|
84
|
-
* React Compiler.
|
|
85
|
-
*/
|
|
60
|
+
// memoised by hand (the React Compiler skips `observer`): a fresh object
|
|
61
|
+
// re-renders every panel's ViewStack. `drag` stays out for the same reason.
|
|
86
62
|
const chrome = useMemo(() => ({
|
|
87
63
|
dragHandlers: handlers,
|
|
88
64
|
onTabClose: closeTab,
|
|
@@ -94,13 +70,12 @@ export const WorkspaceContainer = observer(function WorkspaceContainer({ session
|
|
|
94
70
|
} })),
|
|
95
71
|
renderTabContent: tab => {
|
|
96
72
|
const views = viewsOf(session, tab.viewIds);
|
|
97
|
-
return views.length > 0 ? (_jsx(ViewStack, { views: views, session: session
|
|
73
|
+
return views.length > 0 ? (_jsx(ViewStack, { views: views, session: session })) : (_jsx("div", { className: classes.empty, children: _jsx(Suspense, { fallback: null, children: _jsx(ViewLauncher, { session: session }) }) }));
|
|
98
74
|
},
|
|
99
|
-
}), [session, handlers, closeTab, closePanel, classes.
|
|
75
|
+
}), [session, handlers, closeTab, closePanel, classes.empty]);
|
|
100
76
|
return (_jsx("div", { className: classes.container, "data-testid": "workspace", children: _jsx(LayoutRenderer, { node: session.visibleTree, layout: session, drag: drag, chrome: chrome }) }));
|
|
101
77
|
});
|
|
102
|
-
// `session.views` is the
|
|
103
|
-
// tab's membership list is filtered through it rather than read as an order.
|
|
78
|
+
// `session.views` is the order; a tab's `viewIds` is membership only
|
|
104
79
|
function viewsOf(session, viewIds) {
|
|
105
80
|
const members = new Set(viewIds);
|
|
106
81
|
return session.views.filter(v => members.has(v.id));
|
|
@@ -1,24 +1,9 @@
|
|
|
1
1
|
import type { WorkspaceSessionType } from '../ui/App/types.ts';
|
|
2
2
|
import type { WorkspaceLayout } from './model.ts';
|
|
3
3
|
import type { PanelNode } from './tree.ts';
|
|
4
|
-
/**
|
|
5
|
-
* The per-cell buttons: new tab, split, close.
|
|
6
|
-
*
|
|
7
|
-
* Every one of these is a single MST action on the layout, where the dockview
|
|
8
|
-
* versions were an api call plus a session write that had to be kept consistent
|
|
9
|
-
* with it — `handleSplit` was `containerApi.addGroup(...)` followed by
|
|
10
|
-
* `addEmptyTab(newGroup)`, and the gap between those two was where the
|
|
11
|
-
* "resource is already disposed" crash lived.
|
|
12
|
-
*/
|
|
13
4
|
export declare const WorkspacePanelActions: ({ panel, session, onClose, }: {
|
|
14
5
|
panel: PanelNode;
|
|
15
6
|
session: WorkspaceSessionType & WorkspaceLayout;
|
|
16
|
-
/**
|
|
17
|
-
* Closing a cell closes the views its tabs held, and the layout does not own
|
|
18
|
-
* views — so the pair is one function at the call site, the same way
|
|
19
|
-
* `WorkspaceTab` takes its `onClose`. Spelled here as well, this file would
|
|
20
|
-
* be the third place stating "and also remove the views", which is one more
|
|
21
|
-
* than the number that can be kept in step.
|
|
22
|
-
*/
|
|
7
|
+
/** closes the cell and the views its tabs hold */
|
|
23
8
|
onClose: () => void;
|
|
24
9
|
}) => import("react").JSX.Element;
|
|
@@ -14,28 +14,20 @@ import ViewColumnIcon from '@mui/icons-material/ViewColumn';
|
|
|
14
14
|
import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
|
15
15
|
import { IconButton, Tooltip } from '@mui/material';
|
|
16
16
|
import { observer } from 'mobx-react';
|
|
17
|
-
import {
|
|
17
|
+
import { stripIconColor, workspaceTheme } from './workspaceTheme.js';
|
|
18
18
|
const useStyles = makeStyles()({
|
|
19
19
|
actions: { display: 'flex', alignItems: 'center', flex: '0 0 auto' },
|
|
20
|
-
// dockview's `.dv-default-tab-action`: inherits the strip's colour, and takes
|
|
21
|
-
// a small rounded chip on hover rather than MUI's circular ripple surface
|
|
22
20
|
button: {
|
|
23
21
|
padding: 4,
|
|
24
22
|
borderRadius: 2,
|
|
25
|
-
color:
|
|
26
|
-
'&:hover': {
|
|
23
|
+
color: stripIconColor,
|
|
24
|
+
'&:hover': {
|
|
25
|
+
background: workspaceTheme.iconHoverBackground,
|
|
26
|
+
borderRadius: 2,
|
|
27
|
+
},
|
|
27
28
|
},
|
|
28
29
|
icon: { fontSize: 16 },
|
|
29
30
|
});
|
|
30
|
-
/**
|
|
31
|
-
* The per-cell buttons: new tab, split, close.
|
|
32
|
-
*
|
|
33
|
-
* Every one of these is a single MST action on the layout, where the dockview
|
|
34
|
-
* versions were an api call plus a session write that had to be kept consistent
|
|
35
|
-
* with it — `handleSplit` was `containerApi.addGroup(...)` followed by
|
|
36
|
-
* `addEmptyTab(newGroup)`, and the gap between those two was where the
|
|
37
|
-
* "resource is already disposed" crash lived.
|
|
38
|
-
*/
|
|
39
31
|
export const WorkspacePanelActions = observer(function WorkspacePanelActions({ panel, session, onClose, }) {
|
|
40
32
|
const { classes } = useStyles();
|
|
41
33
|
const canClose = session.panels.length > 1;
|
|
@@ -52,23 +44,21 @@ export const WorkspacePanelActions = observer(function WorkspacePanelActions({ p
|
|
|
52
44
|
},
|
|
53
45
|
},
|
|
54
46
|
{
|
|
55
|
-
label: 'New empty split
|
|
47
|
+
label: 'New empty split (right)',
|
|
56
48
|
icon: VerticalSplitIcon,
|
|
57
49
|
onClick: () => {
|
|
58
50
|
session.splitPanel(panel.id, 'row');
|
|
59
51
|
},
|
|
60
52
|
},
|
|
61
53
|
{
|
|
62
|
-
label: 'New empty split
|
|
54
|
+
label: 'New empty split (below)',
|
|
63
55
|
icon: HorizontalSplitIcon,
|
|
64
56
|
onClick: () => {
|
|
65
57
|
session.splitPanel(panel.id, 'column');
|
|
66
58
|
},
|
|
67
59
|
},
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
// being another cell to hide, since maximizing the only one is a
|
|
71
|
-
// no-op with a label promising otherwise.
|
|
60
|
+
// the keyboard route to the strip's double-click; hidden when there
|
|
61
|
+
// is no other cell to hide
|
|
72
62
|
...(session.panels.length > 1 || maximized
|
|
73
63
|
? [
|
|
74
64
|
{
|
|
@@ -80,41 +70,46 @@ export const WorkspacePanelActions = observer(function WorkspacePanelActions({ p
|
|
|
80
70
|
},
|
|
81
71
|
]
|
|
82
72
|
: []),
|
|
83
|
-
// The whole-workspace commands, kept behind the same "Global:" prefix
|
|
84
|
-
// and the same >1-view gate they had on the dockview header, so a
|
|
85
|
-
// returning user finds them where they were and the label still says
|
|
86
|
-
// that the button in THIS cell rearranges every cell.
|
|
87
73
|
...(session.views.length > 1
|
|
88
74
|
? [
|
|
89
75
|
{
|
|
90
|
-
label: '
|
|
91
|
-
icon: DynamicFeedIcon,
|
|
92
|
-
onClick: () => {
|
|
93
|
-
tile('tabs');
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
label: 'Global: tile horizontally',
|
|
98
|
-
icon: ViewColumnIcon,
|
|
99
|
-
onClick: () => {
|
|
100
|
-
tile('horizontal');
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
label: 'Global: tile vertically',
|
|
105
|
-
icon: TableRowsIcon,
|
|
106
|
-
onClick: () => {
|
|
107
|
-
tile('vertical');
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
label: 'Global: tile grid',
|
|
76
|
+
label: 'Arrange all views',
|
|
112
77
|
icon: ViewModuleIcon,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
78
|
+
type: 'subMenu',
|
|
79
|
+
subMenu: [
|
|
80
|
+
{
|
|
81
|
+
label: 'As tabs',
|
|
82
|
+
icon: DynamicFeedIcon,
|
|
83
|
+
onClick: () => {
|
|
84
|
+
tile('tabs');
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
label: 'Side by side',
|
|
89
|
+
icon: ViewColumnIcon,
|
|
90
|
+
onClick: () => {
|
|
91
|
+
tile('horizontal');
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: 'Stacked',
|
|
96
|
+
icon: TableRowsIcon,
|
|
97
|
+
onClick: () => {
|
|
98
|
+
tile('vertical');
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
label: 'Grid',
|
|
103
|
+
icon: ViewModuleIcon,
|
|
104
|
+
onClick: () => {
|
|
105
|
+
tile('grid');
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
],
|
|
116
109
|
},
|
|
117
110
|
]
|
|
118
111
|
: []),
|
|
119
|
-
], size: "small", className: classes.button, children: _jsx(AddIcon, { className: classes.icon }) }),
|
|
112
|
+
], tooltip: "Panel menu", size: "small", className: classes.button, children: _jsx(AddIcon, { className: classes.icon }) }), maximized ? (_jsx(Tooltip, { title: "Restore panel", children: _jsx(IconButton, { size: "small", className: classes.button, onClick: () => {
|
|
113
|
+
session.restorePanels();
|
|
114
|
+
}, children: _jsx(CloseFullscreenIcon, { className: classes.icon }) }) })) : null, canClose ? (_jsx(Tooltip, { title: "Close panel", children: _jsx(IconButton, { size: "small", className: classes.button, onClick: onClose, children: _jsx(CloseIcon, { className: classes.icon }) }) })) : null] }));
|
|
120
115
|
});
|
|
@@ -2,26 +2,11 @@ import type { WorkspaceSessionType } from '../ui/App/types.ts';
|
|
|
2
2
|
import type { WorkspaceLayout } from './model.ts';
|
|
3
3
|
import type { TabNode } from './tree.ts';
|
|
4
4
|
import type { AbstractViewModel } from '@jbrowse/core/util';
|
|
5
|
-
/**
|
|
6
|
-
* A tab's name is **derived** from the views it holds, unless the user set one.
|
|
7
|
-
*
|
|
8
|
-
* `title === undefined` is the sentinel for "not renamed", which is a plain
|
|
9
|
-
* `maybe` on the tab rather than the comparison the dockview version needed:
|
|
10
|
-
* dockview restores an unset title as the panel id, so telling a real title
|
|
11
|
-
* from a restored one meant testing `title !== panelId` and hoping nobody named
|
|
12
|
-
* a tab after a panel.
|
|
13
|
-
*/
|
|
14
|
-
export declare function tabDisplayName(tab: TabNode, views: AbstractViewModel[], session: WorkspaceSessionType): string;
|
|
15
5
|
export declare const WorkspaceTab: ({ tab, views, session, layout, onClose, }: {
|
|
16
6
|
tab: TabNode;
|
|
17
7
|
views: AbstractViewModel[];
|
|
18
8
|
session: WorkspaceSessionType;
|
|
19
9
|
layout: WorkspaceLayout;
|
|
20
|
-
/**
|
|
21
|
-
* Closing a tab closes the views it held, and the layout does not own views —
|
|
22
|
-
* so the pair is one function at the call site rather than spelled out here.
|
|
23
|
-
* The strip's middle-click needs the same pair, and two spellings of "and
|
|
24
|
-
* also remove the views" is how one of them comes to be missing it.
|
|
25
|
-
*/
|
|
10
|
+
/** closes the tab and the views it holds */
|
|
26
11
|
onClose: () => void;
|
|
27
12
|
}) => import("react").JSX.Element;
|
|
@@ -4,20 +4,15 @@ import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
|
4
4
|
import { InputBase, Typography } from '@mui/material';
|
|
5
5
|
import { observer } from 'mobx-react';
|
|
6
6
|
import JBrowseTabMenu from '../ui/App/JBrowseTabMenu.js';
|
|
7
|
-
import {
|
|
7
|
+
import { tabDisplayName } from './tabName.js';
|
|
8
8
|
const useStyles = makeStyles()(theme => ({
|
|
9
|
-
//
|
|
10
|
-
// hover live on the strip's own `[role=tab]` wrapper, which is where the
|
|
11
|
-
// selection is known — two places styling one tab is how they drift.
|
|
9
|
+
// the `[role=tab]` wrapper in TabStrip owns background and selected state
|
|
12
10
|
tab: {
|
|
13
11
|
display: 'flex',
|
|
14
12
|
alignItems: 'center',
|
|
15
13
|
gap: 4,
|
|
16
14
|
minWidth: 0,
|
|
17
15
|
},
|
|
18
|
-
// dockview's `.dv-default-tab-content`: takes the space, truncates with an
|
|
19
|
-
// ellipsis, and takes its colour from the tab, which is what encodes
|
|
20
|
-
// active/inactive
|
|
21
16
|
title: {
|
|
22
17
|
flexGrow: 1,
|
|
23
18
|
marginRight: 4,
|
|
@@ -36,38 +31,20 @@ const useStyles = makeStyles()(theme => ({
|
|
|
36
31
|
flex: 1,
|
|
37
32
|
},
|
|
38
33
|
}));
|
|
39
|
-
/**
|
|
40
|
-
* A tab's name is **derived** from the views it holds, unless the user set one.
|
|
41
|
-
*
|
|
42
|
-
* `title === undefined` is the sentinel for "not renamed", which is a plain
|
|
43
|
-
* `maybe` on the tab rather than the comparison the dockview version needed:
|
|
44
|
-
* dockview restores an unset title as the panel id, so telling a real title
|
|
45
|
-
* from a restored one meant testing `title !== panelId` and hoping nobody named
|
|
46
|
-
* a tab after a panel.
|
|
47
|
-
*/
|
|
48
|
-
export function tabDisplayName(tab, views, session) {
|
|
49
|
-
if (tab.title) {
|
|
50
|
-
return tab.title;
|
|
51
|
-
}
|
|
52
|
-
if (views.length === 0) {
|
|
53
|
-
return 'Empty';
|
|
54
|
-
}
|
|
55
|
-
if (views.length === 1) {
|
|
56
|
-
return viewName(views[0], r => session.assemblyManager.getDisplayName(r));
|
|
57
|
-
}
|
|
58
|
-
return `${views.length} views`;
|
|
59
|
-
}
|
|
60
34
|
export const WorkspaceTab = observer(function WorkspaceTab({ tab, views, session, layout, onClose, }) {
|
|
61
35
|
const { classes } = useStyles();
|
|
62
36
|
const [editing, setEditing] = useState(false);
|
|
63
37
|
const [draft, setDraft] = useState('');
|
|
64
38
|
const title = tabDisplayName(tab, views, session);
|
|
39
|
+
const startEditing = () => {
|
|
40
|
+
setDraft(title);
|
|
41
|
+
setEditing(true);
|
|
42
|
+
};
|
|
43
|
+
// an empty box or the unchanged automatic name keeps the name automatic
|
|
65
44
|
const save = () => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// rename could be made and never unmade.
|
|
70
|
-
layout.renameTab(tab.id, draft.trim() || undefined);
|
|
45
|
+
const name = draft.trim();
|
|
46
|
+
const automatic = tabDisplayName({ ...tab, title: undefined }, views, session);
|
|
47
|
+
layout.renameTab(tab.id, name && name !== automatic ? name : undefined);
|
|
71
48
|
setEditing(false);
|
|
72
49
|
};
|
|
73
50
|
return (_jsx("div", { className: classes.tab, children: editing ? (_jsx(InputBase, { autoFocus: true, className: classes.editInput, value: draft, onChange: e => {
|
|
@@ -75,13 +52,6 @@ export const WorkspaceTab = observer(function WorkspaceTab({ tab, views, session
|
|
|
75
52
|
}, onFocus: e => {
|
|
76
53
|
e.target.select();
|
|
77
54
|
}, onBlur: save, onKeyDown: e => {
|
|
78
|
-
// The box sits INSIDE the `role="tab"` that owns the strip's roving
|
|
79
|
-
// tabindex, and every key it handles — the arrows, Home, End, Enter
|
|
80
|
-
// and Space — it also preventDefault()s. All of them reach that
|
|
81
|
-
// handler by bubbling out of here, so while the box is open the
|
|
82
|
-
// arrows jumped to the next tab instead of moving the caret and a
|
|
83
|
-
// space never reached the input at all: a tab could not be given a
|
|
84
|
-
// name with a space in it.
|
|
85
55
|
e.stopPropagation();
|
|
86
56
|
if (e.key === 'Enter') {
|
|
87
57
|
save();
|
|
@@ -95,11 +65,5 @@ export const WorkspaceTab = observer(function WorkspaceTab({ tab, views, session
|
|
|
95
65
|
e.stopPropagation();
|
|
96
66
|
}, onClick: e => {
|
|
97
67
|
e.stopPropagation();
|
|
98
|
-
} })) : (_jsxs(_Fragment, { children: [_jsx(Typography, { className: classes.title, variant: "body2", onDoubleClick: ()
|
|
99
|
-
setDraft(title);
|
|
100
|
-
setEditing(true);
|
|
101
|
-
}, children: title }), _jsx(JBrowseTabMenu, { onRename: () => {
|
|
102
|
-
setDraft(title);
|
|
103
|
-
setEditing(true);
|
|
104
|
-
}, onClose: onClose })] })) }));
|
|
68
|
+
} })) : (_jsxs(_Fragment, { children: [_jsx(Typography, { className: classes.title, variant: "body2", onDoubleClick: startEditing, children: title }), _jsx(JBrowseTabMenu, { onRename: startEditing, onClose: onClose })] })) }));
|
|
105
69
|
});
|