UNPKG

chrome-devtools-frontend

Version:
958 lines (828 loc) • 33.8 kB
// Copyright 2012 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* eslint-disable @devtools/no-imperative-dom-api */ import * as Common from '../../core/common/common.js'; import * as Platform from '../../core/platform/platform.js'; import * as Geometry from '../../models/geometry/geometry.js'; import * as VisualLogging from '../visual_logging/visual_logging.js'; import * as ARIAUtils from './ARIAUtils.js'; import {Events as ResizerWidgetEvents, type ResizeUpdatePositionEvent, SimpleResizerWidget} from './ResizerWidget.js'; import splitWidgetStyles from './splitWidget.css.js'; import {ToolbarButton} from './Toolbar.js'; import {Widget, WidgetElement} from './Widget.js'; import {Events as ZoomManagerEvents, ZoomManager} from './ZoomManager.js'; export class SplitWidget extends Common.ObjectWrapper.eventMixin<EventTypes, typeof Widget>(Widget) { #sidebarElement: HTMLElement; #mainElement: HTMLElement; #resizerElement: HTMLElement; #resizerElementSize: number|null = null; readonly #resizerWidget: SimpleResizerWidget; #defaultSidebarWidth: number; #defaultSidebarHeight: number; readonly #constraintsInDip: boolean; #resizeStartSizeDIP = 0; // TODO: Used in WebTests private setting: Common.Settings.Setting<{ vertical?: SettingForOrientation, horizontal?: SettingForOrientation, }>|null; #totalSizeCSS = 0; #totalSizeOtherDimensionCSS = 0; #mainWidget: Widget|null = null; #sidebarWidget: Widget|null = null; #animationFrameHandle = 0; #animationCallback: (() => void)|null = null; #showSidebarButtonTitle: Common.UIString.LocalizedString = Common.UIString.LocalizedEmptyString; #hideSidebarButtonTitle: Common.UIString.LocalizedString = Common.UIString.LocalizedEmptyString; #shownSidebarString: Common.UIString.LocalizedString = Common.UIString.LocalizedEmptyString; #hiddenSidebarString: Common.UIString.LocalizedString = Common.UIString.LocalizedEmptyString; #showHideSidebarButton: ToolbarButton|null = null; #isVertical = false; #sidebarMinimized = false; #detaching = false; #sidebarSizeDIP = -1; #savedSidebarSizeDIP: number; #secondIsSidebar = false; #shouldSaveShowMode = false; #savedVerticalMainSize: number|null = null; #savedHorizontalMainSize: number|null = null; #showMode: ShowMode = ShowMode.BOTH; #savedShowMode: ShowMode; #autoAdjustOrientation = false; constructor( isVertical: boolean, secondIsSidebar: boolean, settingName?: string, defaultSidebarWidth?: number, defaultSidebarHeight?: number, constraintsInDip?: boolean, element?: SplitWidgetElement, ) { super(element, {useShadowDom: true}); this.element.classList.add('split-widget'); this.registerRequiredCSS(splitWidgetStyles); this.contentElement.classList.add('shadow-split-widget'); this.#sidebarElement = this.contentElement.createChild('div', 'shadow-split-widget-contents shadow-split-widget-sidebar vbox'); this.#mainElement = this.contentElement.createChild('div', 'shadow-split-widget-contents shadow-split-widget-main vbox'); const mainSlot = this.#mainElement.createChild('slot'); mainSlot.name = 'main'; mainSlot.addEventListener('slotchange', (_: Event) => { const assignedNode = mainSlot.assignedNodes()[0]; const widget = assignedNode instanceof HTMLElement ? Widget.getOrCreateWidget(assignedNode) : null; if (widget && widget !== this.#mainWidget) { this.setMainWidget(widget); } }); const sidebarSlot = this.#sidebarElement.createChild('slot'); sidebarSlot.name = 'sidebar'; sidebarSlot.addEventListener('slotchange', (_: Event) => { const assignedNode = sidebarSlot.assignedNodes()[0]; const widget = assignedNode instanceof HTMLElement ? Widget.getOrCreateWidget(assignedNode) : null; if (widget && widget !== this.#sidebarWidget) { this.setSidebarWidget(widget); } }); this.#resizerElement = this.contentElement.createChild('div', 'shadow-split-widget-resizer'); this.#resizerWidget = new SimpleResizerWidget(); this.#resizerWidget.setEnabled(true); this.#resizerWidget.addEventListener(ResizerWidgetEvents.RESIZE_START, this.#onResizeStart, this); this.#resizerWidget.addEventListener(ResizerWidgetEvents.RESIZE_UPDATE_POSITION, this.#onResizeUpdate, this); this.#resizerWidget.addEventListener(ResizerWidgetEvents.RESIZE_END, this.#onResizeEnd, this); this.#defaultSidebarWidth = defaultSidebarWidth || 200; this.#defaultSidebarHeight = defaultSidebarHeight || this.#defaultSidebarWidth; this.#constraintsInDip = Boolean(constraintsInDip); this.setting = settingName ? Common.Settings.Settings.instance().createSetting(settingName, {}) : null; this.#savedSidebarSizeDIP = this.#sidebarSizeDIP; this.setSecondIsSidebar(secondIsSidebar); this.#setVertical(isVertical); this.#savedShowMode = this.#showMode; // Should be called after isVertical has the right value. this.installResizer(this.#resizerElement); } isVertical(): boolean { return this.#isVertical; } setVertical(isVertical: boolean): void { if (this.#isVertical === isVertical) { return; } this.#setVertical(isVertical); if (this.isShowing()) { this.#updateLayout(); } } setAutoAdjustOrientation(autoAdjustOrientation: boolean): void { this.#autoAdjustOrientation = autoAdjustOrientation; this.#maybeAutoAdjustOrientation(); } #setVertical(isVertical: boolean): void { this.contentElement.classList.toggle('vbox', !isVertical); this.contentElement.classList.toggle('hbox', isVertical); this.#isVertical = isVertical; this.#resizerElementSize = null; this.#sidebarSizeDIP = -1; this.#restoreSidebarSizeFromSettings(); if (this.#shouldSaveShowMode) { this.#restoreAndApplyShowModeFromSettings(); } this.#updateShowHideSidebarButton(); // FIXME: reverse SplitWidget.isVertical meaning. this.#resizerWidget.setVertical(!isVertical); this.invalidateConstraints(); } #updateLayout(animate?: boolean): void { this.#totalSizeCSS = 0; // Lazy update. this.#totalSizeOtherDimensionCSS = 0; // Remove properties that might affect total size calculation. this.#mainElement.style.removeProperty('width'); this.#mainElement.style.removeProperty('height'); this.#sidebarElement.style.removeProperty('width'); this.#sidebarElement.style.removeProperty('height'); this.#setSidebarSizeDIP(this.#preferredSidebarSizeDIP(), Boolean(animate)); } setMainWidget(widget: Widget): void { if (this.#mainWidget === widget) { return; } this.suspendInvalidations(); if (this.#mainWidget) { this.#mainWidget.detach(); } this.#mainWidget = widget; if (widget) { widget.element.slot = 'main'; if (this.#showMode === ShowMode.ONLY_MAIN || this.#showMode === ShowMode.BOTH) { widget.show(this.element); } } this.resumeInvalidations(); } setSidebarWidget(widget: Widget): void { if (this.#sidebarWidget === widget) { return; } this.suspendInvalidations(); if (this.#sidebarWidget) { this.#sidebarWidget.detach(); } this.#sidebarWidget = widget; if (widget) { widget.element.slot = 'sidebar'; if (this.#showMode === ShowMode.ONLY_SIDEBAR || this.#showMode === ShowMode.BOTH) { widget.show(this.element); } } this.resumeInvalidations(); } mainWidget(): Widget|null { return this.#mainWidget; } sidebarWidget(): Widget|null { return this.#sidebarWidget; } sidebarElement(): HTMLElement { return this.#sidebarElement; } override childWasDetached(widget: Widget): void { if (this.#detaching) { return; } if (this.#mainWidget === widget) { this.#mainWidget = null; } if (this.#sidebarWidget === widget) { this.#sidebarWidget = null; } this.invalidateConstraints(); } isSidebarSecond(): boolean { return this.#secondIsSidebar; } enableShowModeSaving(): void { this.#shouldSaveShowMode = true; this.#restoreAndApplyShowModeFromSettings(); } showMode(): string { return this.#showMode; } sidebarIsShowing(): boolean { return this.#showMode !== ShowMode.ONLY_MAIN; } setSecondIsSidebar(secondIsSidebar: boolean): void { if (secondIsSidebar === this.#secondIsSidebar) { return; } this.#secondIsSidebar = secondIsSidebar; if (!this.#mainWidget?.shouldHideOnDetach()) { if (secondIsSidebar) { this.contentElement.insertBefore(this.#mainElement, this.#sidebarElement); } else { this.contentElement.insertBefore(this.#mainElement, this.#resizerElement); } } else if (!this.#sidebarWidget?.shouldHideOnDetach()) { if (secondIsSidebar) { this.contentElement.insertBefore(this.#sidebarElement, this.#resizerElement); } else { this.contentElement.insertBefore(this.#sidebarElement, this.#mainElement); } } else { console.error('Could not swap split widget side. Both children widgets contain iframes.'); this.#secondIsSidebar = !secondIsSidebar; } } resizerElement(): Element { return this.#resizerElement; } hideMain(animate?: boolean): void { this.#showOnly(this.#sidebarWidget, this.#mainWidget, this.#sidebarElement, this.#mainElement, animate); this.#updateShowMode(ShowMode.ONLY_SIDEBAR); } hideSidebar(animate?: boolean): void { this.#showOnly(this.#mainWidget, this.#sidebarWidget, this.#mainElement, this.#sidebarElement, animate); this.#updateShowMode(ShowMode.ONLY_MAIN); } setSidebarMinimized(minimized: boolean): void { this.#sidebarMinimized = minimized; this.invalidateConstraints(); } isSidebarMinimized(): boolean { return this.#sidebarMinimized; } #showOnly( sideToShow: Widget|null, sideToHide: Widget|null, shadowToShow: Element, shadowToHide: Element, animate?: boolean, ): void { this.#cancelAnimation(); function callback(this: SplitWidget): void { if (sideToShow) { // Make sure main is first in the children list. if (sideToShow === this.#mainWidget) { this.#mainWidget.show(this.element, this.#sidebarWidget ? this.#sidebarWidget.element : null); } else if (this.#sidebarWidget) { this.#sidebarWidget.show(this.element); } } if (sideToHide) { this.#detaching = true; sideToHide.detach(); this.#detaching = false; } this.#resizerElement.classList.add('hidden'); shadowToShow.classList.remove('hidden'); shadowToShow.classList.add('maximized'); shadowToHide.classList.add('hidden'); shadowToHide.classList.remove('maximized'); this.#removeAllLayoutProperties(); this.doResize(); this.showFinishedForTest(); } if (animate) { this.#animate(true, callback.bind(this)); } else { callback.call(this); } this.#sidebarSizeDIP = -1; this.setResizable(false); } protected showFinishedForTest(): void { // This method is sniffed in tests. } #removeAllLayoutProperties(): void { this.#sidebarElement.style.removeProperty('flexBasis'); this.#mainElement.style.removeProperty('width'); this.#mainElement.style.removeProperty('height'); this.#sidebarElement.style.removeProperty('width'); this.#sidebarElement.style.removeProperty('height'); this.#resizerElement.style.removeProperty('left'); this.#resizerElement.style.removeProperty('right'); this.#resizerElement.style.removeProperty('top'); this.#resizerElement.style.removeProperty('bottom'); this.#resizerElement.style.removeProperty('margin-left'); this.#resizerElement.style.removeProperty('margin-right'); this.#resizerElement.style.removeProperty('margin-top'); this.#resizerElement.style.removeProperty('margin-bottom'); } showBoth(animate?: boolean): void { if (this.#showMode === ShowMode.BOTH) { animate = false; } this.#cancelAnimation(); this.#mainElement.classList.remove('maximized', 'hidden'); this.#sidebarElement.classList.remove('maximized', 'hidden'); this.#resizerElement.classList.remove('hidden'); this.setResizable(true); // Make sure main is the first in the children list. this.suspendInvalidations(); if (this.#sidebarWidget) { this.#sidebarWidget.show(this.element); } if (this.#mainWidget) { this.#mainWidget.show(this.element, this.#sidebarWidget ? this.#sidebarWidget.element : null); } this.resumeInvalidations(); // Order widgets in DOM properly. this.setSecondIsSidebar(this.#secondIsSidebar); this.#sidebarSizeDIP = -1; this.#updateShowMode(ShowMode.BOTH); this.#updateLayout(animate); } setResizable(resizable: boolean): void { this.#resizerWidget.setEnabled(resizable); } // Currently unused forceSetSidebarWidth(width: number): void { this.#defaultSidebarWidth = width; this.#savedSidebarSizeDIP = width; this.#updateLayout(); } isResizable(): boolean { return this.#resizerWidget.isEnabled(); } setSidebarSize(size: number): void { const sizeDIP = ZoomManager.instance().cssToDIP(size); this.#savedSidebarSizeDIP = sizeDIP; this.#saveSetting(); this.#setSidebarSizeDIP(sizeDIP, false, true); } sidebarSize(): number { const sizeDIP = Math.max(0, this.#sidebarSizeDIP); return ZoomManager.instance().dipToCSS(sizeDIP); } totalSize(): number { const sizeDIP = Math.max(0, this.#totalSizeDIP()); return ZoomManager.instance().dipToCSS(sizeDIP); } /** * Returns total size in DIP. */ #totalSizeDIP(): number { if (!this.#totalSizeCSS) { this.#totalSizeCSS = this.#isVertical ? this.contentElement.offsetWidth : this.contentElement.offsetHeight; this.#totalSizeOtherDimensionCSS = this.#isVertical ? this.contentElement.offsetHeight : this.contentElement.offsetWidth; } return ZoomManager.instance().cssToDIP(this.#totalSizeCSS); } #updateShowMode(showMode: ShowMode): void { this.#showMode = showMode; this.#saveShowModeToSettings(); this.#updateShowHideSidebarButton(); this.dispatchEventToListeners(Events.SHOW_MODE_CHANGED, showMode); this.invalidateConstraints(); } #setSidebarSizeDIP(sizeDIP: number, animate: boolean, userAction?: boolean): void { if (this.#showMode !== ShowMode.BOTH || !this.isShowing()) { return; } sizeDIP = this.#applyConstraints(sizeDIP, userAction); if (this.#sidebarSizeDIP === sizeDIP) { return; } if (!this.#resizerElementSize) { this.#resizerElementSize = this.#isVertical ? this.#resizerElement.offsetWidth : this.#resizerElement.offsetHeight; } // Invalidate layout below. this.#removeAllLayoutProperties(); // this.#totalSizeDIP is available below since we successfully applied constraints. const roundSizeCSS = Math.round(ZoomManager.instance().dipToCSS(sizeDIP)); const sidebarSizeValue = roundSizeCSS + 'px'; const mainSizeValue = (this.#totalSizeCSS - roundSizeCSS) + 'px'; this.#sidebarElement.style.flexBasis = sidebarSizeValue; // Make both sides relayout boundaries. if (this.#isVertical) { this.#sidebarElement.style.width = sidebarSizeValue; this.#mainElement.style.width = mainSizeValue; this.#sidebarElement.style.height = this.#totalSizeOtherDimensionCSS + 'px'; this.#mainElement.style.height = this.#totalSizeOtherDimensionCSS + 'px'; } else { this.#sidebarElement.style.height = sidebarSizeValue; this.#mainElement.style.height = mainSizeValue; this.#sidebarElement.style.width = this.#totalSizeOtherDimensionCSS + 'px'; this.#mainElement.style.width = this.#totalSizeOtherDimensionCSS + 'px'; } // Position resizer. if (this.#isVertical) { if (this.#secondIsSidebar) { this.#resizerElement.style.right = sidebarSizeValue; this.#resizerElement.style.marginRight = -this.#resizerElementSize / 2 + 'px'; } else { this.#resizerElement.style.left = sidebarSizeValue; this.#resizerElement.style.marginLeft = -this.#resizerElementSize / 2 + 'px'; } } else if (this.#secondIsSidebar) { this.#resizerElement.style.bottom = sidebarSizeValue; this.#resizerElement.style.marginBottom = -this.#resizerElementSize / 2 + 'px'; } else { this.#resizerElement.style.top = sidebarSizeValue; this.#resizerElement.style.marginTop = -this.#resizerElementSize / 2 + 'px'; } this.#sidebarSizeDIP = sizeDIP; // Force layout. if (animate) { this.#animate(false); } else { // No need to recalculate this.sidebarSizeDIP and this.#totalSizeDIP again. this.doResize(); this.dispatchEventToListeners(Events.SIDEBAR_SIZE_CHANGED, this.sidebarSize()); } } #animate(reverse: boolean, callback?: (() => void)): void { const animationTime = 50; this.#animationCallback = callback || null; let animatedMarginPropertyName: string; if (this.#isVertical) { animatedMarginPropertyName = this.#secondIsSidebar ? 'margin-right' : 'margin-left'; } else { animatedMarginPropertyName = this.#secondIsSidebar ? 'margin-bottom' : 'margin-top'; } const marginFrom = reverse ? '0' : '-' + ZoomManager.instance().dipToCSS(this.#sidebarSizeDIP) + 'px'; const marginTo = reverse ? '-' + ZoomManager.instance().dipToCSS(this.#sidebarSizeDIP) + 'px' : '0'; // This order of things is important. // 1. Resize main element early and force layout. this.contentElement.style.setProperty(animatedMarginPropertyName, marginFrom); this.contentElement.style.setProperty('overflow', 'hidden'); if (!reverse) { suppressUnused(this.#mainElement.offsetWidth); suppressUnused(this.#sidebarElement.offsetWidth); } // 2. Issue onresize to the sidebar element, its size won't change. if (!reverse && this.#sidebarWidget) { this.#sidebarWidget.doResize(); } // 3. Configure and run animation this.contentElement.style.setProperty('transition', animatedMarginPropertyName + ' ' + animationTime + 'ms linear'); const boundAnimationFrame = animationFrame.bind(this); let startTime: number|null = null; function animationFrame(this: SplitWidget): void { this.#animationFrameHandle = 0; if (!startTime) { // Kick animation on first frame. this.contentElement.style.setProperty(animatedMarginPropertyName, marginTo); startTime = window.performance.now(); } else if (window.performance.now() < startTime + animationTime) { // Process regular animation frame. if (this.#mainWidget) { this.#mainWidget.doResize(); } } else { // Complete animation. this.#cancelAnimation(); if (this.#mainWidget) { this.#mainWidget.doResize(); } this.dispatchEventToListeners(Events.SIDEBAR_SIZE_CHANGED, this.sidebarSize()); return; } this.#animationFrameHandle = this.contentElement.window().requestAnimationFrame(boundAnimationFrame); } this.#animationFrameHandle = this.contentElement.window().requestAnimationFrame(boundAnimationFrame); } #cancelAnimation(): void { this.contentElement.style.removeProperty('margin-top'); this.contentElement.style.removeProperty('margin-right'); this.contentElement.style.removeProperty('margin-bottom'); this.contentElement.style.removeProperty('margin-left'); this.contentElement.style.removeProperty('transition'); this.contentElement.style.removeProperty('overflow'); if (this.#animationFrameHandle) { this.contentElement.window().cancelAnimationFrame(this.#animationFrameHandle); this.#animationFrameHandle = 0; } if (this.#animationCallback) { this.#animationCallback(); this.#animationCallback = null; } } #applyConstraints(sidebarSize: number, userAction?: boolean): number { const totalSize = this.#totalSizeDIP(); const zoomFactor = this.#constraintsInDip ? 1 : ZoomManager.instance().zoomFactor(); let constraints: Geometry.Constraints = this.#sidebarWidget ? this.#sidebarWidget.constraints() : new Geometry.Constraints(); let minSidebarSize: 20|number = this.isVertical() ? constraints.minimum.width : constraints.minimum.height; if (!minSidebarSize) { minSidebarSize = MinPadding; } minSidebarSize *= zoomFactor; if (this.#sidebarMinimized) { sidebarSize = minSidebarSize; } let preferredSidebarSize: 20|number = this.isVertical() ? constraints.preferred.width : constraints.preferred.height; if (!preferredSidebarSize) { preferredSidebarSize = MinPadding; } preferredSidebarSize *= zoomFactor; // Allow sidebar to be less than preferred by explicit user action. if (sidebarSize < preferredSidebarSize) { preferredSidebarSize = Math.max(sidebarSize, minSidebarSize); } preferredSidebarSize += zoomFactor; // 1 css pixel for splitter border. constraints = this.#mainWidget ? this.#mainWidget.constraints() : new Geometry.Constraints(); let minMainSize: 20|number = this.isVertical() ? constraints.minimum.width : constraints.minimum.height; if (!minMainSize) { minMainSize = MinPadding; } minMainSize *= zoomFactor; let preferredMainSize: 20|number = this.isVertical() ? constraints.preferred.width : constraints.preferred.height; if (!preferredMainSize) { preferredMainSize = MinPadding; } preferredMainSize *= zoomFactor; const savedMainSize = this.isVertical() ? this.#savedVerticalMainSize : this.#savedHorizontalMainSize; if (savedMainSize !== null) { preferredMainSize = Math.min(preferredMainSize, savedMainSize * zoomFactor); } if (userAction) { preferredMainSize = minMainSize; } // Enough space for preferred. const totalPreferred = preferredMainSize + preferredSidebarSize; if (totalPreferred <= totalSize) { return Platform.NumberUtilities.clamp(sidebarSize, preferredSidebarSize, totalSize - preferredMainSize); } // Enough space for minimum. if (minMainSize + minSidebarSize <= totalSize) { const delta = totalPreferred - totalSize; const sidebarDelta = delta * preferredSidebarSize / totalPreferred; sidebarSize = preferredSidebarSize - sidebarDelta; return Platform.NumberUtilities.clamp(sidebarSize, minSidebarSize, totalSize - minMainSize); } // Not enough space even for minimum sizes. return Math.max(0, totalSize - minMainSize); } override wasShown(): void { super.wasShown(); this.#forceUpdateLayout(); ZoomManager.instance().addEventListener(ZoomManagerEvents.ZOOM_CHANGED, this.onZoomChanged, this); } override willHide(): void { super.willHide(); ZoomManager.instance().removeEventListener(ZoomManagerEvents.ZOOM_CHANGED, this.onZoomChanged, this); } override onResize(): void { this.#maybeAutoAdjustOrientation(); this.#updateLayout(); } override onLayout(): void { this.#updateLayout(); } override calculateConstraints(): Geometry.Constraints { if (this.#showMode === ShowMode.ONLY_MAIN) { return this.#mainWidget ? this.#mainWidget.constraints() : new Geometry.Constraints(); } if (this.#showMode === ShowMode.ONLY_SIDEBAR) { return this.#sidebarWidget ? this.#sidebarWidget.constraints() : new Geometry.Constraints(); } let mainConstraints: Geometry.Constraints = this.#mainWidget ? this.#mainWidget.constraints() : new Geometry.Constraints(); let sidebarConstraints: Geometry.Constraints = this.#sidebarWidget ? this.#sidebarWidget.constraints() : new Geometry.Constraints(); const min = MinPadding; if (this.#isVertical) { mainConstraints = mainConstraints.widthToMax(min).addWidth(1); // 1 for splitter sidebarConstraints = sidebarConstraints.widthToMax(min); return mainConstraints.addWidth(sidebarConstraints).heightToMax(sidebarConstraints); } mainConstraints = mainConstraints.heightToMax(min).addHeight(1); // 1 for splitter sidebarConstraints = sidebarConstraints.heightToMax(min); return mainConstraints.widthToMax(sidebarConstraints).addHeight(sidebarConstraints); } #maybeAutoAdjustOrientation(): void { if (this.#autoAdjustOrientation) { const width = this.isVertical() ? this.#totalSizeCSS : this.#totalSizeOtherDimensionCSS; const height = this.isVertical() ? this.#totalSizeOtherDimensionCSS : this.#totalSizeCSS; if (width <= 600 && height >= 600) { this.setVertical(false); } else { this.setVertical(true); } } } #onResizeStart(): void { this.#resizeStartSizeDIP = this.#sidebarSizeDIP; } #onResizeUpdate(event: Common.EventTarget.EventTargetEvent<ResizeUpdatePositionEvent>): void { const offset = event.data.currentPosition - event.data.startPosition; const offsetDIP = ZoomManager.instance().cssToDIP(offset); const newSizeDIP = this.#secondIsSidebar ? this.#resizeStartSizeDIP - offsetDIP : this.#resizeStartSizeDIP + offsetDIP; const constrainedSizeDIP = this.#applyConstraints(newSizeDIP, true); this.#savedSidebarSizeDIP = constrainedSizeDIP; this.#saveSetting(); this.#setSidebarSizeDIP(constrainedSizeDIP, false, true); if (this.isVertical()) { this.#savedVerticalMainSize = this.#totalSizeDIP() - this.#sidebarSizeDIP; } else { this.#savedHorizontalMainSize = this.#totalSizeDIP() - this.#sidebarSizeDIP; } } #onResizeEnd(): void { this.#resizeStartSizeDIP = 0; } hideDefaultResizer(noSplitter?: boolean): void { this.#resizerElement.classList.toggle('hidden', Boolean(noSplitter)); this.uninstallResizer(this.#resizerElement); this.#sidebarElement.classList.toggle('no-default-splitter', Boolean(noSplitter)); } installResizer(resizerElement: Element): void { this.#resizerWidget.addElement((resizerElement as HTMLElement)); } uninstallResizer(resizerElement: Element): void { this.#resizerWidget.removeElement((resizerElement as HTMLElement)); } toggleResizer(resizer: Element, on: boolean): void { if (on) { this.installResizer(resizer); } else { this.uninstallResizer(resizer); } } #settingForOrientation(): SettingForOrientation|null { const state = this.setting ? this.setting.get() : {}; const orientationState = this.#isVertical ? state.vertical : state.horizontal; return orientationState ?? null; } #preferredSidebarSizeDIP(): number { let size: number = this.#savedSidebarSizeDIP; if (!size) { size = this.#isVertical ? this.#defaultSidebarWidth : this.#defaultSidebarHeight; // If we have default value in percents, calculate it on first use. if (0 < size && size < 1) { size *= this.#totalSizeDIP(); } } return size; } #restoreSidebarSizeFromSettings(): void { const settingForOrientation = this.#settingForOrientation(); this.#savedSidebarSizeDIP = settingForOrientation ? settingForOrientation.size : 0; } #restoreAndApplyShowModeFromSettings(): void { const orientationState = this.#settingForOrientation(); this.#savedShowMode = orientationState?.showMode ? orientationState.showMode : this.#showMode; this.#showMode = this.#savedShowMode; switch (this.#savedShowMode) { case ShowMode.BOTH: this.showBoth(); break; case ShowMode.ONLY_MAIN: this.hideSidebar(); break; case ShowMode.ONLY_SIDEBAR: this.hideMain(); break; } } #saveShowModeToSettings(): void { this.#savedShowMode = this.#showMode; this.#saveSetting(); } #saveSetting(): void { if (!this.setting) { return; } const state = this.setting.get(); const orientationState = (this.#isVertical ? state.vertical : state.horizontal) || {} as SettingForOrientation; orientationState.size = this.#savedSidebarSizeDIP; if (this.#shouldSaveShowMode) { orientationState.showMode = this.#savedShowMode; } if (this.#isVertical) { state.vertical = orientationState; } else { state.horizontal = orientationState; } this.setting.set(state); } #forceUpdateLayout(): void { // Force layout even if sidebar size does not change. this.#sidebarSizeDIP = -1; this.#updateLayout(); } onZoomChanged(): void { this.#forceUpdateLayout(); } createShowHideSidebarButton( showTitle: Common.UIString.LocalizedString, hideTitle: Common.UIString.LocalizedString, shownString: Common.UIString.LocalizedString, hiddenString: Common.UIString.LocalizedString, jslogContext?: string): ToolbarButton { this.#showSidebarButtonTitle = showTitle; this.#hideSidebarButtonTitle = hideTitle; this.#shownSidebarString = shownString; this.#hiddenSidebarString = hiddenString; this.#showHideSidebarButton = new ToolbarButton('', 'right-panel-open'); this.#showHideSidebarButton.addEventListener(ToolbarButton.Events.CLICK, buttonClicked, this); if (jslogContext) { this.#showHideSidebarButton.element.setAttribute( 'jslog', `${VisualLogging.toggleSubpane().track({click: true}).context(jslogContext)}`); } this.#updateShowHideSidebarButton(); function buttonClicked(this: SplitWidget): void { this.toggleSidebar(); } return this.#showHideSidebarButton; } /** * @returns true if this call makes the sidebar visible, and false otherwise. */ toggleSidebar(): boolean { if (this.#showMode !== ShowMode.BOTH) { this.showBoth(true); ARIAUtils.LiveAnnouncer.alert(this.#shownSidebarString); return true; } this.hideSidebar(true); ARIAUtils.LiveAnnouncer.alert(this.#hiddenSidebarString); return false; } #updateShowHideSidebarButton(): void { if (!this.#showHideSidebarButton) { return; } const sidebarHidden = this.#showMode === ShowMode.ONLY_MAIN; let glyph = ''; if (sidebarHidden) { glyph = this.isVertical() ? (this.isSidebarSecond() ? 'right-panel-open' : 'left-panel-open') : (this.isSidebarSecond() ? 'bottom-panel-open' : 'top-panel-open'); } else { glyph = this.isVertical() ? (this.isSidebarSecond() ? 'right-panel-close' : 'left-panel-close') : (this.isSidebarSecond() ? 'bottom-panel-close' : 'top-panel-close'); } this.#showHideSidebarButton.setGlyph(glyph); this.#showHideSidebarButton.setTitle(sidebarHidden ? this.#showSidebarButtonTitle : this.#hideSidebarButtonTitle); } } export class SplitWidgetElement extends WidgetElement<SplitWidget> { static readonly observedAttributes = ['direction', 'sidebar-position', 'sidebar-initial-size', 'sidebar-visibility']; override createWidget(): SplitWidget { const vertical = this.getAttribute('direction') === 'column'; const autoAdjustOrientation = this.getAttribute('direction') === 'auto'; const secondIsSidebar = this.getAttribute('sidebar-position') === 'second'; const settingName = this.getAttribute('name') ?? undefined; const sidebarSize = parseInt(this.getAttribute('sidebar-initial-size') || '', 10); const defaultSidebarWidth = !isNaN(sidebarSize) ? sidebarSize : undefined; const defaultSidebarHeight = !isNaN(sidebarSize) ? sidebarSize : undefined; const widget = new SplitWidget( vertical, secondIsSidebar, settingName, defaultSidebarWidth, defaultSidebarHeight, /* constraintsInDip=*/ false, this); if (this.getAttribute('sidebar-initial-size') === 'minimized') { widget.setSidebarMinimized(true); } if (autoAdjustOrientation) { widget.setAutoAdjustOrientation(true); } const sidebarHidden = this.getAttribute('sidebar-visibility') === 'hidden'; if (sidebarHidden) { widget.hideSidebar(); } widget.addEventListener(Events.SHOW_MODE_CHANGED, () => { this.dispatchEvent(new CustomEvent('change', {detail: widget.showMode()})); }); return widget; } attributeChangedCallback(name: string, _oldValue: string, newValue: string): void { const widget = Widget.get(this) as SplitWidget | null; if (!widget) { return; } if (name === 'direction') { widget.setVertical(newValue === 'column'); widget.setAutoAdjustOrientation(newValue === 'auto'); } else if (name === 'sidebar-position') { widget.setSecondIsSidebar(newValue === 'second'); } else if (name === 'sidebar-visibility') { if (newValue === 'hidden') { widget.hideSidebar(); } else { widget.showBoth(); } } } } customElements.define('devtools-split-view', SplitWidgetElement); export const enum ShowMode { BOTH = 'Both', ONLY_MAIN = 'OnlyMain', ONLY_SIDEBAR = 'OnlySidebar', } export const enum Events { SIDEBAR_SIZE_CHANGED = 'SidebarSizeChanged', SHOW_MODE_CHANGED = 'ShowModeChanged', } export interface EventTypes { [Events.SIDEBAR_SIZE_CHANGED]: number; [Events.SHOW_MODE_CHANGED]: string; } const MinPadding = 20; export interface SettingForOrientation { showMode: ShowMode; size: number; } const suppressUnused = function(_value: unknown): void {};