@jbrowse/plugin-linear-genome-view 5.0.0-beta.17 → 5.0.0-beta.19

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 (46) hide show
  1. package/esm/LinearGenomeView/afterAttach.js +6 -4
  2. package/esm/LinearGenomeView/components/ImportForm.js +2 -4
  3. package/esm/LinearGenomeView/components/LinearGenomeViewContainer.js +4 -2
  4. package/esm/LinearGenomeView/components/SearchBox.js +1 -9
  5. package/esm/LinearGenomeView/components/SearchResultsDialog.js +1 -1
  6. package/esm/LinearGenomeView/components/SearchResultsTable.d.ts +2 -1
  7. package/esm/LinearGenomeView/components/SearchResultsTable.js +23 -5
  8. package/esm/LinearGenomeView/components/SequenceFeatureHoverHighlight.js +2 -3
  9. package/esm/LinearGenomeView/components/TrackContainer.d.ts +2 -3
  10. package/esm/LinearGenomeView/components/TrackContainer.js +3 -4
  11. package/esm/LinearGenomeView/components/TrackLabelRefNameWarning.d.ts +3 -4
  12. package/esm/LinearGenomeView/components/TrackLabelRefNameWarning.js +23 -10
  13. package/esm/LinearGenomeView/components/TrackRenderingContainer.d.ts +2 -3
  14. package/esm/LinearGenomeView/components/TrackRenderingContainer.js +1 -1
  15. package/esm/LinearGenomeView/components/useSearchBoxPrefs.d.ts +1 -1
  16. package/esm/LinearGenomeView/components/useSearchBoxPrefs.js +1 -1
  17. package/esm/LinearGenomeView/index.d.ts +1 -1
  18. package/esm/LinearGenomeView/index.js +2 -0
  19. package/esm/LinearGenomeView/keyboardHandler.d.ts +8 -9
  20. package/esm/LinearGenomeView/keyboardHandler.js +58 -34
  21. package/esm/LinearGenomeView/menuItems.js +3 -2
  22. package/esm/LinearGenomeView/model.d.ts +489 -52
  23. package/esm/LinearGenomeView/model.js +125 -45
  24. package/esm/LinearGenomeView/rExport/exportLinearView.js +4 -5
  25. package/esm/LinearGenomeView/rExport/rScript.d.ts +26 -1
  26. package/esm/LinearGenomeView/rExport/rScript.js +27 -6
  27. package/esm/LinearGenomeView/sharedScaleContainer.js +2 -12
  28. package/esm/LinearGenomeView/showFiles.d.ts +2 -0
  29. package/esm/LinearGenomeView/showFiles.js +19 -0
  30. package/esm/LinearGenomeView/showRegionsWithUndo.d.ts +1 -1
  31. package/esm/LinearGenomeView/showRegionsWithUndo.js +1 -1
  32. package/esm/LinearGenomeView/svgcomponents/SVGLinearGenomeView.js +3 -1
  33. package/esm/LinearGenomeView/util.d.ts +1 -1
  34. package/esm/LinearGenomeView/util.js +1 -1
  35. package/esm/TrackModifiers/index.d.ts +80 -0
  36. package/esm/TrackModifiers/index.js +212 -0
  37. package/esm/displayKitTests/perRegionTestEnv.d.ts +10 -4
  38. package/esm/index.d.ts +3 -1
  39. package/esm/index.js +1 -2
  40. package/esm/searchUtils.d.ts +14 -2
  41. package/esm/searchUtils.js +56 -36
  42. package/package.json +10 -9
  43. package/esm/FeatureTrack/configSchema.d.ts +0 -217
  44. package/esm/FeatureTrack/configSchema.js +0 -55
  45. package/esm/FeatureTrack/index.d.ts +0 -2
  46. package/esm/FeatureTrack/index.js +0 -13
@@ -2,8 +2,10 @@ import { getSession, isSessionModelWithWidgets, resolveNamedRegions, } from '@jb
2
2
  import { coerceHighlight } from '@jbrowse/core/util/highlights';
3
3
  import { installInitAutorun } from '@jbrowse/core/util/installInitAutorun';
4
4
  import { normalizeTrackInit, warmTrackDisplayGeneric, } from '@jbrowse/core/util/tracks';
5
+ import { requestedDisplayType } from '@jbrowse/launch-args/displaySettings';
5
6
  import { addDisposer } from '@jbrowse/mobx-state-tree';
6
7
  import { autorun, untracked, when } from 'mobx';
8
+ import { launchTrackInit } from '../TrackModifiers/index.js';
7
9
  import { SearchResultsNotFoundError } from '../searchUtils.js';
