Skip to content

Commit 95d4069

Browse files
authored
Clean up handling of "toggle maximized". (#15547)
Fixes #15462, #15525 Contributed on behalf of STMicroelectronics Signed-off-by: Thomas Mäder <t.s.maeder@gmail.com>
1 parent fb77fd7 commit 95d4069

File tree

9 files changed

+124
-154
lines changed

9 files changed

+124
-154
lines changed

packages/core/src/browser/frontend-application-module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is
205205
return new TabBarRenderer(contextMenuRenderer, tabBarDecoratorService, iconThemeService,
206206
selectionService, commandService, corePreferences, hoverService, contextKeyService);
207207
});
208-
bind(TheiaDockPanel.Factory).toFactory(({ container }) => (options?: DockPanel.IOptions) => {
208+
bind(TheiaDockPanel.Factory).toFactory(({ container }) => (options?: DockPanel.IOptions, maximizeCallback?: (area: TheiaDockPanel) => void) => {
209209
const corePreferences = container.get<CorePreferences>(CorePreferences);
210210
return new TheiaDockPanel(options, corePreferences);
211211
});

packages/core/src/browser/shell/application-shell.ts

Lines changed: 88 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { FrontendApplicationStateService } from '../frontend-application-state';
3434
import { TabBarToolbarRegistry, TabBarToolbarFactory } from './tab-bar-toolbar';
3535
import { ContextKeyService } from '../context-key-service';
3636
import { Emitter } from '../../common/event';
37-
import { waitForRevealed, waitForClosed, PINNED_CLASS } from '../widgets';
37+
import { waitForRevealed, waitForClosed, PINNED_CLASS, UnsafeWidgetUtilities } from '../widgets';
3838
import { CorePreferences } from '../core-preferences';
3939
import { BreadcrumbsRendererFactory } from '../breadcrumbs/breadcrumbs-renderer';
4040
import { Deferred } from '../../common/promise-util';
@@ -85,10 +85,6 @@ export interface DockPanelRendererFactory {
8585
*/
8686
@injectable()
8787
export class DockPanelRenderer implements DockLayout.IRenderer {
88-
89-
@inject(TheiaDockPanel.Factory)
90-
protected readonly dockPanelFactory: TheiaDockPanel.Factory;
91-
9288
readonly tabBarClasses: string[] = [];
9389

9490
private readonly onDidCreateTabBarEmitter = new Emitter<TabBar<Widget>>();
@@ -175,6 +171,7 @@ interface WidgetDragState {
175171
leaveTimeout?: number;
176172
}
177173

174+
export const MAXIMIZED_CLASS = 'theia-maximized';
178175
/**
179176
* The application shell manages the top-level widgets of the application. Use this class to
180177
* add, remove, or activate a widget.
@@ -269,6 +266,8 @@ export class ApplicationShell extends Widget {
269266
protected initializedDeferred = new Deferred<void>();
270267
initialized = this.initializedDeferred.promise;
271268

269+
protected readonly maximizedElement: HTMLElement;
270+
272271
/**
273272
* Construct a new application shell.
274273
*/
@@ -286,6 +285,16 @@ export class ApplicationShell extends Widget {
286285
) {
287286
super(options as Widget.IOptions);
288287

288+
this.maximizedElement = this.node.ownerDocument.createElement('div');
289+
this.maximizedElement.style.position = 'fixed';
290+
this.maximizedElement.style.display = 'none';
291+
this.maximizedElement.style.left = '0px';
292+
this.maximizedElement.style.bottom = '0px';
293+
this.maximizedElement.style.right = '0px';
294+
this.maximizedElement.style.background = 'var(--theia-editor-background)';
295+
this.maximizedElement.style.zIndex = '2000';
296+
this.node.ownerDocument.body.appendChild(this.maximizedElement);
297+
289298
// Merge the user-defined application options with the default options
290299
this.options = {
291300
bottomPanel: {
@@ -301,6 +310,13 @@ export class ApplicationShell extends Widget {
301310
...options?.rightPanel || {}
302311
}
303312
};
313+
if (corePreferences) {
314+
corePreferences.onPreferenceChanged(preference => {
315+
if (preference.preferenceName === 'window.menuBarVisibility' && (preference.newValue === 'visible' || preference.oldValue === 'visible')) {
316+
this.handleMenuBarVisibility(preference.newValue);
317+
}
318+
});
319+
}
304320
}
305321

306322
@postConstruct()
@@ -561,7 +577,7 @@ export class ApplicationShell extends Widget {
561577
mode: 'multiple-document',
562578
renderer,
563579
spacing: 0
564-
});
580+
}, area => this.doToggleMaximized(area));
565581
dockPanel.id = MAIN_AREA_ID;
566582
dockPanel.widgetAdded.connect((_, widget) => this.fireDidAddWidget(widget));
567583
dockPanel.widgetRemoved.connect((_, widget) => this.fireDidRemoveWidget(widget));
@@ -658,7 +674,7 @@ export class ApplicationShell extends Widget {
658674
mode: 'multiple-document',
659675
renderer,
660676
spacing: 0
661-
});
677+
}, area => this.doToggleMaximized(area));
662678
dockPanel.id = BOTTOM_AREA_ID;
663679
dockPanel.widgetAdded.connect((sender, widget) => {
664680
this.refreshBottomPanelToggleButton();
@@ -1178,9 +1194,6 @@ export class ApplicationShell extends Widget {
11781194
w.title.className = w.title.className.replace(' theia-mod-active', '');
11791195
w = w.parent;
11801196
}
1181-
// Reset the z-index to the default
1182-
// eslint-disable-next-line no-null/no-null
1183-
this.setZIndex(oldValue.node, null);
11841197
}
11851198
if (newValue) {
11861199
let w: Widget | null = newValue;
@@ -1203,11 +1216,6 @@ export class ApplicationShell extends Widget {
12031216
// if widget was undefined, we wouldn't have gotten a panel back before
12041217
panel.markAsCurrent(widget!.title);
12051218
}
1206-
// Add checks to ensure that the 'sash' for left panel is displayed correctly
1207-
if (newValue.node.className === 'lm-Widget theia-view-container lm-DockPanel-widget') {
1208-
// Set the z-index so elements with `position: fixed` contained in the active widget are displayed correctly
1209-
this.setZIndex(newValue.node, '1');
1210-
}
12111219

12121220
// activate another widget if an active widget will be closed
12131221
const onCloseRequest = newValue['onCloseRequest'];
@@ -1237,17 +1245,6 @@ export class ApplicationShell extends Widget {
12371245
this.onDidChangeActiveWidgetEmitter.fire(args);
12381246
}
12391247

1240-
/**
1241-
* Set the z-index of the given element and its ancestors to the value `z`.
1242-
*/
1243-
private setZIndex(element: HTMLElement, z: string | null): void {
1244-
element.style.zIndex = z || '';
1245-
const parent = element.parentElement;
1246-
if (parent && parent !== this.node) {
1247-
this.setZIndex(parent, z);
1248-
}
1249-
}
1250-
12511248
/**
12521249
* Track the given widget so it is considered in the `current` and `active` state of the shell.
12531250
*/
@@ -2118,11 +2115,75 @@ export class ApplicationShell extends Widget {
21182115
toggleMaximized(widget: Widget | undefined = this.currentWidget): void {
21192116
const area = widget && this.getAreaPanelFor(widget);
21202117
if (area instanceof TheiaDockPanel && (area === this.mainPanel || area === this.bottomPanel)) {
2121-
area.toggleMaximized();
2118+
this.doToggleMaximized(area);
21222119
this.revealWidget(widget!.id);
21232120
}
21242121
}
21252122

2123+
protected handleMenuBarVisibility(newValue: string): void {
2124+
if (newValue === 'visible') {
2125+
const topRect = this.topPanel.node.getBoundingClientRect();
2126+
this.maximizedElement.style.top = `${topRect.bottom}px`;
2127+
} else {
2128+
this.maximizedElement.style.removeProperty('top');
2129+
}
2130+
}
2131+
2132+
protected readonly onDidToggleMaximizedEmitter = new Emitter<Widget>();
2133+
readonly onDidToggleMaximized = this.onDidToggleMaximizedEmitter.event;
2134+
2135+
protected unmaximize: (() => void) | undefined;
2136+
doToggleMaximized(area: TheiaDockPanel): void {
2137+
if (this.unmaximize) {
2138+
this.unmaximize();
2139+
this.unmaximize = undefined;
2140+
return;
2141+
}
2142+
2143+
const removedListener = () => {
2144+
if (!area.widgets().next().value) {
2145+
this.doToggleMaximized(area);
2146+
}
2147+
};
2148+
2149+
const parent = area.parent as SplitPanel;
2150+
const layout = area.parent?.layout as SplitLayout;
2151+
const sizes = layout.relativeSizes().slice();
2152+
const stretch = SplitPanel.getStretch(area);
2153+
const index = parent.widgets.indexOf(area);
2154+
parent.layout?.removeWidget(area);
2155+
2156+
// eslint-disable-next-line no-null/no-null
2157+
this.maximizedElement.style.display = 'block';
2158+
area.addClass(MAXIMIZED_CLASS);
2159+
const topRect = this.topPanel.node.getBoundingClientRect();
2160+
UnsafeWidgetUtilities.attach(area, this.maximizedElement);
2161+
this.maximizedElement.style.top = `${topRect.bottom}px`;
2162+
area.fit();
2163+
const observer = new ResizeObserver(entries => {
2164+
area.fit();
2165+
});
2166+
observer.observe(this.maximizedElement);
2167+
2168+
this.unmaximize = () => {
2169+
observer.unobserve(this.maximizedElement);
2170+
observer.disconnect();
2171+
this.maximizedElement.style.display = 'none';
2172+
area.removeClass(MAXIMIZED_CLASS);
2173+
if (area.isAttached) {
2174+
UnsafeWidgetUtilities.detach(area);
2175+
}
2176+
parent?.insertWidget(index, area);
2177+
SplitPanel.setStretch(area, stretch);
2178+
layout.setRelativeSizes(sizes);
2179+
parent.fit();
2180+
this.onDidToggleMaximizedEmitter.fire(area);
2181+
area.widgetRemoved.disconnect(removedListener);
2182+
};
2183+
2184+
area.widgetRemoved.connect(removedListener);
2185+
this.onDidToggleMaximizedEmitter.fire(area);
2186+
}
21262187
}
21272188

21282189
/**

packages/core/src/browser/shell/tab-bars.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,7 @@ export class ToolbarAwareTabBar extends ScrollableTabBar {
958958
this.node.appendChild(this.breadcrumbsContainer);
959959

960960
this.toolbar = this.tabBarToolbarFactory();
961+
this.toDispose.push(this.toolbar);
961962
this.toDispose.push(this.tabBarToolbarRegistry.onDidChange(() => this.update()));
962963
this.toDispose.push(this.breadcrumbsRenderer);
963964

@@ -1009,7 +1010,7 @@ export class ToolbarAwareTabBar extends ScrollableTabBar {
10091010

10101011
protected override onBeforeDetach(msg: Message): void {
10111012
if (this.toolbar && this.toolbar.isAttached) {
1012-
this.toolbar.dispose();
1013+
Widget.detach(this.toolbar);
10131014
}
10141015
super.onBeforeDetach(msg);
10151016
}

packages/core/src/browser/shell/theia-dock-panel.ts

Lines changed: 10 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,14 @@
1515
// *****************************************************************************
1616

1717
import { find, toArray } from '@lumino/algorithm';
18-
import { TabBar, Widget, DockPanel, Title, DockLayout } from '@lumino/widgets';
18+
import { TabBar, Widget, DockPanel, Title } from '@lumino/widgets';
1919
import { Signal } from '@lumino/signaling';
2020
import { Disposable, DisposableCollection } from '../../common/disposable';
21-
import { UnsafeWidgetUtilities } from '../widgets';
2221
import { CorePreferences } from '../core-preferences';
2322
import { Emitter, Event, environment } from '../../common';
2423
import { ToolbarAwareTabBar } from './tab-bars';
2524

26-
export const MAXIMIZED_CLASS = 'theia-maximized';
2725
export const ACTIVE_TABBAR_CLASS = 'theia-tabBar-active';
28-
const VISIBLE_MENU_MAXIMIZED_CLASS = 'theia-visible-menu-maximized';
2926

3027
export const MAIN_AREA_ID = 'theia-main-content-panel';
3128
export const BOTTOM_AREA_ID = 'theia-bottom-content-panel';
@@ -49,15 +46,15 @@ export class TheiaDockPanel extends DockPanel {
4946
*/
5047
readonly widgetRemoved = new Signal<this, Widget>(this);
5148

52-
protected readonly onDidToggleMaximizedEmitter = new Emitter<Widget>();
53-
readonly onDidToggleMaximized = this.onDidToggleMaximizedEmitter.event;
5449
protected readonly onDidChangeCurrentEmitter = new Emitter<Title<Widget> | undefined>();
50+
5551
get onDidChangeCurrent(): Event<Title<Widget> | undefined> {
5652
return this.onDidChangeCurrentEmitter.event;
5753
}
5854

5955
constructor(options?: DockPanel.IOptions,
60-
protected readonly preferences?: CorePreferences
56+
protected readonly preferences?: CorePreferences,
57+
protected readonly maximizeCallback?: (area: TheiaDockPanel) => void
6158
) {
6259
super(options);
6360
this['_onCurrentChanged'] = (sender: TabBar<Widget>, args: TabBar.ICurrentChangedArgs<Widget>) => {
@@ -77,32 +74,18 @@ export class TheiaDockPanel extends DockPanel {
7774
this.markAsCurrent(args.title);
7875
super['_onTabActivateRequested'](sender, args);
7976
};
80-
if (preferences) {
81-
preferences.onPreferenceChanged(preference => {
82-
if (!this.isElectron() && preference.preferenceName === 'window.menuBarVisibility' && (preference.newValue === 'visible' || preference.oldValue === 'visible')) {
83-
this.handleMenuBarVisibility(preference.newValue);
84-
}
85-
});
77+
}
78+
79+
toggleMaximized(): void {
80+
if (this.maximizeCallback) {
81+
this.maximizeCallback(this);
8682
}
8783
}
8884

8985
isElectron(): boolean {
9086
return environment.electron.is();
9187
}
9288

93-
protected handleMenuBarVisibility(newValue: string): void {
94-
const areaContainer = this.node.parentElement;
95-
const maximizedElement = this.getMaximizedElement();
96-
97-
if (areaContainer === maximizedElement) {
98-
if (newValue === 'visible') {
99-
this.addClass(VISIBLE_MENU_MAXIMIZED_CLASS);
100-
} else {
101-
this.removeClass(VISIBLE_MENU_MAXIMIZED_CLASS);
102-
}
103-
}
104-
}
105-
10689
protected _currentTitle: Title<Widget> | undefined;
10790
get currentTitle(): Title<Widget> | undefined {
10891
return this._currentTitle;
@@ -195,75 +178,11 @@ export class TheiaDockPanel extends DockPanel {
195178
}
196179
return undefined;
197180
}
198-
199-
protected readonly toDisposeOnToggleMaximized = new DisposableCollection();
200-
toggleMaximized(): void {
201-
const areaContainer = this.node.parentElement;
202-
if (!areaContainer) {
203-
return;
204-
}
205-
const maximizedElement = this.getMaximizedElement();
206-
if (areaContainer === maximizedElement) {
207-
this.toDisposeOnToggleMaximized.dispose();
208-
return;
209-
}
210-
if (this.isAttached) {
211-
UnsafeWidgetUtilities.detach(this);
212-
}
213-
maximizedElement.style.display = 'block';
214-
this.addClass(MAXIMIZED_CLASS);
215-
const preference = this.preferences?.get('window.menuBarVisibility');
216-
if (!this.isElectron() && preference === 'visible') {
217-
this.addClass(VISIBLE_MENU_MAXIMIZED_CLASS);
218-
}
219-
UnsafeWidgetUtilities.attach(this, maximizedElement);
220-
this.fit();
221-
this.onDidToggleMaximizedEmitter.fire(this);
222-
this.toDisposeOnToggleMaximized.push(Disposable.create(() => {
223-
maximizedElement.style.display = 'none';
224-
this.removeClass(MAXIMIZED_CLASS);
225-
this.onDidToggleMaximizedEmitter.fire(this);
226-
if (!this.isElectron()) {
227-
this.removeClass(VISIBLE_MENU_MAXIMIZED_CLASS);
228-
}
229-
if (this.isAttached) {
230-
UnsafeWidgetUtilities.detach(this);
231-
}
232-
UnsafeWidgetUtilities.attach(this, areaContainer);
233-
this.fit();
234-
}));
235-
236-
const layout = this.layout;
237-
if (layout instanceof DockLayout) {
238-
const onResize = layout['onResize'];
239-
layout['onResize'] = () => onResize.bind(layout)(Widget.ResizeMessage.UnknownSize);
240-
this.toDisposeOnToggleMaximized.push(Disposable.create(() => layout['onResize'] = onResize));
241-
}
242-
243-
const removedListener = () => {
244-
if (!this.widgets().next()) {
245-
this.toDisposeOnToggleMaximized.dispose();
246-
}
247-
};
248-
this.widgetRemoved.connect(removedListener);
249-
this.toDisposeOnToggleMaximized.push(Disposable.create(() => this.widgetRemoved.disconnect(removedListener)));
250-
}
251-
252-
protected maximizedElement: HTMLElement | undefined;
253-
protected getMaximizedElement(): HTMLElement {
254-
if (!this.maximizedElement) {
255-
this.maximizedElement = document.createElement('div');
256-
this.maximizedElement.style.display = 'none';
257-
document.body.appendChild(this.maximizedElement);
258-
}
259-
return this.maximizedElement;
260-
}
261-
262181
}
263182
export namespace TheiaDockPanel {
264183
export const Factory = Symbol('TheiaDockPanel#Factory');
265184
export interface Factory {
266-
(options?: DockPanel.IOptions): TheiaDockPanel;
185+
(options?: DockPanel.IOptions, maximizeCallback?: (area: TheiaDockPanel) => void): TheiaDockPanel;
267186
}
268187

269188
export interface AddOptions extends DockPanel.IAddOptions {

0 commit comments

Comments
 (0)