@theia/core 1.77.0-next.0 → 1.77.0-next.12

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 (65) hide show
  1. package/lib/browser/catalog.json +256 -14
  2. package/lib/browser/menu/browser-menu-plugin.d.ts +42 -1
  3. package/lib/browser/menu/browser-menu-plugin.d.ts.map +1 -1
  4. package/lib/browser/menu/browser-menu-plugin.js +152 -5
  5. package/lib/browser/menu/browser-menu-plugin.js.map +1 -1
  6. package/lib/browser/shell/application-shell.js +1 -1
  7. package/lib/browser/shell/application-shell.js.map +1 -1
  8. package/lib/browser/tree/tree-widget.d.ts +28 -0
  9. package/lib/browser/tree/tree-widget.d.ts.map +1 -1
  10. package/lib/browser/tree/tree-widget.js +59 -3
  11. package/lib/browser/tree/tree-widget.js.map +1 -1
  12. package/lib/browser/tree/tree-widget.spec.js +53 -3
  13. package/lib/browser/tree/tree-widget.spec.js.map +1 -1
  14. package/lib/common/core-preferences.d.ts +1 -1
  15. package/lib/common/core-preferences.d.ts.map +1 -1
  16. package/lib/common/core-preferences.js +7 -6
  17. package/lib/common/core-preferences.js.map +1 -1
  18. package/lib/electron-browser/menu/electron-main-menu-factory.d.ts +5 -0
  19. package/lib/electron-browser/menu/electron-main-menu-factory.d.ts.map +1 -1
  20. package/lib/electron-browser/menu/electron-main-menu-factory.js +39 -8
  21. package/lib/electron-browser/menu/electron-main-menu-factory.js.map +1 -1
  22. package/lib/electron-browser/menu/electron-menu-contribution.d.ts +8 -1
  23. package/lib/electron-browser/menu/electron-menu-contribution.d.ts.map +1 -1
  24. package/lib/electron-browser/menu/electron-menu-contribution.js +50 -4
  25. package/lib/electron-browser/menu/electron-menu-contribution.js.map +1 -1
  26. package/lib/electron-browser/preload.d.ts.map +1 -1
  27. package/lib/electron-browser/preload.js +1 -0
  28. package/lib/electron-browser/preload.js.map +1 -1
  29. package/lib/electron-common/electron-api.d.ts +2 -0
  30. package/lib/electron-common/electron-api.d.ts.map +1 -1
  31. package/lib/electron-common/electron-api.js +2 -1
  32. package/lib/electron-common/electron-api.js.map +1 -1
  33. package/lib/electron-main/electron-api-main.d.ts.map +1 -1
  34. package/lib/electron-main/electron-api-main.js +15 -0
  35. package/lib/electron-main/electron-api-main.js.map +1 -1
  36. package/lib/node/backend-application-module.d.ts.map +1 -1
  37. package/lib/node/backend-application-module.js +3 -0
  38. package/lib/node/backend-application-module.js.map +1 -1
  39. package/lib/node/bundled-resource-provider.d.ts +86 -0
  40. package/lib/node/bundled-resource-provider.d.ts.map +1 -0
  41. package/lib/node/bundled-resource-provider.js +241 -0
  42. package/lib/node/bundled-resource-provider.js.map +1 -0
  43. package/lib/node/bundled-resource-provider.spec.d.ts +2 -0
  44. package/lib/node/bundled-resource-provider.spec.d.ts.map +1 -0
  45. package/lib/node/bundled-resource-provider.spec.js +160 -0
  46. package/lib/node/bundled-resource-provider.spec.js.map +1 -0
  47. package/lib/node/index.d.ts +1 -0
  48. package/lib/node/index.d.ts.map +1 -1
  49. package/lib/node/index.js +3 -1
  50. package/lib/node/index.js.map +1 -1
  51. package/package.json +4 -4
  52. package/src/browser/menu/browser-menu-plugin.ts +169 -6
  53. package/src/browser/shell/application-shell.ts +1 -1
  54. package/src/browser/tree/tree-widget.spec.tsx +59 -3
  55. package/src/browser/tree/tree-widget.tsx +64 -2
  56. package/src/common/core-preferences.ts +8 -7
  57. package/src/electron-browser/menu/electron-main-menu-factory.ts +45 -7
  58. package/src/electron-browser/menu/electron-menu-contribution.ts +50 -5
  59. package/src/electron-browser/preload.ts +3 -1
  60. package/src/electron-common/electron-api.ts +2 -0
  61. package/src/electron-main/electron-api-main.ts +16 -1
  62. package/src/node/backend-application-module.ts +4 -0
  63. package/src/node/bundled-resource-provider.spec.ts +196 -0
  64. package/src/node/bundled-resource-provider.ts +266 -0
  65. package/src/node/index.ts +1 -0
@@ -18,7 +18,7 @@ import { injectable, inject } from 'inversify';
18
18
  import { Menu, MenuBar, Menu as MenuWidget, Widget } from '@lumino/widgets';
