@jbrowse/plugin-linear-genome-view 5.0.0-beta.2 → 5.0.0-beta.20

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 (177) hide show
  1. package/esm/LinearGenomeView/afterAttach.d.ts +22 -5
  2. package/esm/LinearGenomeView/afterAttach.js +129 -106
  3. package/esm/LinearGenomeView/axisConfigSchema.d.ts +18 -0
  4. package/esm/LinearGenomeView/axisConfigSchema.js +22 -0
  5. package/esm/LinearGenomeView/components/CenterLine.js +10 -5
  6. package/esm/LinearGenomeView/components/ConnectedHoverHighlight.js +2 -3
  7. package/esm/LinearGenomeView/components/CrisprGuidePanel.js +3 -5
  8. package/esm/LinearGenomeView/components/ExportSvgDialog.d.ts +9 -4
  9. package/esm/LinearGenomeView/components/ExportSvgDialog.js +45 -8
  10. package/esm/LinearGenomeView/components/GetSequenceDialog.js +2 -2
  11. package/esm/LinearGenomeView/components/Header.js +11 -1
  12. package/esm/LinearGenomeView/components/HeaderClearHighlightButton.js +2 -2
  13. package/esm/LinearGenomeView/components/HeaderPanControls.d.ts +3 -3
  14. package/esm/LinearGenomeView/components/HeaderPanControls.js +6 -3
  15. package/esm/LinearGenomeView/components/HeaderSearchBoxes.js +4 -1
  16. package/esm/LinearGenomeView/components/HeaderZoomControls.js +6 -35
  17. package/esm/LinearGenomeView/components/Highlight.js +27 -11
  18. package/esm/LinearGenomeView/components/HighlightBand.d.ts +11 -2
  19. package/esm/LinearGenomeView/components/HighlightBand.js +32 -13
  20. package/esm/LinearGenomeView/components/HighlightChip.d.ts +3 -3
  21. package/esm/LinearGenomeView/components/HighlightChip.js +13 -8
  22. package/esm/LinearGenomeView/components/HighlightLabelDialog.d.ts +6 -0
  23. package/esm/LinearGenomeView/components/HighlightLabelDialog.js +18 -0
  24. package/esm/LinearGenomeView/components/HoverPositionHighlight.js +3 -3
  25. package/esm/LinearGenomeView/components/ImportForm.js +7 -9
  26. package/esm/LinearGenomeView/components/LinearGenomeView.js +5 -12
  27. package/esm/LinearGenomeView/components/LinearGenomeViewContainer.js +13 -14
  28. package/esm/LinearGenomeView/components/MiniControls.js +11 -5
  29. package/esm/LinearGenomeView/components/OverviewHighlight.js +3 -3
  30. package/esm/LinearGenomeView/components/OverviewHighlightBand.d.ts +3 -2
  31. package/esm/LinearGenomeView/components/OverviewHighlightBand.js +6 -5
  32. package/esm/LinearGenomeView/components/OverviewRubberband.js +25 -48
  33. package/esm/LinearGenomeView/components/OverviewScalebar.js +4 -5
  34. package/esm/LinearGenomeView/components/OverviewScalebarPolygon.js +5 -3
  35. package/esm/LinearGenomeView/components/PaddingBlocks.js +3 -8
  36. package/esm/LinearGenomeView/components/RangeSelectOverlay.js +5 -6
  37. package/esm/LinearGenomeView/components/RegionWidthEditorDialog.d.ts +3 -2
  38. package/esm/LinearGenomeView/components/RegionWidthEditorDialog.js +4 -4
  39. package/esm/LinearGenomeView/components/ResizeAllTracksHandle.d.ts +5 -0
  40. package/esm/LinearGenomeView/components/ResizeAllTracksHandle.js +74 -0
  41. package/esm/LinearGenomeView/components/Rubberband.d.ts +1 -2
  42. package/esm/LinearGenomeView/components/Rubberband.js +3 -2
  43. package/esm/LinearGenomeView/components/SVGHighlightBand.d.ts +10 -1
  44. package/esm/LinearGenomeView/components/SVGHighlightBand.js +16 -5
  45. package/esm/LinearGenomeView/components/Scalebar.d.ts +2 -6
  46. package/esm/LinearGenomeView/components/Scalebar.js +8 -5
  47. package/esm/LinearGenomeView/components/ScalebarRefNameLabels.js +51 -36
  48. package/esm/LinearGenomeView/components/SearchBox.js +10 -16
  49. package/esm/LinearGenomeView/components/SearchPanelForm.d.ts +3 -2
  50. package/esm/LinearGenomeView/components/SearchPanelForm.js +4 -2
  51. package/esm/LinearGenomeView/components/SearchResultsDialog.d.ts +4 -8
  52. package/esm/LinearGenomeView/components/SearchResultsDialog.js +7 -4
  53. package/esm/LinearGenomeView/components/SearchResultsTable.d.ts +4 -7
  54. package/esm/LinearGenomeView/components/SearchResultsTable.js +25 -53
  55. package/esm/LinearGenomeView/components/SequenceFeatureHoverHighlight.js +2 -3
  56. package/esm/LinearGenomeView/components/SequenceSearchDialog.d.ts +2 -4
  57. package/esm/LinearGenomeView/components/StrandCheckboxes.d.ts +3 -2
  58. package/esm/LinearGenomeView/components/StrandCheckboxes.js +4 -2
  59. package/esm/LinearGenomeView/components/TrackContainer.d.ts +2 -3
  60. package/esm/LinearGenomeView/components/TrackContainer.js +63 -10
  61. package/esm/LinearGenomeView/components/TrackLabel.js +14 -4
  62. package/esm/LinearGenomeView/components/TrackLabelButtons.js +2 -2
  63. package/esm/LinearGenomeView/components/TrackLabelDragHandle.js +4 -5
  64. package/esm/LinearGenomeView/components/TrackLabelMenu.js +11 -14
  65. package/esm/LinearGenomeView/components/TrackLabelRefNameWarning.d.ts +3 -4
  66. package/esm/LinearGenomeView/components/TrackLabelRefNameWarning.js +23 -10
  67. package/esm/LinearGenomeView/components/TrackRenderingContainer.d.ts +6 -4
  68. package/esm/LinearGenomeView/components/TrackRenderingContainer.js +12 -24
  69. package/esm/LinearGenomeView/components/TracksContainer.js +11 -23
  70. package/esm/LinearGenomeView/components/VerticalGuide.d.ts +1 -2
  71. package/esm/LinearGenomeView/components/VerticalGuide.js +3 -16
  72. package/esm/LinearGenomeView/components/fetchSequence.d.ts +1 -2
  73. package/esm/LinearGenomeView/components/fetchSequence.js +15 -7
  74. package/esm/LinearGenomeView/components/recentLocationsMenu.js +10 -0
  75. package/esm/LinearGenomeView/components/useRangeSelect.d.ts +0 -3
  76. package/esm/LinearGenomeView/components/useRangeSelect.js +26 -61
  77. package/esm/LinearGenomeView/components/useSearchBoxPrefs.d.ts +3 -1
  78. package/esm/LinearGenomeView/components/useSearchBoxPrefs.js +4 -2
  79. package/esm/LinearGenomeView/components/useSideScroll.d.ts +17 -2
  80. package/esm/LinearGenomeView/components/useSideScroll.js +156 -102
  81. package/esm/LinearGenomeView/components/util.d.ts +4 -0
  82. package/esm/LinearGenomeView/components/util.js +4 -2
  83. package/esm/LinearGenomeView/consts.d.ts +2 -0
  84. package/esm/LinearGenomeView/consts.js +3 -1
  85. package/esm/LinearGenomeView/containingLgv.d.ts +21 -0
  86. package/esm/LinearGenomeView/containingLgv.js +23 -0
  87. package/esm/LinearGenomeView/headerFit.d.ts +4 -2
  88. package/esm/LinearGenomeView/headerFit.js +20 -7
  89. package/esm/LinearGenomeView/index.d.ts +2 -7
  90. package/esm/LinearGenomeView/index.js +5 -7
  91. package/esm/LinearGenomeView/keyboardHandler.d.ts +8 -7
  92. package/esm/LinearGenomeView/keyboardHandler.js +61 -23
  93. package/esm/LinearGenomeView/launchKeys.d.ts +1 -1
  94. package/esm/LinearGenomeView/launchKeys.js +11 -6
  95. package/esm/LinearGenomeView/lazyDialogs.d.ts +16 -3
  96. package/esm/LinearGenomeView/lazyDialogs.js +1 -0
  97. package/esm/LinearGenomeView/linkedViewSync.d.ts +3 -11
  98. package/esm/LinearGenomeView/linkedViewSync.js +27 -49
  99. package/esm/LinearGenomeView/menuItems.d.ts +16 -18
  100. package/esm/LinearGenomeView/menuItems.js +264 -164
  101. package/esm/LinearGenomeView/model.d.ts +1192 -560
  102. package/esm/LinearGenomeView/model.js +805 -590
  103. package/esm/LinearGenomeView/rExport/exportLinearView.d.ts +17 -0
  104. package/esm/LinearGenomeView/rExport/exportLinearView.js +117 -0
  105. package/esm/LinearGenomeView/rExport/rScript.d.ts +110 -0
  106. package/esm/LinearGenomeView/rExport/rScript.js +524 -0
  107. package/esm/LinearGenomeView/sharedScaleContainer.d.ts +1 -1
  108. package/esm/LinearGenomeView/sharedScaleContainer.js +2 -12
  109. package/esm/LinearGenomeView/showFiles.d.ts +2 -0
  110. package/esm/LinearGenomeView/showFiles.js +19 -0
  111. package/esm/LinearGenomeView/showRegionsWithUndo.d.ts +1 -1
  112. package/esm/LinearGenomeView/showRegionsWithUndo.js +13 -5
  113. package/esm/LinearGenomeView/stickyChrome.d.ts +5 -9
  114. package/esm/LinearGenomeView/stickyChrome.js +5 -9
  115. package/esm/LinearGenomeView/svgcomponents/SVGGridlines.js +4 -4
  116. package/esm/LinearGenomeView/svgcomponents/SVGHeader.d.ts +4 -1
  117. package/esm/LinearGenomeView/svgcomponents/SVGHeader.js +8 -21
  118. package/esm/LinearGenomeView/svgcomponents/SVGHighlights.d.ts +3 -1
  119. package/esm/LinearGenomeView/svgcomponents/SVGHighlights.js +9 -9
  120. package/esm/LinearGenomeView/svgcomponents/SVGHighlightsOverlay.d.ts +3 -1
  121. package/esm/LinearGenomeView/svgcomponents/SVGHighlightsOverlay.js +5 -5
  122. package/esm/LinearGenomeView/svgcomponents/SVGLinearGenomeView.js +13 -18
  123. package/esm/LinearGenomeView/svgcomponents/SVGRegionSeparators.js +2 -5
  124. package/esm/LinearGenomeView/svgcomponents/SVGRowHeader.js +5 -6
  125. package/esm/LinearGenomeView/svgcomponents/SVGRuler.js +9 -10
  126. package/esm/LinearGenomeView/svgcomponents/SVGScalebar.js +3 -3
  127. package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.d.ts +26 -0
  128. package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.js +12 -0
  129. package/esm/LinearGenomeView/svgcomponents/SVGTrackLabel.js +21 -13
  130. package/esm/LinearGenomeView/svgcomponents/SVGTracks.d.ts +4 -5
  131. package/esm/LinearGenomeView/svgcomponents/SVGTracks.js +7 -6
  132. package/esm/LinearGenomeView/svgcomponents/SVGView.d.ts +2 -3
  133. package/esm/LinearGenomeView/svgcomponents/SVGView.js +17 -3
  134. package/esm/LinearGenomeView/svgcomponents/renderViewTracks.d.ts +24 -20
  135. package/esm/LinearGenomeView/svgcomponents/renderViewTracks.js +42 -29
  136. package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.js +74 -77
  137. package/esm/LinearGenomeView/svgcomponents/util.d.ts +28 -12
  138. package/esm/LinearGenomeView/svgcomponents/util.js +41 -29
  139. package/esm/LinearGenomeView/types.d.ts +13 -10
  140. package/esm/LinearGenomeView/types.js +0 -1
  141. package/esm/LinearGenomeView/util.d.ts +103 -46
  142. package/esm/LinearGenomeView/util.js +222 -170
  143. package/esm/MultiLevelRubberband/MultiLevelRubberband.js +9 -6
  144. package/esm/MultiLevelRubberband/index.d.ts +2 -0
  145. package/esm/MultiLevelRubberband/index.js +2 -0
  146. package/esm/MultiLevelRubberband/menuItems.d.ts +34 -0
  147. package/esm/MultiLevelRubberband/menuItems.js +52 -0
  148. package/esm/MultiLevelRubberband/rowLabel.d.ts +22 -0
  149. package/esm/MultiLevelRubberband/rowLabel.js +27 -0
  150. package/esm/MultiLevelRubberband/types.d.ts +1 -0
  151. package/esm/MultiLevelRubberband/useRangeSelect.d.ts +2 -1
  152. package/esm/MultiLevelRubberband/useRangeSelect.js +45 -59
  153. package/esm/TrackModifiers/index.d.ts +83 -0
  154. package/esm/TrackModifiers/index.js +231 -0
  155. package/esm/displayKitTests/perRegionTestEnv.d.ts +244 -68
  156. package/esm/displayKitTests/perRegionTestEnv.js +30 -26
  157. package/esm/index.d.ts +21 -99
  158. package/esm/index.js +17 -38
  159. package/esm/searchUtils.d.ts +26 -10
  160. package/esm/searchUtils.js +137 -137
  161. package/esm/shared/RubberbandSpan.d.ts +3 -2
  162. package/esm/shared/RubberbandSpan.js +6 -3
  163. package/esm/shared/coordLabels.d.ts +11 -4
  164. package/esm/shared/coordLabels.js +20 -5
  165. package/esm/shared/useWindowDrag.d.ts +18 -0
  166. package/esm/shared/useWindowDrag.js +47 -0
  167. package/package.json +12 -11
  168. package/esm/FeatureTrack/configSchema.d.ts +0 -124
  169. package/esm/FeatureTrack/configSchema.js +0 -54
  170. package/esm/FeatureTrack/index.d.ts +0 -2
  171. package/esm/FeatureTrack/index.js +0 -13
  172. package/esm/LinearGenomeView/initKeys.d.ts +0 -12
  173. package/esm/LinearGenomeView/initKeys.js +0 -68
  174. package/esm/MultiLevelRubberband/VerticalGuide.d.ts +0 -6
  175. package/esm/MultiLevelRubberband/VerticalGuide.js +0 -21
  176. package/esm/lazyPluginExports.d.ts +0 -23
  177. package/esm/lazyPluginExports.js +0 -36