8
10
  // `init` is a frozen blob that often comes from hand-authored JSON, where a
9
11
  // lone entry gets written bare (`tracks: 'genes'`). A string is iterable, so
@@ -127,8 +129,7 @@ async function navigateInit(self, session, init) {
127
129
  // display's state model first when it is registered lazily
128
130
  async function showInitTracks(self, init) {
129
131
  for (const t of asArray(init.tracks)) {
130
- const { trackId, trackSnapshot, displaySnapshot } = normalizeTrackInit(t);
131
- await self.launchTrack(trackId, trackSnapshot, displaySnapshot);
132
+ await launchTrackInit(self, t);
132
133
  }
133
134
  }
134
135
  /**
@@ -219,8 +220,9 @@ function setupInitWarmup(self) {
219
220
  }
220
221
  }
221
222
  for (const t of tracks) {
222
- const { trackId, displaySnapshot } = normalizeTrackInit(t);
223
- warmTrackDisplayGeneric(self, trackId, displaySnapshot, assembly);
223
+ const { trackId, displaySnapshot, modifiers } = normalizeTrackInit(t);
224
+ const type = requestedDisplayType(modifiers);
225
+ warmTrackDisplayGeneric(self, trackId, type ? { ...displaySnapshot, type } : displaySnapshot, assembly);
224
226
  }
225
227
  });
226
228
  }, { name: 'LGVInitWarmup' }));
@@ -20,9 +20,8 @@ const useStyles = makeStyles()(theme => ({
20
20
  const LinearGenomeViewImportForm = observer(function LinearGenomeViewImportForm({ model, }) {
21
21
  const { classes } = useStyles();
22
22
  const session = getSession(model);
23
- const { textSearchManager } = session;
24
23
  const { error: viewError } = model;
25
- const { selectedAssemblyName: selectedAsm, setSelectedAssemblyName, assembly, assemblyError, regions, } = useAssemblySelection(session, 'lgv');
24
+ const { selectedAssemblyName: selectedAsm, setSelectedAssemblyName, assemblyError, regions, } = useAssemblySelection(session, 'lgv');
26
25
  const { recentLocations, addRecentLocation, clearRecentLocations } = useRecentLocations(selectedAsm);
27
26
  // The location the form will open, tagged with the assembly it was entered
28
27
  // for, and read back only while that tag still matches. Switching assembly
@@ -85,9 +84,8 @@ const LinearGenomeViewImportForm = observer(function LinearGenomeViewImportForm(
85
84
  model.setError(undefined);
86
85
  setSelectedAssemblyName(val);
87
86
  }, session: session, selected: selectedAsm }), selectedAsm ? (assemblyError ? (_jsx(CloseIcon, { color: "error" })) : regions ? (_jsx(RefNameAutocomplete, { fetchResults: (queryString, signal) => fetchResults({
87
+ session,
88
88
  queryString,
89
- assembly,
90
- textSearchManager,
91
89
  assemblyName: selectedAsm,
92
90
  signal,
93
91
  }), session: session, assemblyName: selectedAsm, value: value, minWidth: 270, adornmentWidth: adornmentReservePx({
@@ -99,10 +99,12 @@ const LinearGenomeViewContainer = observer(function LinearGenomeViewContainer({
99
99
  // alone. At most one display can be under the pointer, so the first
100
100
  // non-empty answer is the answer.
101
101
  const hoverFeature = tracks
102
- .flatMap(t => t.displays)
102
+ .flatMap((t) => t.displays)
103
103
  .map(d => d.hoveredFeature)
104
104
  .find(Boolean);
105
- session.setHovered({ hoverPosition, hoverFeature });
105
+ // `viewId` lets a view paired with one linear view ignore another
106
+ // showing the same locus
107
+ session.setHovered({ hoverPosition, hoverFeature, viewId: model.id });
106
108
  }, children: [_jsx(NavigationAnnouncer, { model: model }), _jsxs("div", { ref: headerRef, "data-testid": `lgv-header-${model.id}`, className: classes.header, style: { position: stickyViewHeaders ? 'sticky' : undefined }, children: [_jsx(Header, { model: model }), hideHeader ? _jsx(MiniControls, { model: model }) : null] }), _jsx("div", { ref: tracksRef, children: model.scalebarOnly ? (_jsx(Rubberband, { model: model })) : (_jsx(TracksContainer, { model: model, children: !tracks.length ? (_jsx(NoTracksActiveButton, { model: model })) : (_jsxs(_Fragment, { children: [pinnedTracks.length ? (_jsx(Paper, { elevation: 2, "data-testid": "pinned-tracks", className: classes.pinnedTracks, style: {
107
109
  top: stickyTops.pinnedTracks,
108
110
  // the scroll port this sticks in, not the window: a
@@ -34,10 +34,8 @@ export function searchBoxWidth(value) {
34
34
  const SearchBox = observer(function SearchBox({ model, showHelp = true, minWidth = MIN_WIDTH, maxWidth = MAX_WIDTH, style = defaultStyle, }) {
35
35
  const palette = usePalette();
36
36
  const session = getSession(model);
37
- const { textSearchManager, assemblyManager } = session;
38
37
  const { assemblyNames } = model;
39
38
  const assemblyName = assemblyNames[0];
40
- const assembly = assemblyManager.get(assemblyName);
41
39
  const { recentLocations, addRecentLocation, clearRecentLocations } = useRecentLocations(assemblyName);
42
40
  async function navigate(option) {
43
41
  try {
@@ -57,13 +55,7 @@ const SearchBox = observer(function SearchBox({ model, showHelp = true, minWidth
57
55
  });
58
56
  return (_jsx(RefNameAutocomplete, { onSelect: option => {
59
57
  void navigate(option);
60
- }, assemblyName: assemblyName, fetchResults: (queryString, signal) => fetchResults({
61
- queryString,
62
- assemblyName,
63
- textSearchManager,
64
- assembly,
65
- signal,
66
- }), session: session, value: model.coarseVisibleLocStrings, minWidth: minWidth, maxWidth: maxWidth, adornmentWidth: adornmentReservePx({
58
+ }, assemblyName: assemblyName, fetchResults: (queryString, signal) => fetchResults({ session, queryString, assemblyName, signal }), session: session, value: model.coarseVisibleLocStrings, minWidth: minWidth, maxWidth: maxWidth, adornmentWidth: adornmentReservePx({
67
59
  showHelp,
68
60
  menuItemCount: recentMenuItems.length,
69
61
  }), style: style, endAdornment: _jsx(RefNameAutocompleteEndAdornment, { showHelp: showHelp, menuItems: recentMenuItems }), inputStyle: {
@@ -5,6 +5,6 @@ import { observer } from 'mobx-react';
5
5
  import SearchResultsTable from './SearchResultsTable.js';
6
6
  const SearchResultsDialog = observer(function SearchResultsDialog({ model, }) {
7
7
  const { searchPicker } = model;
8
- return searchPicker ? (_jsxs(InfoDialog, { open: true, maxWidth: "xl", onClose: model.closeSearchPicker, title: "Search results", children: [_jsxs(Typography, { children: ["Showing results for ", _jsx("b", { children: searchPicker.query })] }), _jsx(SearchResultsTable, { picker: searchPicker })] })) : null;
8
+ return searchPicker ? (_jsxs(InfoDialog, { open: true, maxWidth: "xl", onClose: model.closeSearchPicker, title: "Search results", children: [_jsxs(Typography, { children: [searchPicker.rows.length, " matches for ", _jsx("b", { children: searchPicker.query })] }), _jsx(SearchResultsTable, { picker: searchPicker })] })) : null;
9
9
  });
10
10
  export default SearchResultsDialog;
@@ -1,4 +1,5 @@
1
- import type { SearchPickerState } from '../model.ts';
1
+ import type { SearchPickerRow, SearchPickerState } from '../model.ts';
2
+ export declare function filterRows(rows: SearchPickerRow[], text: string): SearchPickerRow[];
2
3
  declare const SearchResultsTable: ({ picker, }: {
3
4
  picker: SearchPickerState;
4
5
  }) => import("react").JSX.Element;
@@ -1,10 +1,28 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
2
3
  import { SanitizedHTML } from '@jbrowse/core/ui';
3
- import { Button, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, } from '@mui/material';
4
+ import { Button, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, } from '@mui/material';
4
5
  import { observer } from 'mobx-react';
6
+ // past this many rows a list is easier to narrow than to read; gene names
7
+ // sort too irregularly (scn1ab, HOXA10-AS) for an order to stand in for it
8
+ const FILTER_AT = 8;
9
+ export function filterRows(rows, text) {
10
+ const needle = text.trim().toLowerCase();
11
+ return needle
12
+ ? rows.filter(row => [row.label, row.location, row.trackName].some(field => field.toLowerCase().includes(needle)))
13
+ : rows;
14
+ }
5
15
  const SearchResultsTable = observer(function SearchResultsTable({ picker, }) {
6
- return (_jsx(TableContainer, { component: Paper, children: _jsxs(Table, { children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: "Name" }), _jsx(TableCell, { align: "right", children: "Location" }), _jsx(TableCell, { align: "right", children: "Track" }), _jsx(TableCell, { align: "right" })] }) }), _jsx(TableBody, { children: picker.rows.map(row => (_jsxs(TableRow, { children: [_jsx(TableCell, { component: "th", scope: "row", children: row.label }), _jsx(TableCell, { align: "right", children: row.location }), _jsx(TableCell, { align: "right", children: _jsx(SanitizedHTML, { html: row.trackName || 'N/A' }) }), _jsx(TableCell, { align: "right", children: _jsx(Button, { onClick: () => {
7
- void picker.pick(row.id);
8
- }, color: "primary", variant: "contained", children: "Go" }) })] }, row.id))) })] }) }));
16
+ const [filter, setFilter] = useState('');
17
+ const rows = filterRows(picker.rows, filter);
18
+ return (_jsxs(_Fragment, { children: [picker.rows.length > FILTER_AT ? (_jsx(TextField, { autoFocus: true, fullWidth: true, size: "small", margin: "dense", placeholder: "Filter by name, location or track", value: filter, onChange: event => {
19
+ setFilter(event.target.value);
20
+ }, onKeyDown: event => {
21
+ if (event.key === 'Enter' && rows.length === 1) {
22
+ void picker.pick(rows[0].id);
23
+ }
24
+ }, helperText: filter ? `${rows.length} of ${picker.rows.length}` : undefined })) : null, _jsx(TableContainer, { component: Paper, children: _jsxs(Table, { children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: "Name" }), _jsx(TableCell, { align: "right", children: "Location" }), _jsx(TableCell, { align: "right", children: "Track" }), _jsx(TableCell, { align: "right" })] }) }), _jsx(TableBody, { children: rows.map(row => (_jsxs(TableRow, { children: [_jsx(TableCell, { component: "th", scope: "row", children: row.label }), _jsx(TableCell, { align: "right", children: row.location }), _jsx(TableCell, { align: "right", children: _jsx(SanitizedHTML, { html: row.trackName || 'N/A' }) }), _jsx(TableCell, { align: "right", children: _jsx(Button, { onClick: () => {
25
+ void picker.pick(row.id);
26
+ }, color: "primary", variant: "contained", children: "Go" }) })] }, row.id))) })] }) })] }));
9
27
  });
10
28
  export default SearchResultsTable;
@@ -15,8 +15,7 @@ function isFeatureWidgetForView(w, viewId) {
15
15
  // is owned by the display rather than by a widget — so the view's own displays
16
16
  // publish hover positions too.
17
17
  function displayHoverPosition(display) {
18
- const candidate = display;
19
- return candidate?.sequenceHoverPosition;
18
+ return display.sequenceHoverPosition;
20
19
  }
21
20
  // Draws a crosshair on the LGV at the base a connected feature-detail sequence
22
21
  // panel is hovering, so mousing over the sequence readout points at the genome.
@@ -24,7 +23,7 @@ const SequenceFeatureHoverHighlight = observer(function SequenceFeatureHoverHigh
24
23
  return (_jsxs(_Fragment, { children: [_jsx(ConnectedHoverHighlight, { model: model, getPosition: (widget, viewId) => isFeatureWidgetForView(widget, viewId)
25
24
  ? widget.sequenceHoverPosition
26
25
  : undefined }), model.tracks
27
- .flatMap(track => track.displays)
26
+ .flatMap((track) => track.displays)
28
27
  .flatMap(display => {
29
28
  const position = displayHoverPosition(display);
30
29
  return position
@@ -1,8 +1,7 @@
1
- import type { LinearGenomeViewModel } from '../index.ts';
2
- import type { BaseTrackModel } from '@jbrowse/core/pluggableElementTypes/models';
1
+ import type { LinearGenomeViewModel, LinearGenomeViewTrack } from '../index.ts';
3
2
  type LGV = LinearGenomeViewModel;
4
3
  declare const TrackContainer: ({ model, track, }: {
5
4
  model: LGV;
6
- track: BaseTrackModel;
5
+ track: LinearGenomeViewTrack;
7
6
  }) => import("react").JSX.Element;
8
7
  export default TrackContainer;
@@ -111,9 +111,8 @@ function useTrackLabelBand(model, trackId) {
111
111
  }
112
112
  const TrackContainer = observer(function TrackContainer({ model, track, }) {
113
113
  const { classes } = useStyles();
114
- // an LGV track always holds at least one linear display (activeDisplay =
115
- // displays[0]). The one narrowing to the linear shape, handed down rather
116
- // than repeated, so a track's chrome and its body can't read two displays.
114
+ // read once and handed down, so a track's chrome and its body can't read two
115
+ // displays
117
116
  const display = track.activeDisplay;
118
117
  const { showTrackOutlines } = model;
119
118
  // The Paper's 1px border, present iff outlines are on. Everything laid out in
@@ -135,7 +134,7 @@ const TrackContainer = observer(function TrackContainer({ model, track, }) {
135
134
  const { setPaper, setContainer } = useTrackLabelBand(model, track.trackId);
136
135
  return (_jsxs(Paper, { ref: setPaper, className: cx(classes.root, track.pinned ? null : classes.unpinnedTrack), variant: showTrackOutlines ? 'outlined' : undefined, elevation: showTrackOutlines ? undefined : 0, onDragOver: event => {
137
136
  model.onTrackDragOver(track.id, event.clientY);
138
- }, children: [track.pinned ? _jsx(Gridlines, { model: model, offset: outlineOffset }) : null, model.effectiveTrackLabels !== 'hidden' ? (_jsx(TrackLabel, { track: track, className: trackLabelStyle })) : null, _jsx(TrackOverlaySlot, { zIndex: 100, overlayStyle: { left: -outlineOffset }, children: _jsx(ErrorBoundary, { FallbackComponent: e => (_jsx("div", { className: classes.crashBanner, "data-gesture-owner": "true", children: _jsx(ErrorBanner, { error: e.error, onReset: e.resetErrorBoundary }) })), children: _jsx(TrackRenderingContainer, { model: model, track: track, display: display, ref: setContainer }) }, display.id) }), _jsx(PaddingBlocks, { model: model, offset: outlineOffset }), _jsx(ResizeHandle, { style: { height: RESIZE_HANDLE_HEIGHT }, onDrag: distance => display.resizeHeight(distance),
137
+ }, children: [track.pinned ? _jsx(Gridlines, { model: model, offset: outlineOffset }) : null, model.effectiveTrackLabels !== 'hidden' ? (_jsx(TrackLabel, { track: track, className: trackLabelStyle })) : null, _jsx(TrackOverlaySlot, { zIndex: 100, overlayStyle: { left: -outlineOffset }, labelBand: model.trackLabelBand(track), children: _jsx(ErrorBoundary, { FallbackComponent: e => (_jsx("div", { className: classes.crashBanner, "data-gesture-owner": "true", children: _jsx(ErrorBanner, { error: e.error, onReset: e.resetErrorBoundary }) })), children: _jsx(TrackRenderingContainer, { model: model, track: track, display: display, ref: setContainer }) }, display.id) }), _jsx(PaddingBlocks, { model: model, offset: outlineOffset }), _jsx(ResizeHandle, { style: { height: RESIZE_HANDLE_HEIGHT }, onDrag: distance => display.resizeHeight(distance),
139
138
  // the drag's shortcut: grow the track by whatever it is scrolled over,
140
139
  // so a pileup showing three rows of forty opens to all forty
141
140
  // Optional: only a display that scrolls its own content has anything
@@ -1,9 +1,8 @@
1
1
  import type { BaseTrackModel } from '@jbrowse/core/pluggableElementTypes/models';
2
2
  /**
3
- * The track label's warning for a file whose reference names have nothing in
4
- * common with the assembly's — the commonest data-configuration mistake in
5
- * JBrowse, and until now a silent one: the track draws nothing and reads
6
- * exactly like a track with no features in view.
3
+ * The track label's warning for a file that does not fit its assembly: its
4
+ * reference names have nothing in common with the assembly's, so the track
5
+ * draws nothing, or its contig lengths say it was made against another build.
7
6
  *
8
7
  * The label rather than the display chrome, deliberately. The chrome's states
9
8
  * are `displayPhase`'s, which are mutually exclusive and two of which replace
@@ -1,6 +1,6 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
- import { refNameMismatchMessage } from '@jbrowse/core/assemblyManager/assembly';
3
+ import { contigLengthMismatchMessage, refNameMismatchMessage, } from '@jbrowse/core/assemblyManager/assembly';
4
4
  import { InfoDialog } from '@jbrowse/core/ui';
5
5
  import { makeStyles } from '@jbrowse/core/util/tss-react';
6
6
  import WarningIcon from '@mui/icons-material/Warning';
@@ -12,11 +12,24 @@ const useStyles = makeStyles()(theme => ({
12
12
  color: theme.palette.warning.main,
13
13
  },
14
14
  }));
15
+ function warningFor(track) {
16
+ const { refNameMismatch, contigLengthMismatch } = track;
17
+ return refNameMismatch
18
+ ? {
19
+ title: 'Reference names do not match the assembly',
20
+ message: refNameMismatchMessage(refNameMismatch),
21
+ }
22
+ : contigLengthMismatch
23
+ ? {
24
+ title: 'Reference sequence lengths do not match the assembly',
25
+ message: contigLengthMismatchMessage(contigLengthMismatch),
26
+ }
27
+ : undefined;
28
+ }
15
29
  /**
16
- * The track label's warning for a file whose reference names have nothing in
17
- * common with the assembly's — the commonest data-configuration mistake in
18
- * JBrowse, and until now a silent one: the track draws nothing and reads
19
- * exactly like a track with no features in view.
30
+ * The track label's warning for a file that does not fit its assembly: its
31
+ * reference names have nothing in common with the assembly's, so the track
32
+ * draws nothing, or its contig lengths say it was made against another build.
20
33
  *
21
34
  * The label rather than the display chrome, deliberately. The chrome's states
22
35
  * are `displayPhase`'s, which are mutually exclusive and two of which replace
@@ -32,15 +45,15 @@ const useStyles = makeStyles()(theme => ({
32
45
  const TrackLabelRefNameWarning = observer(function TrackLabelRefNameWarning({ track, }) {
33
46
  const { classes } = useStyles();
34
47
  const [open, setOpen] = useState(false);
35
- const { refNameMismatch } = track;
36
- if (!refNameMismatch) {
48
+ const warning = warningFor(track);
49
+ if (!warning) {
37
50
  return null;
38
51
  }
39
- const message = refNameMismatchMessage(refNameMismatch);
52
+ const { title, message } = warning;
40
53
  return (_jsxs(_Fragment, { children: [_jsx(IconButton, { onClick: () => {
41
54
  setOpen(true);
42
- }, className: classes.iconButton, title: message, "data-testid": `track-refname-warning-${track.trackId}`, children: _jsx(WarningIcon, { fontSize: "small" }) }), open ? (_jsxs(InfoDialog, { open: true, title: "Reference names do not match the assembly", onClose: () => {
55
+ }, className: classes.iconButton, title: message, "data-testid": `track-refname-warning-${track.trackId}`, children: _jsx(WarningIcon, { fontSize: "small" }) }), open ? (_jsxs(InfoDialog, { open: true, title: title, onClose: () => {
43
56
  setOpen(false);
44
- }, children: [_jsx(Typography, { children: message }), _jsx(Typography, { variant: "body2", style: { marginTop: 8 }, children: "The track menu's \"About track\" shows every reference name the file uses." })] })) : null] }));
57
+ }, children: [_jsx(Typography, { children: message }), _jsx(Typography, { variant: "body2", style: { marginTop: 8 }, children: "The track menu's \"About track\" shows every reference name the file uses, with the lengths it states." })] })) : null] }));
45
58
  });
46
59
  export default TrackLabelRefNameWarning;
@@ -1,10 +1,9 @@
1
- import type { LinearGenomeViewModel } from '../index.ts';
2
- import type { BaseTrackModel } from '@jbrowse/core/pluggableElementTypes/models';
1
+ import type { LinearGenomeViewModel, LinearGenomeViewTrack } from '../index.ts';
3
2
  import type { LinearDisplayModel } from '@jbrowse/display-kit/types';
4
3
  type LGV = LinearGenomeViewModel;
5
4
  declare const TrackRenderingContainer: ({ model, track, display, ref, }: {
6
5
  model: LGV;
7
- track: BaseTrackModel;
6
+ track: LinearGenomeViewTrack;
8
7
  display: LinearDisplayModel;
9
8
  ref?: React.Ref<HTMLDivElement>;
10
9
  }) => import("react").JSX.Element;
@@ -18,7 +18,7 @@ const useStyles = makeStyles()({
18
18
  width: '100%',
19
19
  },
20
20
  // Tracks never scroll natively on this outer container — every display owns
21
- // its own vertical scroll: canvas displays scroll an inner sticky-canvas
21
+ // its own vertical scroll: feature displays scroll an inner sticky-canvas
22
22
  // container (FeatureComponent), while alignments and variants draw a custom
23
23
  // VerticalScrollbar overlay and redraw the canvas at `scrollTop`. A native
24
24
  // scroll port here only produced a *second*, spurious scrollbar whenever a
@@ -22,7 +22,7 @@ export type SearchBoxPrefs = ReturnType<typeof useSearchBoxPrefs>;
22
22
  /**
23
23
  * The menu rows that drive those prefs. "Stacked" rather than "Vertical": the
24
24
  * two headers name the same state and had drifted apart on it once already.
25
- * A caller spreading the rows into a "Show..." submenu passes `'Search boxes'`
25
+ * A caller spreading the rows into a "Show" submenu passes `'Search boxes'`
26
26
  * as the toggle's label.
27
27
  */
28
28
  export declare function searchBoxMenuItems({ showSearchBoxes, setShowSearchBoxes, sideBySide, setSideBySide, }: SearchBoxPrefs, toggleLabel?: string): MenuItem[];
@@ -22,7 +22,7 @@ export function useSearchBoxPrefs(prefix, numViews) {
22
22
  /**
23
23
  * The menu rows that drive those prefs. "Stacked" rather than "Vertical": the
24
24
  * two headers name the same state and had drifted apart on it once already.
25
- * A caller spreading the rows into a "Show..." submenu passes `'Search boxes'`
25
+ * A caller spreading the rows into a "Show" submenu passes `'Search boxes'`
26
26
  * as the toggle's label.
27
27
  */
28
28
  export function searchBoxMenuItems({ showSearchBoxes, setShowSearchBoxes, sideBySide, setSideBySide, }, toggleLabel = 'Show search boxes') {
@@ -25,6 +25,6 @@ declare module '@jbrowse/core/PluginManager' {
25
25
  }
26
26
  export default function LinearGenomeViewF(pluginManager: PluginManager): void;
27
27
  export { stateModelFactory } from './model.ts';
28
- export type { LinearGenomeViewModel, LinearGenomeViewStateModel, } from './model.ts';
28
+ export type { LinearGenomeViewModel, LinearGenomeViewStateModel, LinearGenomeViewTrack, } from './model.ts';
29
29
  export { installLinkedViewSync } from './linkedViewSync.ts';
30
30
  export { default as HighlightChip } from './components/HighlightChip.tsx';
@@ -1,5 +1,6 @@
1
1
  import { ViewType } from '@jbrowse/core/pluggableElementTypes';
2
2
  import { lazyWithPreload } from '@jbrowse/core/util/lazyWithPreload';
3
+ import { lgvKeyboardShortcuts } from './keyboardHandler.js';
3
4
  import { lgvLaunchKeys } from './launchKeys.js';
4
5
  import { stateModelFactory } from './model.js';
5
6
  export default function LinearGenomeViewF(pluginManager) {
@@ -13,6 +14,7 @@ export default function LinearGenomeViewF(pluginManager) {
13
14
  displayName: 'Linear genome view',
14
15
  stateModel,
15
16
  launchKeys: lgvLaunchKeys,
17
+ keyboardShortcuts: lgvKeyboardShortcuts,
16
18
  ReactComponent: lazyWithPreload(() => import('./components/LinearGenomeView.js')),
17
19
  });
18
20
  });
@@ -1,11 +1,10 @@
1
1
  import type { LinearGenomeViewModel } from './model.ts';
2
- /**
3
- * Sets up keyboard shortcuts for the LinearGenomeView
4
- * - Ctrl/Cmd + ArrowLeft: slide left
5
- * - Ctrl/Cmd + ArrowRight: slide right
6
- * - Ctrl/Cmd + ArrowUp: zoom in
7
- * - Ctrl/Cmd + ArrowDown: zoom out
8
- * - Ctrl/Cmd + Shift + D: highlight the region in view
9
- * - Ctrl/Cmd + Shift + M: go to the newest highlight
10
- */
2
+ import type { KeyboardShortcut } from '@jbrowse/core/pluggableElementTypes';
3
+ interface LgvShortcut extends KeyboardShortcut {
4
+ code: string;
5
+ shift?: boolean;
6
+ run: (self: LinearGenomeViewModel) => void;
7
+ }
8
+ export declare const lgvKeyboardShortcuts: readonly LgvShortcut[];
11
9
  export declare function setupKeyboardHandler(self: LinearGenomeViewModel): void;
10
+ export {};
@@ -1,48 +1,72 @@
1
1
  import { getSession } from '@jbrowse/core/util';
2
2
  import { isTextEntryFocused } from '@jbrowse/core/util/isTextEntryFocused';
3
3
  import { addDisposer } from '@jbrowse/mobx-state-tree';
4
- /**
5
- * Sets up keyboard shortcuts for the LinearGenomeView
6
- * - Ctrl/Cmd + ArrowLeft: slide left
7
- * - Ctrl/Cmd + ArrowRight: slide right
8
- * - Ctrl/Cmd + ArrowUp: zoom in
9
- * - Ctrl/Cmd + ArrowDown: zoom out
10
- * - Ctrl/Cmd + Shift + D: highlight the region in view
11
- * - Ctrl/Cmd + Shift + M: go to the newest highlight
12
- */
4
+ // every entry also needs Ctrl or Cmd; an arrow fires with or without Shift
5
+ export const lgvKeyboardShortcuts = [
6
+ {
7
+ keys: 'Ctrl/Cmd + ←',
8
+ description: 'Pan left',
9
+ code: 'ArrowLeft',
10
+ run: self => {
11
+ self.slide(-0.9);
12
+ },
13
+ },
14
+ {
15
+ keys: 'Ctrl/Cmd + →',
16
+ description: 'Pan right',
17
+ code: 'ArrowRight',
18
+ run: self => {
19
+ self.slide(0.9);
20
+ },
21
+ },
22
+ {
23
+ keys: 'Ctrl/Cmd + ↑',
24
+ description: 'Zoom in',
25
+ code: 'ArrowUp',
26
+ run: self => {
27
+ self.zoom(self.bpPerPx / 2);
28
+ },
29
+ },
30
+ {
31
+ keys: 'Ctrl/Cmd + ↓',
32
+ description: 'Zoom out',
33
+ code: 'ArrowDown',
34
+ run: self => {
35
+ self.zoom(self.bpPerPx * 2);
36
+ },
37
+ },
38
+ {
39
+ keys: 'Ctrl/Cmd + Shift + D',
40
+ description: 'Highlight the region in view',
41
+ code: 'KeyD',
42
+ shift: true,
43
+ run: self => {
44
+ self.highlightCurrentRegion();
45
+ getSession(self).notify('Region highlighted.', 'success');
46
+ },
47
+ },
48
+ {
49
+ keys: 'Ctrl/Cmd + Shift + M',
50
+ description: 'Go to the newest highlight',
51
+ code: 'KeyM',
52
+ shift: true,
53
+ run: self => {
54
+ void self.navigateNewestHighlight();
55
+ },
56
+ },
57
+ ];
13
58
  export function setupKeyboardHandler(self) {
14
59
  if (typeof document === 'undefined') {
15
60
  return;
16
61
  }
17
62
  function handler(e) {
18
- const session = getSession(self);
19
- if (session.focusedViewId === self.id &&
63
+ if (getSession(self).focusedViewId === self.id &&
20
64
  (e.ctrlKey || e.metaKey) &&
21
65
  !isTextEntryFocused()) {
22
- if (e.code === 'ArrowLeft') {
66
+ const shortcut = lgvKeyboardShortcuts.find(s => s.code === e.code && (!s.shift || e.shiftKey));
67
+ if (shortcut) {
23
68
  e.preventDefault();
24
- self.slide(-0.9);
25
- }
26
- else if (e.code === 'ArrowRight') {
27
- e.preventDefault();
28
- self.slide(0.9);
29
- }
30
- else if (e.code === 'ArrowUp') {
31
- e.preventDefault();
32
- self.zoom(self.bpPerPx / 2);
33
- }
34
- else if (e.code === 'ArrowDown') {
35
- e.preventDefault();
36
- self.zoom(self.bpPerPx * 2);
37
- }
38
- else if (e.shiftKey && e.code === 'KeyD') {
39
- e.preventDefault();
40
- self.highlightCurrentRegion();
41
- session.notify('Region highlighted.', 'success');
42
- }
43
- else if (e.shiftKey && e.code === 'KeyM') {
44
- e.preventDefault();
45
- void self.navigateNewestHighlight();
69
+ shortcut.run(self);
46
70
  }
47
71
  }
48
72
  }
@@ -1,7 +1,7 @@
1
1
  import { Highlighter, TrackSelector as TrackSelectorIcon, } from '@jbrowse/core/ui/Icons';
2
- import { radioItems, subMenuIfAny } from '@jbrowse/core/ui/menuItems';
2
+ import { highlightListMenuItems, radioItems, subMenuIfAny, } from '@jbrowse/core/ui/menuItems';
3
3
  import { makeShowSubMenu } from '@jbrowse/core/ui/showSubMenu';
4
- import { assembleLocStrings, getDialogHost, getSession, isSessionWithAddSessionTrack, toLocale, } from '@jbrowse/core/util';
4
+ import { assembleLocStrings, getDialogHost, getEnv, getSession, isSessionWithAddSessionTrack, toLocale, } from '@jbrowse/core/util';
5
5
  import { basePaintedAt, computeMoveToLayout, } from '@jbrowse/core/util/Base1DUtils';
6
6
  import { copyText } from '@jbrowse/core/util/copyText';
7
7
  import { renameIds } from '@jbrowse/core/util/types/mst';
@@ -78,6 +78,7 @@ export function buildHighlightsSubMenuItems(self) {
78
78
  session.setHighlightLabelsVisible(!session.highlightLabelsVisible);
79
79
  },
80
80
  },
81
+ ...highlightListMenuItems(session, getEnv(self).pluginManager),
81
82
  ];
82
83
  }
83
84
  /**