19
19
  import { CommandRegistry as LuminoCommandRegistry } from '@lumino/commands';
20
20
  import {
21
- environment, DisposableCollection,
21
+ environment, Disposable, DisposableCollection,
22
22
  AcceleratorSource,
23
23
  ArrayUtils,
24
24
  PreferenceService
@@ -92,7 +92,7 @@ export class BrowserMainMenuFactory implements MenuWidgetFactory {
92
92
  }
93
93
 
94
94
  protected showMenuBar(menuBar: DynamicMenuBarWidget, preference = this.getMenuBarVisibility()): void {
95
- if (preference && ['classic', 'visible'].includes(preference)) {
95
+ if (preference && ['classic', 'visible', 'toggle'].includes(preference)) {
96
96
  menuBar.clearMenus();
97
97
  this.fillMenuBar(menuBar);
98
98
  } else {
@@ -432,6 +432,11 @@ export class BrowserMenuBarContribution implements FrontendApplicationContributi
432
432
  @inject(PreferenceService)
433
433
  protected readonly preferenceService: PreferenceService;
434
434
 
435
+ protected toggleModeListeners = new DisposableCollection();
436
+
437
+ protected menuWidget?: MenuBarWidget;
438
+ protected logoWidget?: Widget;
439
+
435
440
  constructor(
436
441
  @inject(BrowserMainMenuFactory) protected readonly factory: BrowserMainMenuFactory
437
442
  ) { }
@@ -449,20 +454,178 @@ export class BrowserMenuBarContribution implements FrontendApplicationContributi
449
454
  shell.addWidget(logo, { area: 'top' });
450
455
  const menu = this.factory.createMenuBar();
451
456
  shell.addWidget(menu, { area: 'top' });
452
- // Hiding the menu is only necessary in electron
453
- // In the browser we hide the whole top panel
457
+ this.menuWidget = menu;
458
+ this.logoWidget = logo;
454
459
  if (environment.electron.is()) {
455
460
  this.preferenceService.ready.then(() => {
456
- menu.setHidden(['compact', 'hidden'].includes(this.preferenceService.get('window.menuBarVisibility', '')));
461
+ this.updateElectronMenuToggleMode(menu, logo);
462
+ });
463
+ this.preferenceService.onPreferenceChanged(change => {
464
+ if (change.preferenceName === 'window.menuBarVisibility') {
465
+ this.updateElectronMenuToggleMode(menu, logo);
466
+ }
467
+ });
468
+ } else {
469
+ // In the browser, the whole top panel is hidden for compact/hidden/toggle
470
+ // (handled by ApplicationShell). For toggle mode, this installs an Alt-key
471
+ // listener that temporarily shows the top panel.
472
+ this.preferenceService.ready.then(() => {
473
+ this.updateBrowserMenuToggleMode(menu);
457
474
  });
458
475
  this.preferenceService.onPreferenceChanged(change => {
459
476
  if (change.preferenceName === 'window.menuBarVisibility') {
460
- menu.setHidden(['compact', 'hidden'].includes(this.preferenceService.get('window.menuBarVisibility', 'classic')));
477
+ this.updateBrowserMenuToggleMode(menu);
461
478
  }
462
479
  });
463
480
  }
464
481
  }
465
482
 
483
+ /**
484
+ * Manages Lumino menu bar visibility and Alt-key toggling in Electron.
485
+ *
486
+ * With native title bar, the native Electron menu bar handles toggle via
487
+ * `autoHideMenuBar`, so the Lumino menu bar is simply hidden.
488
+ *
489
+ * With custom title bar, the Lumino menu bar is the only menu bar visible
490
+ * to the user. For 'toggle' mode, an Alt-key listener is installed to
491
+ * show/hide the menu bar widget.
492
+ */
493
+ protected updateElectronMenuToggleMode(menu: MenuBarWidget, logo: Widget): void {
494
+ this.toggleModeListeners.dispose();
495
+ const pref = this.preferenceService.get<string>('window.menuBarVisibility', 'classic');
496
+ this.applyElectronMenuBarVisibility(menu, logo, pref);
497
+ if (pref === 'toggle') {
498
+ this.toggleModeListeners = this.installAltKeyToggle(menu, () => this.getElectronToggleTargets(menu, logo));
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Establishes the resting visibility of the menu bar widgets in Electron, i.e. the state
504
+ * a clean Alt press toggles away from in 'toggle' mode.
505
+ */
506
+ protected applyElectronMenuBarVisibility(menu: MenuBarWidget, logo: Widget, pref: string): void {
507
+ const shouldHide = ['compact', 'hidden', 'toggle'].includes(pref);
508
+ menu.setHidden(shouldHide);
509
+ logo.setHidden(shouldHide);
510
+ }
511
+
512
+ /**
513
+ * The widgets a clean Alt press shows and hides in 'toggle' mode in Electron.
514
+ */
515
+ protected getElectronToggleTargets(menu: MenuBarWidget, logo: Widget): Widget[] {
516
+ return [menu, logo];
517
+ }
518
+
519
+ /**
520
+ * Manages Alt-key toggle behavior for 'toggle' mode in the browser.
521
+ * The top panel's initial visibility is handled by {@link ApplicationShell.setTopPanelVisibility};
522
+ * this method only installs or removes the Alt-key listener.
523
+ */
524
+ protected updateBrowserMenuToggleMode(menu: MenuBarWidget): void {
525
+ this.toggleModeListeners.dispose();
526
+ const pref = this.preferenceService.get<string>('window.menuBarVisibility', 'classic');
527
+ if (pref === 'toggle') {
528
+ this.toggleModeListeners = this.installAltKeyToggle(menu, () => [this.shell.topPanel]);
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Installs Alt-key listeners to toggle menu bar visibility.
534
+ * A clean Alt press (press and release without other keys) shows the
535
+ * given toggle targets and focuses the first menu. Pressing Alt again
536
+ * or clicking outside hides them.
537
+ *
538
+ * @param menu the menu bar widget to focus when shown
539
+ * @param toggleTargets supplies the widgets to show/hide on Alt toggle. It is consulted on
540
+ * each gesture, because the targets can depend on state that changes while the listener is
541
+ * installed, e.g. whether the window is in full screen.
542
+ */
543
+ protected installAltKeyToggle(menu: MenuBarWidget, toggleTargets: () => Widget[]): DisposableCollection {
544
+ const disposables = new DisposableCollection();
545
+ let altKeyPressed = false;
546
+ let lastKeyWasAlt = false;
547
+
548
+ const isHidden = (): boolean => toggleTargets().some(w => w.isHidden);
549
+
550
+ const hide = (): void => {
551
+ for (const w of toggleTargets()) {
552
+ w.setHidden(true);
553
+ }
554
+ };
555
+
556
+ const show = (): void => {
557
+ for (const w of toggleTargets()) {
558
+ w.setHidden(false);
559
+ }
560
+ };
561
+
562
+ const onKeyDown = (e: KeyboardEvent): void => {
563
+ if (e.key === 'Alt') {
564
+ if (!e.repeat) {
565
+ altKeyPressed = true;
566
+ lastKeyWasAlt = true;
567
+ }
568
+ } else {
569
+ // Any other key invalidates the Alt-only gesture
570
+ lastKeyWasAlt = false;
571
+ }
572
+ };
573
+
574
+ // Mouse interaction while Alt is held is a modifier gesture of its own, e.g. Alt+click to add a
575
+ // cursor or Alt+scroll to scroll faster, so it invalidates the Alt-only gesture as any other key does.
576
+ const onPointerInteraction = (): void => {
577
+ lastKeyWasAlt = false;
578
+ };
579
+
580
+ const onKeyUp = (e: KeyboardEvent): void => {
581
+ if (e.key !== 'Alt') {
582
+ return;
583
+ }
584
+ const wasCleanAltPress = altKeyPressed && lastKeyWasAlt;
585
+ altKeyPressed = false;
586
+ lastKeyWasAlt = false;
587
+ if (!wasCleanAltPress) {
588
+ return;
589
+ }
590
+ // Keep the browser from acting on the solitary Alt press itself: Firefox, for
591
+ // instance, focuses its own menu bar, which would then toggle along with ours.
592
+ e.preventDefault();
593
+ if (isHidden()) {
594
+ show();
595
+ menu.activeIndex = 0;
596
+ menu.node.focus();
597
+ } else {
598
+ hide();
599
+ }
600
+ };
601
+
602
+ // When a menu closes and focus leaves the menu bar, hide it again
603
+ const onFocusOut = (): void => {
604
+ // Use setTimeout to allow focus to settle (e.g. focus moving between menu items)
605
+ setTimeout(() => {
606
+ if (!isHidden() && !menu.node.contains(document.activeElement)) {
607
+ hide();
608
+ }
609
+ }, 100);
610
+ };
611
+
612
+ document.addEventListener('keydown', onKeyDown, true);
613
+ document.addEventListener('keyup', onKeyUp, true);
614
+ document.addEventListener('mousedown', onPointerInteraction, true);
615
+ document.addEventListener('wheel', onPointerInteraction, { capture: true, passive: true });
616
+ menu.node.addEventListener('focusout', onFocusOut);
617
+
618
+ disposables.push(Disposable.create(() => {
619
+ document.removeEventListener('keydown', onKeyDown, true);
620
+ document.removeEventListener('keyup', onKeyUp, true);
621
+ document.removeEventListener('mousedown', onPointerInteraction, true);
622
+ document.removeEventListener('wheel', onPointerInteraction, true);
623
+ menu.node.removeEventListener('focusout', onFocusOut);
624
+ }));
625
+
626
+ return disposables;
627
+ }
628
+
466
629
  protected createLogo(): Widget {
467
630
  const logo = new Widget();
468
631
  logo.id = 'theia:icon';
@@ -422,7 +422,7 @@ export class ApplicationShell extends Widget {
422
422
  }
423
423
 
424
424
  protected setTopPanelVisibility(preference: string): void {
425
- const hiddenPreferences = ['compact', 'hidden'];
425
+ const hiddenPreferences = ['compact', 'hidden', 'toggle'];
426
426
  this.topPanel.setHidden(hiddenPreferences.includes(preference));
427
427
  }
428
428
 
@@ -38,6 +38,14 @@ class CountingView extends TreeWidget.View {
38
38
  this.scrollCount++;
39
39
  }
40
40
  };
41
+ /** Reports the list height the way Virtuoso does once it has measured the list. */
42
+ measure(height: number, renderedHeight = height): void {
43
+ this.setRenderedHeight(renderedHeight);
44
+ this.onTotalListHeightChanged(height);
45
+ }
46
+ setRenderedHeight(scrollHeight: number): void {
47
+ this.scroller = { scrollHeight } as HTMLElement;
48
+ }
41
49
  }
42
50
 
43
51
  class TestTreeWidget extends TreeWidget {
@@ -190,6 +198,8 @@ describe('TreeWidget', () => {
190
198
  let container: HTMLElement;
191
199
  let root: Root;
192
200
  let view: CountingView | undefined;
201
+ let frames: FrameRequestCallback[];
202
+ let requestAnimationFrame: typeof window.requestAnimationFrame;
193
203
  const rows: TreeWidget.NodeRow[] = [0, 1, 2].map(index => ({ index, depth: 0, node: leaf(`node-${index}`) }));
194
204
 
195
205
  const render = (props: Partial<TreeWidget.ViewProps>): void => flushSync(() => root.render(
@@ -202,20 +212,66 @@ describe('TreeWidget', () => {
202
212
  {...props}
203
213
  />
204
214
  ));
215
+ const runFrames = (): void => frames.splice(0).forEach(frame => frame(0));
216
+ /** Mount and measure the list, and serve the mount-time request. */
217
+ const mount = (props: Partial<TreeWidget.ViewProps>): void => {
218
+ render(props);
219
+ view!.measure(300);
220
+ runFrames();
221
+ };
205
222
 
206
223
  beforeEach(() => {
207
224
  container = document.createElement('div');
208
225
  document.body.appendChild(container);
209
226
  root = createRoot(container);
227
+ frames = [];
228
+ requestAnimationFrame = window.requestAnimationFrame;
229
+ window.requestAnimationFrame = frame => frames.push(frame);
210
230
  });
211
231
 
212
232
  afterEach(() => {
233
+ window.requestAnimationFrame = requestAnimationFrame;
213
234
  flushSync(() => root.unmount());
214
235
  document.body.removeChild(container);
215
236
  });
216
237
 
217
- it('does not scroll again when re-rendered without a new scroll request', () => {
238
+ it('holds a scroll request made before the list is measured until the list is rendered at its height', () => {
239
+ render({ scrollToRow: 2, scrollToRowRequestId: 1 });
240
+ expect(view!.scrollCount).to.equal(0);
241
+
242
+ view!.measure(300, 100);
243
+ runFrames();
244
+ expect(view!.scrollCount).to.equal(0);
245
+
246
+ view!.setRenderedHeight(300);
247
+ runFrames();
248
+ expect(view!.scrollCount).to.equal(1);
249
+
250
+ runFrames();
251
+ expect(view!.scrollCount).to.equal(1);
252
+ });
253
+
254
+ it('waits for the latest list height when the first reported height shrinks', () => {
255
+ render({ scrollToRow: 2, scrollToRowRequestId: 1 });
256
+ view!.measure(300, 100);
257
+ view!.measure(200, 200);
258
+ runFrames();
259
+ expect(view!.scrollCount).to.equal(1);
260
+ });
261
+
262
+ it('serves a held scroll request when the list is never rendered at its height', () => {
218
263
  render({ scrollToRow: 2, scrollToRowRequestId: 1 });
264
+ view!.measure(300, 100);
265
+ for (let frame = 0; frame < 30; frame++) {
266
+ runFrames();
267
+ expect(view!.scrollCount).to.equal(0);
268
+ }
269
+ runFrames();
270
+ expect(view!.scrollCount).to.equal(1);
271
+ });
272
+
273
+ it('does not scroll again when re-rendered without a new scroll request', () => {
274
+ mount({ scrollToRow: 2, scrollToRowRequestId: 1 });
219
275
  const scrollsAfterMount = view!.scrollCount;
220
276
 
221
277
  render({ scrollToRow: 2, scrollToRowRequestId: 1 });
@@ -225,7 +281,7 @@ describe('TreeWidget', () => {
225
281
  });
226
282
 
227
283
  it('scrolls again when a new scroll request arrives for the same row', () => {
228
- render({ scrollToRow: 2, scrollToRowRequestId: 1 });
284
+ mount({ scrollToRow: 2, scrollToRowRequestId: 1 });
229
285
  const scrollsAfterMount = view!.scrollCount;
230
286
 
231
287
  render({ scrollToRow: 2, scrollToRowRequestId: 2 });
@@ -234,7 +290,7 @@ describe('TreeWidget', () => {
234
290
  });
235
291
 
236
292
  it('scrolls when the row to scroll to changes', () => {
237
- render({ scrollToRow: 1 });
293
+ mount({ scrollToRow: 1 });
238
294
  const scrollsAfterMount = view!.scrollCount;
239
295
 
240
296
  render({ scrollToRow: 2 });
@@ -1655,13 +1655,73 @@ export namespace TreeWidget {
1655
1655
  }
1656
1656
  };
1657
1657
 
1658
+ /**
1659
+ * `true` once Virtuoso has measured the list. Before that, `scrollIntoView` sees every row as visible and does nothing.
1660
+ */
1661
+ protected measured = false;
1662
+ /**
1663
+ * `true` while a scroll request made before the list was measured waits to be served.
1664
+ */
1665
+ protected scrollHeld = false;
1666
+ /**
1667
+ * The latest list height reported by Virtuoso. It can still change after the first report, as more rows are measured.
1668
+ */
1669
+ protected listHeight = 0;
1670
+ /**
1671
+ * How many frames a held scroll request waits for the list to be rendered at its height, about half a second at 60 Hz.
1672
+ */
1673
+ protected readonly maxRenderWaitFrames = 30;
1674
+ protected scroller: HTMLElement | undefined;
1675
+
1676
+ /**
1677
+ * Scroll now, or hold the request until the list is measured, e.g. for the focus restored when the view mounts.
1678
+ */
1679
+ protected requestScroll(): void {
1680
+ if (this.measured) {
1681
+ this.scrollIntoViewIfNeeded();
1682
+ } else {
1683
+ this.scrollHeld = this.props.scrollToRow !== undefined;
1684
+ }
1685
+ }
1686
+
1687
+ protected readonly onTotalListHeightChanged = (height: number): void => {
1688
+ this.listHeight = height;
1689
+ if (!this.measured && height > 0) {
1690
+ this.measured = true;
1691
+ if (this.scrollHeld) {
1692
+ this.scrollHeld = false;
1693
+ this.scrollWhenRendered();
1694
+ }
1695
+ }
1696
+ this.props.totalListHeightChanged?.(height);
1697
+ };
1698
+
1699
+ protected readonly setScroller = (scroller: HTMLElement | Window | null): void => {
1700
+ this.scroller = scroller instanceof HTMLElement ? scroller : undefined;
1701
+ this.props.scrollerRef?.(scroller);
1702
+ };
1703
+
1704
+ /**
1705
+ * Virtuoso reports the list height before React renders the list at it, and a scroll before that is clamped
1706
+ * to the old height. Wait for the rendered height, at most `maxRenderWaitFrames` frames.
1707
+ */
1708
+ protected scrollWhenRendered(framesLeft = this.maxRenderWaitFrames): void {
1709
+ window.requestAnimationFrame(() => {
1710
+ if (this.scroller && this.scroller.scrollHeight < this.listHeight && framesLeft > 0) {
1711
+ this.scrollWhenRendered(framesLeft - 1);
1712
+ } else {
1713
+ this.scrollIntoViewIfNeeded();
1714
+ }
1715
+ });
1716
+ }
1717
+
1658
1718
  override componentDidMount(): void {
1659
- this.scrollIntoViewIfNeeded();
1719
+ this.requestScroll();
1660
1720
  }
1661
1721
 
1662
1722
  override componentDidUpdate(prevProps: ViewProps): void {
1663
1723
  if (this.props.scrollToRow !== prevProps.scrollToRow || this.props.scrollToRowRequestId !== prevProps.scrollToRowRequestId) {
1664
- this.scrollIntoViewIfNeeded();
1724
+ this.requestScroll();
1665
1725
  }
1666
1726
  }
1667
1727
 
@@ -1681,6 +1741,8 @@ export namespace TreeWidget {
1681
1741
  // Higher value provides smoother scrolling experience especially during inference, but uses more memory
1682
1742
  overscan={800}
1683
1743
  {...other}
1744
+ totalListHeightChanged={this.onTotalListHeightChanged}
1745
+ scrollerRef={this.setScroller}
1684
1746
  />;
1685
1747
  }
1686
1748
  }
@@ -92,10 +92,11 @@ export const corePreferenceSchema: PreferenceSchema = {
92
92
  },
93
93
  'window.menuBarVisibility': {
94
94
  type: 'string',
95
- enum: ['classic', 'visible', 'hidden', 'compact'],
95
+ enum: ['classic', 'visible', 'toggle', 'hidden', 'compact'],
96
96
  markdownEnumDescriptions: [
97
97
  nls.localizeByDefault('Menu is displayed at the top of the window and only hidden in full screen mode.'),
98
98
  nls.localizeByDefault('Menu is always visible at the top of the window even in full screen mode.'),
99
+ nls.localizeByDefault('Menu is hidden but can be displayed at the top of the window via the Alt key.'),
99
100
  nls.localizeByDefault('Menu is always hidden.'),
100
101
  environment.electron.is()
101
102
  // we do not support the window.menuStyle setting yet, so we do not use the default string in this case yet
@@ -152,18 +153,18 @@ export const corePreferenceSchema: PreferenceSchema = {
152
153
  'http.proxy': {
153
154
  type: 'string',
154
155
  pattern: '^https?://([^:]*(:[^@]*)?@)?([^:]+|\\[[:0-9a-fA-F]+\\])(:\\d+)?/?$|^$',
155
- markdownDescription: nls.localizeByDefault('The proxy setting to use. If not set, will be inherited from the `http_proxy` and `https_proxy` environment variables. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.'),
156
+ markdownDescription: nls.localize('theia/core/http/proxy', 'The proxy setting to use. If not set, will be inherited from the `http_proxy` and `https_proxy` environment variables.'),
156
157
  scope: PreferenceScope.User
157
158
  },
158
159
  'http.proxyStrictSSL': {
159
160
  type: 'boolean',
160
161
  default: true,
161
- description: nls.localizeByDefault('Controls whether the proxy server certificate should be verified against the list of supplied CAs. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.'),
162
+ description: nls.localize('theia/core/http/proxyStrictSSL', 'Controls whether the proxy server certificate should be verified against the list of supplied CAs.'),
162
163
  scope: PreferenceScope.User
163
164
  },
164
165
  'http.proxyAuthorization': {
165
166
  type: 'string',
166
- markdownDescription: nls.localizeByDefault('The value to send as the `Proxy-Authorization` header for every network request. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.'),
167
+ markdownDescription: nls.localize('theia/core/http/proxyAuthorization', 'The value to send as the `Proxy-Authorization` header for every network request.'),
167
168
  scope: PreferenceScope.User
168
169
  },
169
170
  'http.proxySupport': {
@@ -176,13 +177,13 @@ export const corePreferenceSchema: PreferenceSchema = {
176
177
  nls.localizeByDefault('Enable proxy support for extensions, override request options.'),
177
178
  ],
178
179
  default: 'override',
179
- description: nls.localizeByDefault('Use the proxy support for extensions. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.'),
180
+ description: nls.localize('theia/core/http/proxySupport', 'Use the proxy support for extensions.'),
180
181
  scope: PreferenceScope.User
181
182
  },
182
183
  'http.systemCertificates': {
183
184
  type: 'boolean',
184
185
  default: true,
185
- description: nls.localizeByDefault('Controls whether CA certificates should be loaded from the OS. On Windows and macOS, a reload of the window is required after turning this off. When during [remote development](https://aka.ms/vscode-remote) the {0} setting is disabled this setting can be configured in the local and the remote settings separately.'),
186
+ description: nls.localize('theia/core/http/systemCertificates', 'Controls whether CA certificates should be loaded from the OS. On Windows and macOS, a reload of the window is required after turning this off.'),
186
187
  scope: PreferenceScope.User
187
188
  },
188
189
  'workbench.list.openMode': {
@@ -314,7 +315,7 @@ export interface CoreConfiguration {
314
315
  'files.encoding': string;
315
316
  'keyboard.dispatch': 'code' | 'keyCode';
316
317
  'window.tabbar.enhancedPreview': 'classic' | 'enhanced' | 'visual';
317
- 'window.menuBarVisibility': 'classic' | 'visible' | 'hidden' | 'compact';
318
+ 'window.menuBarVisibility': 'classic' | 'visible' | 'toggle' | 'hidden' | 'compact';
318
319
  'window.title': string;
319
320
  'window.titleSeparator': string;
320
321
  'window.tabCloseIconPlacement': 'end' | 'start';
@@ -91,6 +91,12 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory {
91
91
  @inject(PreferenceService)
92
92
  protected preferencesService: PreferenceService;
93
93
 
94
+ /**
95
+ * The title bar style ('native' or 'custom'). Set by {@link ElectronMenuContribution}
96
+ * after querying the main process for the active style.
97
+ */
98
+ titleBarStyle?: string;
99
+
94
100
  setMenuBar = debounce(() => this.doSetMenuBar(), 100);
95
101
 
96
102
  @postConstruct()
@@ -140,13 +146,45 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory {
140
146
 
141
147
  doSetMenuBar(): void {
142
148
  const preference = this.preferencesService.get<string>('window.menuBarVisibility') || 'classic';
143
- const shouldShowTop = !window.electronTheiaCore.isFullScreen() || preference === 'visible';
144
- if (shouldShowTop) {
145
- this.menu = this.createElectronMenuBar();
146
- window.electronTheiaCore.setMenu(this.menu);
147
- window.electronTheiaCore.setMenuBarVisible(true);
148
- } else {
149
- window.electronTheiaCore.setMenuBarVisible(false);
149
+ const isFullScreen = window.electronTheiaCore.isFullScreen();
150
+ const isCustomTitleBar = this.titleBarStyle === 'custom';
151
+
152
+ switch (preference) {
153
+ case 'visible':
154
+ this.menu = this.createElectronMenuBar();
155
+ window.electronTheiaCore.setMenu(this.menu);
156
+ window.electronTheiaCore.setMenuBarVisible(true);
157
+ window.electronTheiaCore.setAutoHideMenuBar(false);
158
+ break;
159
+ case 'toggle':
160
+ if (isCustomTitleBar) {
161
+ // With custom title bar, the Lumino menu bar handles toggle via Alt-key listener.
162
+ // Do not set autoHideMenuBar on the native window to prevent it from appearing.
163
+ window.electronTheiaCore.setMenuBarVisible(false);
164
+ window.electronTheiaCore.setAutoHideMenuBar(false);
165
+ } else {
166
+ // With native title bar, Electron's autoHideMenuBar handles Alt toggling.
167
+ this.menu = this.createElectronMenuBar();
168
+ window.electronTheiaCore.setMenu(this.menu);
169
+ window.electronTheiaCore.setMenuBarVisible(false);
170
+ window.electronTheiaCore.setAutoHideMenuBar(true);
171
+ }
172
+ break;
173
+ case 'hidden':
174
+ window.electronTheiaCore.setMenuBarVisible(false);
175
+ window.electronTheiaCore.setAutoHideMenuBar(false);
176
+ break;
177
+ case 'compact':
178
+ window.electronTheiaCore.setMenuBarVisible(false);
179
+ window.electronTheiaCore.setAutoHideMenuBar(false);
180
+ break;
181
+ case 'classic':
182
+ default:
183
+ this.menu = this.createElectronMenuBar();
184
+ window.electronTheiaCore.setMenu(this.menu);
185
+ window.electronTheiaCore.setMenuBarVisible(!isFullScreen);
186
+ window.electronTheiaCore.setAutoHideMenuBar(isFullScreen);
187
+ break;
150
188
  }
151
189
  }
152
190
 
@@ -27,7 +27,7 @@ import { ElectronMainMenuFactory } from './electron-main-menu-factory';
27
27
  import { FrontendApplicationStateService, FrontendApplicationState } from '../../browser/frontend-application-state';
28
28
  import { FrontendApplicationConfigProvider } from '../../browser/frontend-application-config-provider';
29
29
  import { PREF_WINDOW_ZOOM_LEVEL, ZoomLevel } from '../../electron-common/electron-window-preferences';
30
- import { BrowserMenuBarContribution } from '../../browser/menu/browser-menu-plugin';
30
+ import { BrowserMenuBarContribution, MenuBarWidget } from '../../browser/menu/browser-menu-plugin';
31
31
  import { WindowService } from '../../browser/window/window-service';
32
32
  import { WindowTitleService } from '../../browser/window/window-title-service';
33
33
 
@@ -152,8 +152,9 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme
152
152
 
153
153
  handleTitleBarStyling(app: FrontendApplication): void {
154
154
  this.hideTopPanel(app);
155
- window.electronTheiaCore.getTitleBarStyleAtStartup().then(style => {
155
+ const titleBarStyleAtStartup = window.electronTheiaCore.getTitleBarStyleAtStartup().then(style => {
156
156
  this.titleBarStyle = style;
157
+ this.factory.titleBarStyle = style;
157
158
  this.setMenu(app);
158
159
  this.preferenceService.ready.then(() => {
159
160
  const current = this.preferenceService.inspect('window.titleBarStyle');
@@ -171,8 +172,17 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme
171
172
  });
172
173
  });
173
174
 
174
- this.preferenceService.ready.then(() => {
175
- window.electronTheiaCore.setMenuBarVisible(['classic', 'visible'].includes(this.preferenceService.get('window.menuBarVisibility', 'classic')));
175
+ // `titleBarStyle` decides whether the native menu bar is used at all, so wait for it:
176
+ // preferences can become ready first, in which case it would still be `undefined` here.
177
+ Promise.all([titleBarStyleAtStartup, this.preferenceService.ready]).then(() => {
178
+ const pref = this.preferenceService.get<string>('window.menuBarVisibility', 'classic');
179
+ if (pref === 'toggle' && this.titleBarStyle !== 'custom') {
180
+ window.electronTheiaCore.setMenuBarVisible(false);
181
+ window.electronTheiaCore.setAutoHideMenuBar(true);
182
+ } else {
183
+ window.electronTheiaCore.setMenuBarVisible(['classic', 'visible'].includes(pref));
184
+ window.electronTheiaCore.setAutoHideMenuBar(false);
185
+ }
176
186
  });
177
187
 
178
188
  this.preferenceService.onPreferenceChanged(change => {
@@ -424,7 +434,19 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme
424
434
  protected handleFullScreen(menuBarVisibility: string): void {
425
435
  const shouldShowTop = !window.electronTheiaCore.isFullScreen() || menuBarVisibility === 'visible';
426
436
  if (this.titleBarStyle === 'native') {
427
- window.electronTheiaCore.setMenuBarVisible(shouldShowTop);
437
+ if (menuBarVisibility === 'toggle') {
438
+ window.electronTheiaCore.setMenuBarVisible(false);
439
+ window.electronTheiaCore.setAutoHideMenuBar(true);
440
+ } else {
441
+ window.electronTheiaCore.setMenuBarVisible(shouldShowTop);
442
+ // Match `ElectronMainMenuFactory.doSetMenuBar`: in 'classic' the menu is hidden in
443
+ // full screen but Alt still reveals it. Without this the two disagree and the
444
+ // effective behaviour depends on which of them ran last.
445
+ window.electronTheiaCore.setAutoHideMenuBar(!shouldShowTop && menuBarVisibility === 'classic');
446
+ }
447
+ } else if (menuBarVisibility === 'toggle' && this.menuWidget && this.logoWidget) {
448
+ // Which widget Alt reveals depends on the full screen state, so re-establish it.
449
+ this.updateElectronMenuToggleMode(this.menuWidget, this.logoWidget);
428
450
  } else if (shouldShowTop) {
429
451
  this.shell.topPanel.show();
430
452
  } else {
@@ -432,6 +454,29 @@ export class ElectronMenuContribution extends BrowserMenuBarContribution impleme
432
454
  }
433
455
  }
434
456
 
457
+ /**
458
+ * Outside of full screen the top panel also carries the window controls, so it has to stay
459
+ * visible and Alt toggles the menu widgets inside it. In full screen the panel is hidden
460
+ * altogether, so Alt toggles the panel itself and the widgets inside it stay visible.
461
+ */
462
+ protected override applyElectronMenuBarVisibility(menu: MenuBarWidget, logo: Widget, pref: string): void {
463
+ if (pref === 'toggle' && this.titleBarStyle === 'custom') {
464
+ const isFullScreen = window.electronTheiaCore.isFullScreen();
465
+ menu.setHidden(!isFullScreen);
466
+ logo.setHidden(!isFullScreen);
467
+ this.shell.topPanel.setHidden(isFullScreen);
468
+ return;
469
+ }
470
+ super.applyElectronMenuBarVisibility(menu, logo, pref);
471
+ }
472
+
473
+ protected override getElectronToggleTargets(menu: MenuBarWidget, logo: Widget): Widget[] {
474
+ if (this.titleBarStyle === 'custom' && window.electronTheiaCore.isFullScreen()) {
475
+ return [this.shell.topPanel];
476
+ }
477
+ return super.getElectronToggleTargets(menu, logo);
478
+ }
479
+
435
480
  protected handleThemeChange(e: ThemeChangeEvent): void {
436
481
  const backgroundColor = window.getComputedStyle(document.body).backgroundColor;
437
482
  window.electronTheiaCore.setBackgroundColor(backgroundColor);
@@ -28,7 +28,8 @@ import {
28
28
  CHANNEL_KEYBOARD_LAYOUT_CHANGED, CHANNEL_IPC_CONNECTION, InternalMenuDto, CHANNEL_REQUEST_SECONDARY_CLOSE, CHANNEL_SET_BACKGROUND_COLOR,
29
29
  CHANNEL_WC_METADATA, CHANNEL_ABOUT_TO_CLOSE, CHANNEL_OPEN_WITH_SYSTEM_APP,
30
30
  CHANNEL_OPEN_URL, CHANNEL_SET_THEME, CHANNEL_OPEN_DEVTOOLS_FOR_WINDOW,
31
- CHANNEL_UPDATE_RECENT_WORKSPACES
31
+ CHANNEL_UPDATE_RECENT_WORKSPACES,
32
+ CHANNEL_SET_AUTO_HIDE_MENU_BAR
32
33
  } from '../electron-common/electron-api';
33
34
 
34
35
  // eslint-disable-next-line import/no-extraneous-dependencies
@@ -80,6 +81,7 @@ function convertMenu(menu: MenuDto[] | undefined, handlerMap: Map<number, () =>
80
81
  const api: TheiaCoreAPI = {
81
82
  WindowMetadata: { webcontentId: 'none' },
82
83
  setMenuBarVisible: (visible: boolean, windowName?: string) => ipcRenderer.send(CHANNEL_SET_MENU_BAR_VISIBLE, visible, windowName),
84
+ setAutoHideMenuBar: (enabled: boolean, windowName?: string) => ipcRenderer.send(CHANNEL_SET_AUTO_HIDE_MENU_BAR, enabled, windowName),
83
85
  setMenu: (menu: MenuDto[] | undefined) => {
84
86
  commandHandlers.delete(mainMenuId);
85
87
  const handlers = new Map<number, () => void>();