@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.
- package/lib/browser/catalog.json +256 -14
- package/lib/browser/menu/browser-menu-plugin.d.ts +42 -1
- package/lib/browser/menu/browser-menu-plugin.d.ts.map +1 -1
- package/lib/browser/menu/browser-menu-plugin.js +152 -5
- package/lib/browser/menu/browser-menu-plugin.js.map +1 -1
- package/lib/browser/shell/application-shell.js +1 -1
- package/lib/browser/shell/application-shell.js.map +1 -1
- package/lib/browser/tree/tree-widget.d.ts +28 -0
- package/lib/browser/tree/tree-widget.d.ts.map +1 -1
- package/lib/browser/tree/tree-widget.js +59 -3
- package/lib/browser/tree/tree-widget.js.map +1 -1
- package/lib/browser/tree/tree-widget.spec.js +53 -3
- package/lib/browser/tree/tree-widget.spec.js.map +1 -1
- package/lib/common/core-preferences.d.ts +1 -1
- package/lib/common/core-preferences.d.ts.map +1 -1
- package/lib/common/core-preferences.js +7 -6
- package/lib/common/core-preferences.js.map +1 -1
- package/lib/electron-browser/menu/electron-main-menu-factory.d.ts +5 -0
- package/lib/electron-browser/menu/electron-main-menu-factory.d.ts.map +1 -1
- package/lib/electron-browser/menu/electron-main-menu-factory.js +39 -8
- package/lib/electron-browser/menu/electron-main-menu-factory.js.map +1 -1
- package/lib/electron-browser/menu/electron-menu-contribution.d.ts +8 -1
- package/lib/electron-browser/menu/electron-menu-contribution.d.ts.map +1 -1
- package/lib/electron-browser/menu/electron-menu-contribution.js +50 -4
- package/lib/electron-browser/menu/electron-menu-contribution.js.map +1 -1
- package/lib/electron-browser/preload.d.ts.map +1 -1
- package/lib/electron-browser/preload.js +1 -0
- package/lib/electron-browser/preload.js.map +1 -1
- package/lib/electron-common/electron-api.d.ts +2 -0
- package/lib/electron-common/electron-api.d.ts.map +1 -1
- package/lib/electron-common/electron-api.js +2 -1
- package/lib/electron-common/electron-api.js.map +1 -1
- package/lib/electron-main/electron-api-main.d.ts.map +1 -1
- package/lib/electron-main/electron-api-main.js +15 -0
- package/lib/electron-main/electron-api-main.js.map +1 -1
- package/lib/node/backend-application-module.d.ts.map +1 -1
- package/lib/node/backend-application-module.js +3 -0
- package/lib/node/backend-application-module.js.map +1 -1
- package/lib/node/bundled-resource-provider.d.ts +86 -0
- package/lib/node/bundled-resource-provider.d.ts.map +1 -0
- package/lib/node/bundled-resource-provider.js +241 -0
- package/lib/node/bundled-resource-provider.js.map +1 -0
- package/lib/node/bundled-resource-provider.spec.d.ts +2 -0
- package/lib/node/bundled-resource-provider.spec.d.ts.map +1 -0
- package/lib/node/bundled-resource-provider.spec.js +160 -0
- package/lib/node/bundled-resource-provider.spec.js.map +1 -0
- package/lib/node/index.d.ts +1 -0
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +3 -1
- package/lib/node/index.js.map +1 -1
- package/package.json +4 -4
- package/src/browser/menu/browser-menu-plugin.ts +169 -6
- package/src/browser/shell/application-shell.ts +1 -1
- package/src/browser/tree/tree-widget.spec.tsx +59 -3
- package/src/browser/tree/tree-widget.tsx +64 -2
- package/src/common/core-preferences.ts +8 -7
- package/src/electron-browser/menu/electron-main-menu-factory.ts +45 -7
- package/src/electron-browser/menu/electron-menu-contribution.ts +50 -5
- package/src/electron-browser/preload.ts +3 -1
- package/src/electron-common/electron-api.ts +2 -0
- package/src/electron-main/electron-api-main.ts +16 -1
- package/src/node/backend-application-module.ts +4 -0
- package/src/node/bundled-resource-provider.spec.ts +196 -0
- package/src/node/bundled-resource-provider.ts +266 -0
- 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
|
-
|
|
453
|
-
|
|
457
|
+
this.menuWidget = menu;
|
|
458
|
+
this.logoWidget = logo;
|
|
454
459
|
if (environment.electron.is()) {
|
|
455
460
|
this.preferenceService.ready.then(() => {
|
|
456
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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>();
|