@@ -1,17 +1,34 @@
1
1
  import type { LinearGenomeViewModel } from './model.ts';
2
2
  import type { InitState } from './types.ts';
3
- import type { AssemblyHost, NotificationSink } from '@jbrowse/core/util';
3
+ import type { AbstractSessionModel, AssemblyHost, NotificationSink } from '@jbrowse/core/util';
4
4
  /**
5
- * Apply an init blob's `highlight` entries to a view, coercing each locstring
6
- * or wire-format object and reporting a bad one without taking out its
7
- * siblings.
5
+ * Navigate a view to an init blob's `loc`, reporting a bad one as a warning
6
+ * (a gene that matched nothing) or an error rather than throwing. Exported
7
+ * because a LinearSyntenyView row is a LinearGenomeView that does not go
8
+ * through this file's init autorun, so it navigates each row itself.
9
+ */
10
+ export declare function navToInitLoc(view: Pick<LinearGenomeViewModel, 'navToLocString'>, session: NotificationSink, init: Pick<InitState, 'assembly' | 'grow' | 'showHitTrack' | 'tracks'> & {
11
+ loc: string;
12
+ }): Promise<void>;
13
+ /**
14
+ * Open an init blob's `tracks`, one at a time, a bare string taken as a list
15
+ * of one. launchTrack funnels through showTrackGeneric, which surfaces any
16
+ * failure (unresolved id, bad config) as its own snackbar, and loads the
17
+ * picked display's state model first when it is registered lazily. Exported
18
+ * for the LinearSyntenyView rows, as `navToInitLoc` is.
19
+ */
20
+ export declare function showInitTracks(self: LinearGenomeViewModel, init: Pick<InitState, 'tracks'>): Promise<void>;
21
+ /**
22
+ * Add an init blob's `highlight` entries to the session, coercing each
23
+ * locstring or wire-format object and reporting a bad one without taking out
24
+ * its siblings. An entry without an assembly takes the launch's.
8
25
  *
9
26
  * Exported because a LinearSyntenyView row is a LinearGenomeView that does NOT
10
27
  * go through this file's init autorun — the synteny view builds each row's
11
28
  * snapshot and navigates it itself — so the row's own `highlight` had nowhere
12
29
  * to be applied and was dropped in silence.
13
30
  */
14
- export declare function applyInitHighlights(self: LinearGenomeViewModel, session: AssemblyHost & NotificationSink, init: Pick<InitState, 'highlight' | 'assembly'>): void;
31
+ export declare function applyInitHighlights(session: AssemblyHost & NotificationSink & Pick<AbstractSessionModel, 'addHighlight'>, init: Pick<InitState, 'highlight' | 'assembly'>): void;
15
32
  /**
16
33
  * Autorun that handles the init state - navigating to initial location,
17
34
  * showing tracks, etc.
@@ -1,9 +1,10 @@
1
- import { getSession, isSessionModelWithWidgets, localStorageSetItem, resolveNamedRegions, } from '@jbrowse/core/util';
1
+ import { getSession, isSessionModelWithWidgets, resolveNamedRegions, } from '@jbrowse/core/util';
2
2
  import { coerceHighlight } from '@jbrowse/core/util/highlights';
3
3
  import { installInitAutorun } from '@jbrowse/core/util/installInitAutorun';
4
- import { normalizeTrackInit } from '@jbrowse/core/util/tracks';
4
+ import { normalizeTrackInit, warmTrackDisplayGeneric, } from '@jbrowse/core/util/tracks';
5
+ import { requestedDisplayType } from '@jbrowse/launch-args/displayAlias';
5
6
  import { addDisposer } from '@jbrowse/mobx-state-tree';
6
- import { autorun, when } from 'mobx';
7
+ import { autorun, untracked, when } from 'mobx';
7
8
  import { SearchResultsNotFoundError } from '../searchUtils.js';
8
9
  // `init` is a frozen blob that often comes from hand-authored JSON, where a
9
10
  // lone entry gets written bare (`tracks: 'genes'`). A string is iterable, so
@@ -21,24 +22,17 @@ async function openTracklist(self, session, superseded) {
21
22
  // activateTrackSelector throws without widget support, which would abort the
22
23
  // rest of init (navigation included) over an optional extra
23
24
  if (isSessionModelWithWidgets(session)) {
24
- // `poppedOut` — the visible widget rendered in a modal, leaving the drawer
25
- // column free — lives on SessionWithDrawerWidgets, whose type guard is
26
- // product-core's, so duck-type it here rather than take an upward
27
- // dependency. A session without the key has no modal to pop out into, so
28
- // false is the right answer for it.
29
- const poppedOut = 'poppedOut' in session && session.poppedOut === true;
30
- // the same line App.tsx draws to decide whether to render the drawer column
31
- // at all. A *minimized* drawer still holds a visibleWidget while taking no
32
- // width, and showWidget un-minimizes it (product-core's DrawerWidgets), so
33
- // `!!visibleWidget` alone reads "already open", skips the wait below, and
34
- // hands navigation the pre-drawer width — the exact thing this is here to
35
- // prevent.
36
- const drawerWasOpen = !!session.visibleWidget && !session.minimized && !poppedOut;
25
+ // `drawerVisible` is the line App.tsx draws to render the drawer column at
26
+ // all: a minimized, popped-out or modal-only widget takes no width. It
27
+ // lives on SessionWithDrawerWidgets, whose type guard is product-core's, so
28
+ // it is duck-typed rather than taken as an upward dependency; a session
29
+ // without it has no drawer column.
30
+ const drawerVisible = () => 'drawerVisible' in session && session.drawerVisible === true;
31
+ const drawerWasVisible = drawerVisible();
37
32
  const widthBefore = self.volatileWidth;
38
33
  self.activateTrackSelector();
39
- // a width change is only coming if the drawer is about to start taking
40
- // space; popped out it never does, so don't sit out the timeout for it
41
- if (!drawerWasOpen && !poppedOut) {
34
+ // a width change is only coming if the drawer just started taking space
35
+ if (!drawerWasVisible && drawerVisible()) {
42
36
  // Bounded so init can't wedge here if the drawer doesn't shrink the view
43
37
  // (e.g. embedded or modal-drawer layouts, where no width change is
44
38
  // coming), and superseded so a re-launch landing mid-wait isn't held
@@ -88,86 +82,102 @@ function showNamedRegions(self, session, assemblyName, names) {
88
82
  }
89
83
  }
90
84
  }
91
- async function navigateInit(self, session, init) {
85
+ function reportInitNavError(session, e) {
86
+ console.error(e);
87
+ if (e instanceof SearchResultsNotFoundError) {
88
+ // a gene name that matched nothing is a soft miss, not an app error
89
+ session.notify(e.message, 'warning');
90
+ }
91
+ else {
92
+ session.notifyError(`${e}`, e);
93
+ }
94
+ }
95
+ /**
96
+ * Navigate a view to an init blob's `loc`, reporting a bad one as a warning
97
+ * (a gene that matched nothing) or an error rather than throwing. Exported
98
+ * because a LinearSyntenyView row is a LinearGenomeView that does not go
99
+ * through this file's init autorun, so it navigates each row itself.
100
+ */
101
+ export async function navToInitLoc(view, session, init) {
92
102
  try {
93
- if (init.loc) {
94
- // navToLocString waits for the assembly itself, and this autorun only
95
- // runs once `initialized` confirms init.assembly has loaded regions, so
96
- // no explicit waitForAssembly is needed here
97
- // a spec that listed its tracks gets those tracks; one that only named
98
- // a gene gets the track the gene was found in, as the search box would
99
- await self.navToLocString(init.loc, init.assembly, init.grow, {
100
- showHitTrack: asArray(init.tracks).length === 0,
101
- });
102
- }
103
- else if (init.displayedRegionNames?.length) {
104
- // an explicit region list is a navigation request just like `loc`, so it
105
- // applies even when regions already exist (URL params layered onto a
106
- // defaultSession that already navigated)
107
- //
108
- // `?.length`, not the bare key: an empty array is truthy, so a spec
109
- // writing `displayedRegionNames: []` took this path with nothing to name
110
- // and got told its list of no names had matched no regions. That is why
111
- // LinearSyntenyView's doSubmit omits the key rather than passing [].
112
- showNamedRegions(self, session, init.assembly, init.displayedRegionNames);
113
- }
114
- else if (!self.hasDisplayedRegions) {
115
- // a highlight-only init (nothing to navigate to) must not clobber a
116
- // defaultSession's existing navigation, so only auto-navigate when
117
- // nothing is shown yet
118
- self.showAllRegionsInAssembly(init.assembly);
119
- }
103
+ // navToLocString waits for the assembly itself. A spec that listed its
104
+ // tracks gets those tracks; one that only named a gene gets the track the
105
+ // gene was found in, as the search box would
106
+ await view.navToLocString(init.loc, init.assembly, init.grow, {
107
+ showHitTrack: init.showHitTrack ?? asArray(init.tracks).length === 0,
108
+ });
120
109
  }
121
110
  catch (e) {
122
- console.error(init, e);
123
- if (e instanceof SearchResultsNotFoundError) {
124
- // a &loc= gene name that matched nothing is a soft miss, not an app error
125
- session.notify(e.message, 'warning');
111
+ reportInitNavError(session, e);
112
+ }
113
+ }
114
+ async function navigateInit(self, session, init) {
115
+ if (init.loc) {
116
+ await navToInitLoc(self, session, { ...init, loc: init.loc });
117
+ }
118
+ else {
119
+ try {
120
+ navigateWithoutLoc(self, session, init);
126
121
  }
127
- else {
128
- session.notifyError(`${e}`, e);
122
+ catch (e) {
123
+ reportInitNavError(session, e);
129
124
  }
130
125
  }
131
126
  }
132
- // launchTrack funnels through showTrackGeneric, which surfaces any failure
133
- // (unresolved id, bad config, etc) as its own snackbar, and loads the picked
134
- // display's state model first when it is registered lazily
135
- async function showInitTracks(self, init) {
136
- for (const t of asArray(init.tracks)) {
137
- const { trackId, trackSnapshot, displaySnapshot } = normalizeTrackInit(t);
138
- await self.launchTrack(trackId, trackSnapshot, displaySnapshot);
127
+ function navigateWithoutLoc(self, session, init) {
128
+ if (init.displayedRegionNames?.length) {
129
+ // an explicit region list is a navigation request just like `loc`, so it
130
+ // applies even when regions already exist (URL params layered onto a
131
+ // defaultSession that already navigated)
132
+ //
133
+ // `?.length`, not the bare key: an empty array is truthy, so a spec
134
+ // writing `displayedRegionNames: []` took this path with nothing to name
135
+ // and got told its list of no names had matched no regions. That is why
136
+ // LinearSyntenyView's doSubmit omits the key rather than passing [].
137
+ showNamedRegions(self, session, init.assembly, init.displayedRegionNames);
138
+ }
139
+ else if (!self.hasDisplayedRegions) {
140
+ // a highlight-only init (nothing to navigate to) must not clobber a
141
+ // defaultSession's existing navigation, so only auto-navigate when
142
+ // nothing is shown yet
143
+ self.showAllRegionsInAssembly(init.assembly);
139
144
  }
140
145
  }
141
- // backfill assemblyName on any session-authored highlights that omitted it so
142
- // downstream code (bookmark widget grid, addBookmark, etc) doesn't have to keep
143
- // falling back
144
- function backfillHighlightAssemblies(self) {
145
- const fallback = self.assemblyNames[0];
146
- if (self.highlight.length && fallback) {
147
- const normalized = self.highlight.map(h => h.assemblyName ? h : { ...h, assemblyName: fallback });
148
- if (normalized.some((h, i) => h !== self.highlight[i])) {
149
- self.setHighlight(normalized);
146
+ /**
147
+ * Open an init blob's `tracks`, one at a time, a bare string taken as a list
148
+ * of one. launchTrack funnels through showTrackGeneric, which surfaces any
149
+ * failure (unresolved id, bad config) as its own snackbar, and loads the
150
+ * picked display's state model first when it is registered lazily. Exported
151
+ * for the LinearSyntenyView rows, as `navToInitLoc` is.
152
+ */
153
+ export async function showInitTracks(self, init) {
154
+ const tracks = asArray(init.tracks);
155
+ if (tracks.length) {
156
+ // lazy, so the modifier and slot tables stay out of every page's first load
157
+ const { launchTrackInit } = await import('../TrackModifiers/index.js');
158
+ for (const t of tracks) {
159
+ await launchTrackInit(self, t);
150
160
  }
151
161
  }
152
162
  }
153
163
  /**
154
- * Apply an init blob's `highlight` entries to a view, coercing each locstring
155
- * or wire-format object and reporting a bad one without taking out its
156
- * siblings.
164
+ * Add an init blob's `highlight` entries to the session, coercing each
165
+ * locstring or wire-format object and reporting a bad one without taking out
166
+ * its siblings. An entry without an assembly takes the launch's.
157
167
  *
158
168
  * Exported because a LinearSyntenyView row is a LinearGenomeView that does NOT
159
169
  * go through this file's init autorun — the synteny view builds each row's
160
170
  * snapshot and navigates it itself — so the row's own `highlight` had nowhere
161
171
  * to be applied and was dropped in silence.
162
172
  */
163
- export function applyInitHighlights(self, session, init) {
173
+ export function applyInitHighlights(session, init) {
164
174
  for (const h of asArray(init.highlight)) {
165
175
  // parseLocString throws on an unknown refName or a malformed locstring, and
166
176
  // one bad entry must take out neither its siblings nor the rest of init
167
177
  try {
168
- const highlight = coerceHighlight(h, init.assembly, refName => session.assemblyManager.isValidRefName(refName, init.assembly));
178
+ const highlight = coerceHighlight(h, init.assembly, (refName, asm) => session.assemblyManager.isValidRefName(refName, asm ?? init.assembly));
169
179
  if (highlight) {
170
- self.addToHighlights(highlight);
180
+ session.addHighlight(highlight);
171
181
  }
172
182
  }
173
183
  catch (e) {
@@ -176,8 +186,14 @@ export function applyInitHighlights(self, session, init) {
176
186
  }
177
187
  }
178
188
  }
179
- async function applyInit(self, init, { superseded }) {
189
+ async function applyInit(self, pending, { superseded }) {
180
190
  const session = getSession(self);
191
+ // A re-launch of a view already showing an assembly need not restate it:
192
+ // jb.setSession writes `loc` and track entries beside the built state, and
193
+ // the readiness gate resolved the same default before letting this run.
194
+ const init = pending.assembly
195
+ ? pending
196
+ : { ...pending, assembly: self.launchAssemblyName ?? pending.assembly };
181
197
  if (init.tracklist) {
182
198
  await openTracklist(self, session, superseded);
183
199
  }
@@ -186,9 +202,8 @@ async function applyInit(self, init, { superseded }) {
186
202
  // reading or mutating a detached node throws) and also covers the case
187
203
  // isAlive misses — a newer setLaunch landed mid-apply, so the rest of *this*
188
204
  // blob is stale. Finishing it anyway appends its tracks and highlights under
189
- // the one that replaced it, and addToHighlights pushes, so a re-launch of the
190
- // same spec (a StrictMode remount) doubles the bands. The drain loop applies
191
- // the newer init next either way.
205
+ // the one that replaced it. The drain loop applies the newer init next either
206
+ // way.
192
207
  //
193
208
  // Checked after each await rather than once at the top, and as early returns
194
209
  // rather than nesting, so a step added later takes a guard beside it instead
@@ -207,8 +222,38 @@ async function applyInit(self, init, { superseded }) {
207
222
  if (init.nav !== undefined) {
208
223
  self.setHideHeader(!init.nav);
209
224
  }
210
- backfillHighlightAssemblies(self);
211
- applyInitHighlights(self, session, init);
225
+ applyInitHighlights(session, init);
226
+ }
227
+ /**
228
+ * The init autorun below waits for the view's width, which exists only once its
229
+ * component has rendered, and then loads the assembly and launches the tracks
230
+ * one at a time. The assembly and the display code each pending track needs can
231
+ * start loading now, all at once.
232
+ */
233
+ function setupInitWarmup(self) {
234
+ addDisposer(self, autorun(function initWarmupAutorun() {
235
+ const pending = self.pendingLaunch;
236
+ const tracks = asArray(pending?.tracks);
237
+ // eslint-disable-next-line no-restricted-syntax -- EFFECT INPUT: the assembly and track configs only feed the loads started here; the pending launch is the key
238
+ untracked(() => {
239
+ const assembly = pending?.assembly ?? self.launchAssemblyName;
240
+ if (pending && assembly) {
241
+ try {
242
+ getSession(self)
243
+ .assemblyManager.waitForAssembly(assembly)
244
+ .catch(() => { });
245
+ }
246
+ catch {
247
+ // navigation reports an assembly that cannot load
248
+ }
249
+ }
250
+ for (const t of tracks) {
251
+ const { trackId, displaySnapshot, modifiers } = normalizeTrackInit(t);
252
+ const type = requestedDisplayType(modifiers);
253
+ warmTrackDisplayGeneric(self, trackId, type ? { ...displaySnapshot, type } : displaySnapshot, assembly);
254
+ }
255
+ });
256
+ }, { name: 'LGVInitWarmup' }));
212
257
  }
213
258
  /**
214
259
  * Autorun that handles the init state - navigating to initial location,
@@ -219,9 +264,8 @@ export function setupInitAutorun(self) {
219
264
  name: 'LGVInit',
220
265
  // `init` is what makes hasSomethingToShow / awaitingInitNavigation report
221
266
  // "loading"
222
- // until navigation populates displayedRegions, so the alternative gate
223
- // (clearing `init` up front, the way SpreadsheetView does) would flash the
224
- // import form mid-load.
267
+ // until navigation populates displayedRegions, so clearing `init` up front
268
+ // would flash the import form mid-load.
225
269
  ready: () => self.initialized,
226
270
  // No pre-materialization phase to protect: the view is its own single row,
227
271
  // and `error` already derives a failed init assembly declaratively (via
@@ -244,32 +288,11 @@ function setupCoarseDynamicBlocksAutorun(self) {
244
288
  name: 'LGVCoarseDynamicBlocks',
245
289
  }));
246
290
  }
247
- /**
248
- * Autorun that syncs view settings to localStorage
249
- */
250
- function setupLocalStorageAutorun(self) {
251
- addDisposer(self, autorun(function localStorageAutorun() {
252
- const s = (s) => JSON.stringify(s);
253
- const { showCytobands, showCenterLine, colorByCDS, showAminoAcids, showTrackOutlines, trackLabels, } = self;
254
- localStorageSetItem('lgv-showCytobands', s(showCytobands));
255
- localStorageSetItem('lgv-showCenterLine', s(showCenterLine));
256
- localStorageSetItem('lgv-colorByCDS', s(colorByCDS));
257
- localStorageSetItem('lgv-showAminoAcids', s(showAminoAcids));
258
- localStorageSetItem('lgv-showTrackOutlines', s(showTrackOutlines));
259
- // skip writing the empty default — otherwise reads later round-trip to
260
- // '' instead of null, hiding the config-fallback path
261
- if (trackLabels) {
262
- localStorageSetItem('lgv-trackLabels', trackLabels);
263
- }
264
- }, {
265
- name: 'LGVLocalStorage',
266
- }));
267
- }
268
291
  /**
269
292
  * Sets up all afterAttach autoruns for the LinearGenomeView
270
293
  */
271
294
  export function doAfterAttach(self) {
295
+ setupInitWarmup(self);
272
296
  setupInitAutorun(self);
273
297
  setupCoarseDynamicBlocksAutorun(self);
274
- setupLocalStorageAutorun(self);
275
298
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * #config LinearGenomeViewAxis
3
+ * #category view
4
+ * The linear genome view's `axis`: the ruler along its top, whose ticks
5
+ * `grid` carries down through the tracks.
6
+ *
7
+ * #example
8
+ * ```js
9
+ * { type: 'LinearGenomeView', axis: { grid: false } }
10
+ * ```
11
+ */
12
+ export declare const linearGenomeViewAxisConfigSchema: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
13
+ readonly grid: {
14
+ type: "boolean";
15
+ } & {
16
+ defaultValue: boolean;
17
+ };
18
+ }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined, undefined, import("@jbrowse/core/configuration").ConfigurationSchemaRequirement<Partial<Record<string, string[]>>, string>, any>>;
@@ -0,0 +1,22 @@
1
+ import { ConfigurationSchema } from '@jbrowse/core/configuration';
2
+ /**
3
+ * #config LinearGenomeViewAxis
4
+ * #category view
5
+ * The linear genome view's `axis`: the ruler along its top, whose ticks
6
+ * `grid` carries down through the tracks.
7
+ *
8
+ * #example
9
+ * ```js
10
+ * { type: 'LinearGenomeView', axis: { grid: false } }
11
+ * ```
12
+ */
13
+ export const linearGenomeViewAxisConfigSchema = ConfigurationSchema('LinearGenomeViewAxis', {
14
+ /**
15
+ * #slot
16
+ */
17
+ grid: {
18
+ type: 'boolean',
19
+ defaultValue: true,
20
+ description: "carry the ruler's ticks down through the tracks",
21
+ },
22
+ }, { closed: true });
@@ -1,4 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { clamp } from '@jbrowse/core/util';
3
+ import { basePaintedAt, bpIntoPaintedBase, } from '@jbrowse/core/util/Base1DUtils';
2
4
  import { makeStyles } from '@jbrowse/core/util/tss-react';
3
5
  import CloseIcon from '@mui/icons-material/Close';
4
6
  import { IconButton, Tooltip } from '@mui/material';
@@ -18,6 +20,7 @@ const useStyles = makeStyles()(theme => ({
18
20
  centerLineText: {
19
21
  position: 'absolute',
20
22
  left: 0,
23
+ bottom: 0,
21
24
  display: 'flex',
22
25
  alignItems: 'center',
23
26
  gap: 2,
@@ -31,16 +34,18 @@ const useStyles = makeStyles()(theme => ({
31
34
  },
32
35
  }));
33
36
  const CenterLine = observer(function CenterLine({ model }) {
34
- const { bpPerPx, centerLineInfo, trackHeights, tracks, width } = model;
37
+ const { bpPerPx, centerLineInfo, tracks, width } = model;
35
38
  const { classes } = useStyles();
36
- const startingPosition = width / 2;
39
+ const left = centerLineInfo && bpPerPx < 1
40
+ ? width / 2 -
41
+ bpIntoPaintedBase(centerLineInfo, centerLineInfo.offset) / bpPerPx
42
+ : width / 2;
37
43
  return tracks.length ? (_jsx("div", { "data-testid": "centerline_container", className: classes.centerLineContainer, role: "presentation", style: {
38
- transform: `translateX(${startingPosition}px)`,
44
+ transform: `translateX(${left}px)`,
39
45
  width: Math.max(1 / bpPerPx, 1),
40
46
  }, children: centerLineInfo && (_jsxs("div", { "data-testid": "centerline_text", className: classes.centerLineText, role: "presentation", style: {
41
47
  transform: `translateX(${Math.max(1 / bpPerPx, 1) + 5}px)`,
42
- top: trackHeights,
43
- }, children: [centerLineInfo.refName, ":", ' ', Math.max(Math.round(centerLineInfo.offset) + 1, 0).toLocaleString('en-US'), _jsx(Tooltip, { title: "Hide center line", children: _jsx(IconButton, { className: classes.dismiss, onClick: () => {
48
+ }, children: [centerLineInfo.refName, ":", ' ', (clamp(basePaintedAt(centerLineInfo, centerLineInfo.offset), centerLineInfo.start, centerLineInfo.end - 1) + 1).toLocaleString('en-US'), _jsx(Tooltip, { title: "Hide center line", children: _jsx(IconButton, { className: classes.dismiss, onClick: () => {
44
49
  model.setShowCenterLine(false);
45
50
  }, children: _jsx(CloseIcon, { fontSize: "inherit" }) }) })] })) })) : null;
46
51
  });
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
- import { getSession } from '@jbrowse/core/util';
2
+ import { getSession, isSessionModelWithWidgets } from '@jbrowse/core/util';
3
3
  import { observer } from 'mobx-react';
4
4
  import HoverPositionHighlight from './HoverPositionHighlight.js';
5
5
  // Draws a crosshair for every session widget "connected" to this view that is
@@ -9,8 +9,7 @@ import HoverPositionHighlight from './HoverPositionHighlight.js';
9
9
  // hovered interval or undefined; the widget iteration and drawing are shared.
10
10
  const ConnectedHoverHighlight = observer(function ConnectedHoverHighlight({ model, getPosition, }) {
11
11
  const session = getSession(model);
12
- const widgets = 'widgets' in session ? session.widgets : undefined;
13
- return !widgets ? null : (_jsx(_Fragment, { children: [...widgets].flatMap(([id, widget]) => {
12
+ return !isSessionModelWithWidgets(session) ? null : (_jsx(_Fragment, { children: [...session.widgets].flatMap(([id, widget]) => {
14
13
  const position = getPosition(widget, model.id);
15
14
  return position
16
15
  ? [
@@ -64,15 +64,14 @@ const CrisprGuidePanel = observer(function CrisprGuidePanel({ model, handleClose
64
64
  const [pamLocation, setPamLocation] = useState(DEFAULT_PRESET.pamLocation);
65
65
  const [guideLengthStr, setGuideLengthStr] = useState(String(DEFAULT_PRESET.guideLength));
66
66
  const [cutOffsetStr, setCutOffsetStr] = useState(String(DEFAULT_PRESET.cutOffset));
67
- // presets carry this; Custom is treated as a blunt cutter, so the panel does
68
- // not grow a second offset field for a case config can express directly
69
- const [cutOffsetBottom, setCutOffsetBottom] = useState(DEFAULT_PRESET.cutOffsetBottom);
70
67
  const [searchForward, setSearchForward] = useState(true);
71
68
  const [searchReverse, setSearchReverse] = useState(true);
72
69
  const [filterQuality, setFilterQuality] = useState(false);
73
70
  // `Number('')` is 0, so an emptied field would otherwise read as a valid zero
74
71
  const guideLength = toBpCount(guideLengthStr);
75
72
  const cutOffset = toBpCount(cutOffsetStr);
73
+ // Custom is a blunt cutter: config can express a staggered one directly
74
+ const cutOffsetBottom = ENZYME_PRESETS[enzyme]?.cutOffsetBottom ?? cutOffset;
76
75
  // both are bp counts used for fixed-length string slicing, so a fractional
77
76
  // value would silently truncate and skew the placement
78
77
  const guideLengthValid = Number.isInteger(guideLength) && guideLength > 0;
@@ -101,7 +100,6 @@ const CrisprGuidePanel = observer(function CrisprGuidePanel({ model, handleClose
101
100
  setPamLocation(preset.pamLocation);
102
101
  setGuideLengthStr(String(preset.guideLength));
103
102
  setCutOffsetStr(String(preset.cutOffset));
104
- setCutOffsetBottom(preset.cutOffsetBottom);
105
103
  }
106
104
  }
107
105
  function handleSubmit() {
@@ -116,7 +114,7 @@ const CrisprGuidePanel = observer(function CrisprGuidePanel({ model, handleClose
116
114
  guideLength,
117
115
  pamLocation,
118
116
  cutOffset,
119
- cutOffsetBottom: enzyme === 'Custom' ? cutOffset : cutOffsetBottom,
117
+ cutOffsetBottom,
120
118
  searchForward,
121
119
  searchReverse,
122
120
  excludePolyT: filterQuality,
@@ -1,11 +1,16 @@
1
- import type { TrackLabelMode } from '../types.ts';
1
+ import type { TrackLabelMode, ViewTrackLabelMode } from '../types.ts';
2
2
  import type { BaseExportSvgOptions } from '@jbrowse/core/ui';
3
- export default function ExportSvgDialog({ model, handleClose, }: {
3
+ declare const ExportSvgDialog: ({ model, handleClose, chooseWidth, }: {
4
4
  model: {
5
+ width?: number;
6
+ effectiveTrackLabels?: ViewTrackLabelMode;
5
7
  exportSvg(opts: BaseExportSvgOptions & {
6
8
  trackLabels: TrackLabelMode;
7
9
  showGridlines: boolean;
8
- }): Promise<void>;
10
+ viewWidth?: number;
11
+ }): Promise<unknown>;
9
12
  };
10
13
  handleClose: () => void;
11
- }): import("react").JSX.Element;
14
+ chooseWidth?: boolean;
15
+ }) => import("react").JSX.Element;
16
+ export default ExportSvgDialog;
@@ -1,14 +1,51 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
2
3
  import { BaseExportSvgDialog, LabeledCheckbox, useExportSvgPreference, } from '@jbrowse/core/ui';
3
4
  import { MenuItem, TextField } from '@mui/material';
5
+ import { observer } from 'mobx-react';
6
+ const MIN_WIDTH = 100;
7
+ const MAX_WIDTH = 10000;
8
+ const TRACK_LABEL_MODES = [
9
+ { mode: 'offset', label: 'Offset' },
10
+ { mode: 'overlapping', label: 'Overlapping' },
11
+ { mode: 'left', label: 'Left' },
12
+ { mode: 'hidden', label: 'Hidden' },
13
+ ];
4
14
  // Shared track-label + gridlines export dialog. Used by LGV, linear-synteny and
5
- // breakpoint-split views (their lazyDialogs re-export this).
6
- export default function ExportSvgDialog({ model, handleClose, }) {
7
- const [trackLabels, setTrackLabels] = useExportSvgPreference('tracklabels', 'offset');
15
+ // breakpoint-split views (their lazyDialogs re-export this). Only a caller
16
+ // passing `chooseWidth` offers the width field, whose model lays itself out at
17
+ // `viewWidth`.
18
+ const ExportSvgDialog = observer(function ExportSvgDialog({ model, handleClose, chooseWidth, }) {
19
+ // The view's own setting is already an export mode, so a user who has not
20
+ // picked one here gets the figure their screen shows. The key is `-mode`
21
+ // because the modes were once spelled `overlay`/`none`, and a stored one of
22
+ // those would select nothing in the dropdown.
23
+ const [trackLabels, setTrackLabels] = useExportSvgPreference('tracklabels-mode', model.effectiveTrackLabels ?? 'offset');
8
24
  const [showGridlines, setShowGridlines] = useExportSvgPreference('gridlines', false);
9
- return (_jsx(BaseExportSvgDialog, { model: model, handleClose: handleClose, exportSvg: opts => model.exportSvg({ ...opts, trackLabels, showGridlines }), checkboxes: _jsx(LabeledCheckbox, { checked: showGridlines, onChange: val => {
25
+ // not a stored preference: a remembered width would drift from the screen's
26
+ const [widthText, setWidthText] = useState(() => String(Math.round(model.width ?? 0)));
27
+ const typedWidth = Number(widthText);
28
+ const widthValid = Number.isInteger(typedWidth) &&
29
+ typedWidth >= MIN_WIDTH &&
30
+ typedWidth <= MAX_WIDTH;
31
+ return (_jsxs(BaseExportSvgDialog, { model: model, handleClose: handleClose, exportSvg: opts => model.exportSvg({
32
+ ...opts,
33
+ trackLabels,
34
+ showGridlines,
35
+ viewWidth: chooseWidth && widthValid ? typedWidth : undefined,
36
+ }), checkboxes: _jsx(LabeledCheckbox, { checked: showGridlines, onChange: val => {
10
37
  setShowGridlines(val);
11
- }, label: "Show gridlines" }), children: _jsxs(TextField, { select: true, label: "Track label positioning", variant: "outlined", style: { minWidth: 200 }, value: trackLabels, onChange: event => {
12
- setTrackLabels(event.target.value);
13
- }, children: [_jsx(MenuItem, { value: "offset", children: "Offset" }), _jsx(MenuItem, { value: "overlay", children: "Overlay" }), _jsx(MenuItem, { value: "left", children: "Left" }), _jsx(MenuItem, { value: "none", children: "None" })] }) }));
14
- }
38
+ }, label: "Show gridlines" }), children: [_jsx(TextField, { select: true, label: "Track label positioning", variant: "outlined", style: { minWidth: 200 }, value: trackLabels, onChange: event => {
39
+ const picked = TRACK_LABEL_MODES.find(m => m.mode === event.target.value);
40
+ if (picked) {
41
+ setTrackLabels(picked.mode);
42
+ }
43
+ }, children: TRACK_LABEL_MODES.map(({ mode, label }) => (_jsx(MenuItem, { value: mode, children: label }, mode))) }), chooseWidth ? (_jsx(TextField, { label: "Width (px)", variant: "outlined", type: "number", value: widthText, error: !widthValid, helperText: widthValid
44
+ ? 'Lays the tracks out as a window this wide would; text keeps its size'
45
+ : `A whole number from ${MIN_WIDTH} to ${MAX_WIDTH}`, slotProps: {
46
+ htmlInput: { min: MIN_WIDTH, max: MAX_WIDTH, step: 1 },
47
+ }, onChange: event => {
48
+ setWidthText(event.target.value);
49
+ } })) : null] }));
50
+ });
51
+ export default ExportSvgDialog;
@@ -32,12 +32,12 @@ const GetSequenceDialog = observer(function GetSequenceDialog({ model, regions,
32
32
  // a selection this dialog will happily take is a whole chromosome of
33
33
  // sequence, so both handles are forwarded: closing the dialog stops the
34
34
  // read rather than leaving the worker on it, and the wait is named
35
- async (_name, _locs, stopToken, statusCallback) => {
35
+ async (_name, _locs, signal, statusCallback) => {
36
36
  if (regions.length === 0) {
37
37
  throw new Error('Selected region is out of bounds');
38
38
  }
39
39
  const chunks = await fetchSequence(model, regions, {
40
- stopToken,
40
+ signal,
41
41
  statusCallback,
42
42
  });
43
43
  // validate here (in the async path) so a length mismatch surfaces via the
@@ -1,7 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton';
2
3
  import ScrollZoomToggle from '@jbrowse/core/ui/ScrollZoomToggle';
4
+ import { getSession } from '@jbrowse/core/util';
3
5
  import { makeStyles } from '@jbrowse/core/util/tss-react';
4
6
  import useMeasure from '@jbrowse/core/util/useMeasure';
7
+ import MenuIcon from '@mui/icons-material/Menu';
8
+ import { useMediaQuery } from '@mui/material';
5
9
  import { observer } from 'mobx-react';
6
10
  import { HEADER_BAR_HEIGHT } from '../consts.js';
7
11
  import { headerFit } from '../headerFit.js';
@@ -35,12 +39,18 @@ const Controls = observer(function Controls({ model, }) {
35
39
  // row
36
40
  const [ref, { width }] = useMeasure('width');
37
41
  const highlighted = highlightedDisplays(model);
42
+ const viewMenu = getSession(model).viewTitleBars === false;
43
+ const touch = useMediaQuery('(pointer: coarse)', {
44
+ noSsr: true,
45
+ });
38
46
  const fit = headerFit({
39
47
  width,
40
48
  searchBoxPx: searchBoxWidth(model.coarseVisibleLocStrings),
41
49
  clearHighlight: highlighted.length > 0,
50
+ viewMenu,
51
+ touch,
42
52
  });
43
- return (_jsxs("div", { className: classes.headerBar, ref: ref, children: [_jsx(HeaderTrackSelectorButton, { model: model, indent: fit.trackSelectorIndent }), _jsx(ScrollZoomToggle, { model: model, iconOnly: !fit.scrollZoomLabel }), _jsx("div", { className: classes.spacer }), _jsx(HeaderPanControls, { model: model, compact: !fit.panButtonSpacing }), _jsx(SearchBox, { model: model }), _jsx(HeaderClearHighlightButton, { highlighted: highlighted }), fit.regionWidth ? _jsx(HeaderRegionWidth, { model: model }) : null, _jsx(HeaderZoomControls, { model: model, showSlider: fit.zoomSlider }), _jsx("div", { className: classes.spacer })] }));
53
+ return (_jsxs("div", { className: classes.headerBar, ref: ref, children: [viewMenu ? (_jsx(CascadingMenuButton, { tooltip: "View menu", menuItems: () => model.menuItems(), "data-testid": "view_menu_icon", children: _jsx(MenuIcon, {}) })) : null, _jsx(HeaderTrackSelectorButton, { model: model, indent: fit.trackSelectorIndent }), touch ? null : (_jsx(ScrollZoomToggle, { model: model, iconOnly: !fit.scrollZoomLabel })), _jsx("div", { className: classes.spacer }), touch ? null : (_jsx(HeaderPanControls, { model: model, compact: !fit.panButtonSpacing })), _jsx(SearchBox, { model: model }), _jsx(HeaderClearHighlightButton, { highlighted: highlighted }), fit.regionWidth ? _jsx(HeaderRegionWidth, { model: model }) : null, _jsx(HeaderZoomControls, { model: model, showSlider: fit.zoomSlider }), _jsx("div", { className: classes.spacer })] }));
44
54
  });
45
55
  const LinearGenomeViewHeader = observer(function LinearGenomeViewHeader({ model, }) {
46
56
  const { hideHeader, hideHeaderOverview } = model;