feat: fullscreen panels

This commit is contained in:
mathuo 2023-10-29 13:53:39 +00:00
parent b17aa24637
commit 73cd0dba4e
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
13 changed files with 301 additions and 59 deletions

View File

@ -804,4 +804,20 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
moveToPrevious(options?: MovementOptions): void { moveToPrevious(options?: MovementOptions): void {
this.component.moveToPrevious(options); this.component.moveToPrevious(options);
} }
maximizeGroup(panel: IDockviewPanel): void {
this.component.maximizeGroup(panel.group);
}
hasMaximizedGroup(): boolean {
return this.component.hasMaximizedGroup();
}
exitMaxmizedGroup(): void {
this.component.exitMaximizedGroup();
}
get onDidMaxmizedGroupChange(): Event<void> {
return this.component.onDidMaxmizedGroupChange;
}
} }

View File

@ -31,6 +31,7 @@ export interface DockviewPanelApi
position?: Position; position?: Position;
index?: number; index?: number;
}): void; }): void;
maximize(): void;
} }
export class DockviewPanelApiImpl export class DockviewPanelApiImpl
@ -120,4 +121,8 @@ export class DockviewPanelApiImpl
close(): void { close(): void {
this.group.model.closePanel(this.panel); this.group.model.closePanel(this.panel);
} }
maximize(): void {
this.accessor.maximizeGroup(this.panel.group);
}
} }

View File

@ -10,6 +10,35 @@
width: 100%; width: 100%;
z-index: 1; z-index: 1;
} }
.dv-fullscreen {
z-index: 9;
transition: all 0.25s ease-in-out;
// outline: 1px solid red;
position: absolute;
}
.dv-fullscreen-animate {
top: 0px !important;
left: 0px !important;
width: 100% !important;
height: 100% !important;
}
.dv-fullscreen-close-button {
position: absolute;
bottom: 0px;
right: 0px;
z-index: 999;
width: 20px;
height: 20px;
color: white;
background-color: black;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
} }
.groupview { .groupview {

View File

@ -7,7 +7,7 @@ import {
import { directionToPosition, Droptarget, Position } from '../dnd/droptarget'; import { directionToPosition, Droptarget, Position } from '../dnd/droptarget';
import { tail, sequenceEquals, remove } from '../array'; import { tail, sequenceEquals, remove } from '../array';
import { DockviewPanel, IDockviewPanel } from './dockviewPanel'; import { DockviewPanel, IDockviewPanel } from './dockviewPanel';
import { CompositeDisposable } from '../lifecycle'; import { CompositeDisposable, IDisposable } from '../lifecycle';
import { Event, Emitter } from '../events'; import { Event, Emitter } from '../events';
import { Watermark } from './components/watermark/watermark'; import { Watermark } from './components/watermark/watermark';
import { import {
@ -46,7 +46,11 @@ import { DockviewPanelModel } from './dockviewPanelModel';
import { getPanelData } from '../dnd/dataTransfer'; import { getPanelData } from '../dnd/dataTransfer';
import { Parameters } from '../panel/types'; import { Parameters } from '../panel/types';
import { Overlay } from '../dnd/overlay'; import { Overlay } from '../dnd/overlay';
import { toggleClass, watchElementResize } from '../dom'; import {
FocusTrap as FocusRetainment,
toggleClass,
watchElementResize,
} from '../dom';
import { import {
DockviewFloatingGroupPanel, DockviewFloatingGroupPanel,
IDockviewFloatingGroupPanel, IDockviewFloatingGroupPanel,
@ -55,6 +59,7 @@ import {
GroupDragEvent, GroupDragEvent,
TabDragEvent, TabDragEvent,
} from './components/titlebar/tabsContainer'; } from './components/titlebar/tabsContainer';
import { createCloseButton } from '../svg';
const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100; const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100;

View File

@ -68,8 +68,8 @@ export class DockviewGroupPanel
id, id,
'groupview_default', 'groupview_default',
{ {
minimumHeight: 100, minimumHeight: 0,
minimumWidth: 100, minimumWidth: 0,
}, },
new DockviewGroupPanelApiImpl(id, accessor) new DockviewGroupPanelApiImpl(id, accessor)
); );

View File

@ -130,6 +130,8 @@ export interface IDockviewGroupPanelModel extends IPanel {
): boolean; ): boolean;
} }
export type DockviewGroupMode = 'grid' | 'floating' | 'fullscreen' | 'popout';
export class DockviewGroupPanelModel export class DockviewGroupPanelModel
extends CompositeDisposable extends CompositeDisposable
implements IDockviewGroupPanelModel implements IDockviewGroupPanelModel

View File

@ -185,3 +185,24 @@ export function quasiPreventDefault(event: Event): void {
export function quasiDefaultPrevented(event: Event): boolean { export function quasiDefaultPrevented(event: Event): boolean {
return (event as any)[QUASI_PREVENT_DEFAULT_KEY]; return (event as any)[QUASI_PREVENT_DEFAULT_KEY];
} }
export class FocusTrap {
private element: Element | null;
constructor() {
this.element = null;
}
retain(): void {
this.element = document.activeElement;
}
focus(): void {
if (
document.activeElement !== this.element &&
this.element?.parentElement
) {
(this.element as HTMLElement)?.focus?.();
}
}
}

View File

@ -64,6 +64,10 @@ export interface IBaseGrid<T extends IGridPanelView> {
layout(width: number, height: number, force?: boolean): void; layout(width: number, height: number, force?: boolean): void;
setVisible(panel: T, visible: boolean): void; setVisible(panel: T, visible: boolean): void;
isVisible(panel: T): boolean; isVisible(panel: T): boolean;
maximizeGroup(panel: T): void;
exitMaximizedGroup(): void;
hasMaximizedGroup(): boolean;
readonly onDidMaxmizedGroupChange: Event<void>;
} }
export abstract class BaseGrid<T extends IGridPanelView> export abstract class BaseGrid<T extends IGridPanelView>
@ -174,6 +178,22 @@ export abstract class BaseGrid<T extends IGridPanelView>
return this.gridview.isViewVisible(getGridLocation(panel.element)); return this.gridview.isViewVisible(getGridLocation(panel.element));
} }
maximizeGroup(panel: T): void {
this.gridview.maximizeView(panel);
}
exitMaximizedGroup(): void {
this.gridview.exitMaximizedView();
}
hasMaximizedGroup(): boolean {
return this.gridview.hasMaximizedView();
}
get onDidMaxmizedGroupChange(): Event<void> {
return this.gridview.onDidMaxmizedNodeChange;
}
protected doAddGroup( protected doAddGroup(
group: T, group: T,
location: number[] = [0], location: number[] = [0],

View File

@ -33,6 +33,10 @@ export class BranchNode extends CompositeDisposable implements IView {
readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> = readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> =
this._onDidChange.event; this._onDidChange.event;
private readonly _onDidVisibilityChange = new Emitter<boolean>();
readonly onDidVisibilityChange: Event<boolean> =
this._onDidVisibilityChange.event;
get width(): number { get width(): number {
return this.orientation === Orientation.HORIZONTAL return this.orientation === Orientation.HORIZONTAL
? this.size ? this.size
@ -48,11 +52,23 @@ export class BranchNode extends CompositeDisposable implements IView {
get minimumSize(): number { get minimumSize(): number {
return this.children.length === 0 return this.children.length === 0
? 0 ? 0
: Math.max(...this.children.map((c) => c.minimumOrthogonalSize)); : Math.max(
...this.children.map((c, index) =>
this.splitview.isViewVisible(index)
? c.minimumOrthogonalSize
: 0
)
);
} }
get maximumSize(): number { get maximumSize(): number {
return Math.min(...this.children.map((c) => c.maximumOrthogonalSize)); return Math.min(
...this.children.map((c, index) =>
this.splitview.isViewVisible(index)
? c.maximumOrthogonalSize
: Number.POSITIVE_INFINITY
)
);
} }
get minimumOrthogonalSize(): number { get minimumOrthogonalSize(): number {
@ -163,6 +179,7 @@ export class BranchNode extends CompositeDisposable implements IView {
this.addDisposables( this.addDisposables(
this._onDidChange, this._onDidChange,
this._onDidVisibilityChange,
this.splitview.onDidSashEnd(() => { this.splitview.onDidSashEnd(() => {
this._onDidChange.fire({}); this._onDidChange.fire({});
}) })
@ -185,7 +202,7 @@ export class BranchNode extends CompositeDisposable implements IView {
return this.splitview.isViewVisible(index); return this.splitview.isViewVisible(index);
} }
setChildVisible(index: number, visible: boolean): void { setChildVisible(index: number, visible: boolean): void {
if (index < 0 || index >= this.children.length) { if (index < 0 || index >= this.children.length) {
throw new Error('Invalid index'); throw new Error('Invalid index');
} }
@ -194,7 +211,18 @@ export class BranchNode extends CompositeDisposable implements IView {
return; return;
} }
const wereAllChildrenHidden = this.splitview.contentSize === 0;
this.splitview.setViewVisible(index, visible); this.splitview.setViewVisible(index, visible);
const areAllChildrenHidden = this.splitview.contentSize === 0;
// If all children are hidden then the parent should hide the entire splitview
// If the entire splitview is hidden then the parent should show the splitview when a child is shown
if (
(visible && wereAllChildrenHidden) ||
(!visible && areAllChildrenHidden)
) {
this._onDidVisibilityChange.fire(visible);
}
} }
moveChild(from: number, to: number): void { moveChild(from: number, to: number): void {
@ -285,15 +313,23 @@ export class BranchNode extends CompositeDisposable implements IView {
private setupChildrenEvents(): void { private setupChildrenEvents(): void {
this._childrenDisposable.dispose(); this._childrenDisposable.dispose();
this._childrenDisposable = Event.any( this._childrenDisposable = new CompositeDisposable(
...this.children.map((c) => c.onDidChange) Event.any(...this.children.map((c) => c.onDidChange))((e) => {
)((e) => { /**
/** * indicate a change has occured to allows any re-rendering but don't bubble
* indicate a change has occured to allows any re-rendering but don't bubble * event because that was specific to this branch
* event because that was specific to this branch */
*/ this._onDidChange.fire({ size: e.orthogonalSize });
this._onDidChange.fire({ size: e.orthogonalSize }); }),
}); ...this.children.map((c, i) => {
if (c instanceof BranchNode) {
return c.onDidVisibilityChange((visible) => {
this.setChildVisible(i, visible);
});
}
return Disposable.NONE;
})
);
} }
public dispose(): void { public dispose(): void {

View File

@ -270,9 +270,11 @@ export interface SerializedGridview<T> {
} }
export class Gridview implements IDisposable { export class Gridview implements IDisposable {
readonly element: HTMLElement;
private _root: BranchNode | undefined; private _root: BranchNode | undefined;
public readonly element: HTMLElement; private _maximizedNode: Node | undefined = undefined;
private disposable: MutableDisposable = new MutableDisposable(); private readonly disposable: MutableDisposable = new MutableDisposable();
private readonly _onDidChange = new Emitter<{ private readonly _onDidChange = new Emitter<{
size?: number; size?: number;
@ -281,6 +283,9 @@ export class Gridview implements IDisposable {
readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> = readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> =
this._onDidChange.event; this._onDidChange.event;
private readonly _onDidMaxmizedNodeChange = new Emitter<void>();
readonly onDidMaxmizedNodeChange = this._onDidMaxmizedNodeChange.event;
public get length(): number { public get length(): number {
return this._root ? this._root.children.length : 0; return this._root ? this._root.children.length : 0;
} }
@ -319,6 +324,62 @@ export class Gridview implements IDisposable {
return this.root.maximumHeight; return this.root.maximumHeight;
} }
hasMaximizedView(): boolean {
return this._maximizedNode !== undefined;
}
maximizeView(view: IGridView): void {
const location = getGridLocation(view.element);
const [_, node] = this.getNode(location);
if (this._maximizedNode === node) {
return;
}
if (this.hasMaximizedView()) {
this.exitMaximizedView();
}
function hideAllViewsBut(parent: BranchNode, exclude: LeafNode): void {
for (let i = 0; i < parent.children.length; i++) {
const child = parent.children[i];
if (child instanceof LeafNode) {
if (child !== exclude) {
parent.setChildVisible(i, false);
}
} else {
hideAllViewsBut(child, exclude);
}
}
}
hideAllViewsBut(this.root, node as LeafNode);
this._maximizedNode = node;
this._onDidMaxmizedNodeChange.fire();
}
exitMaximizedView(): void {
if (!this._maximizedNode) {
return;
}
function showViewsInReverseOrder(parent: BranchNode): void {
for (let index = parent.children.length - 1; index >= 0; index--) {
const child = parent.children[index];
if (child instanceof LeafNode) {
parent.setChildVisible(index, true);
} else {
showViewsInReverseOrder(child);
}
}
}
showViewsInReverseOrder(this.root);
this._maximizedNode = undefined;
this._onDidMaxmizedNodeChange.fire();
}
public serialize(): SerializedGridview<any> { public serialize(): SerializedGridview<any> {
const root = serializeBranchNode(this.getView(), this.orientation); const root = serializeBranchNode(this.getView(), this.orientation);
@ -333,6 +394,7 @@ export class Gridview implements IDisposable {
public dispose(): void { public dispose(): void {
this.disposable.dispose(); this.disposable.dispose();
this._onDidChange.dispose(); this._onDidChange.dispose();
this._onDidMaxmizedNodeChange.dispose();
this.root.dispose(); this.root.dispose();
this.element.remove(); this.element.remove();
@ -584,6 +646,10 @@ export class Gridview implements IDisposable {
} }
setViewVisible(location: number[], visible: boolean): void { setViewVisible(location: number[], visible: boolean): void {
if (this.hasMaximizedView()) {
this.exitMaximizedView();
}
const [rest, index] = tail(location); const [rest, index] = tail(location);
const [, parent] = this.getNode(rest); const [, parent] = this.getNode(rest);
@ -595,6 +661,10 @@ export class Gridview implements IDisposable {
} }
public moveView(parentLocation: number[], from: number, to: number): void { public moveView(parentLocation: number[], from: number, to: number): void {
if (this.hasMaximizedView()) {
this.exitMaximizedView();
}
const [, parent] = this.getNode(parentLocation); const [, parent] = this.getNode(parentLocation);
if (!(parent instanceof BranchNode)) { if (!(parent instanceof BranchNode)) {
@ -609,6 +679,10 @@ export class Gridview implements IDisposable {
size: number | Sizing, size: number | Sizing,
location: number[] location: number[]
): void { ): void {
if (this.hasMaximizedView()) {
this.exitMaximizedView();
}
const [rest, index] = tail(location); const [rest, index] = tail(location);
const [pathToParent, parent] = this.getNode(rest); const [pathToParent, parent] = this.getNode(rest);
@ -670,6 +744,10 @@ export class Gridview implements IDisposable {
} }
removeView(location: number[], sizing?: Sizing): IGridView { removeView(location: number[], sizing?: Sizing): IGridView {
if (this.hasMaximizedView()) {
this.exitMaximizedView();
}
const [rest, index] = tail(location); const [rest, index] = tail(location);
const [pathToParent, parent] = this.getNode(rest); const [pathToParent, parent] = this.getNode(rest);

View File

@ -121,7 +121,7 @@ export class LeafNode implements IView {
public setVisible(visible: boolean): void { public setVisible(visible: boolean): void {
if (this.view.setVisible) { if (this.view.setVisible) {
this.view.setVisible(visible); this.view.setVisible(visible);
this._onDidChange.fire({}); // this._onDidChange.fire({});
} }
} }

View File

@ -104,8 +104,8 @@ export class Splitview {
private _orientation: Orientation; private _orientation: Orientation;
private _size = 0; private _size = 0;
private _orthogonalSize = 0; private _orthogonalSize = 0;
private contentSize = 0; private _contentSize = 0;
private _proportions: number[] | undefined = undefined; private _proportions: (number | undefined)[] | undefined = undefined;
private proportionalLayout: boolean; private proportionalLayout: boolean;
private _startSnappingEnabled = true; private _startSnappingEnabled = true;
private _endSnappingEnabled = true; private _endSnappingEnabled = true;
@ -117,6 +117,10 @@ export class Splitview {
private readonly _onDidRemoveView = new Emitter<IView>(); private readonly _onDidRemoveView = new Emitter<IView>();
readonly onDidRemoveView = this._onDidRemoveView.event; readonly onDidRemoveView = this._onDidRemoveView.event;
get contentSize(): number {
return this._contentSize;
}
get size(): number { get size(): number {
return this._size; return this._size;
} }
@ -137,7 +141,7 @@ export class Splitview {
return this.viewItems.length; return this.viewItems.length;
} }
public get proportions(): number[] | undefined { public get proportions(): (number | undefined)[] | undefined {
return this._proportions ? [...this._proportions] : undefined; return this._proportions ? [...this._proportions] : undefined;
} }
@ -242,7 +246,7 @@ export class Splitview {
}); });
// Initialize content size and proportions for first layout // Initialize content size and proportions for first layout
this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); this._contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
this.saveProportions(); this.saveProportions();
} }
} }
@ -654,7 +658,7 @@ export class Splitview {
} }
public layout(size: number, orthogonalSize: number): void { public layout(size: number, orthogonalSize: number): void {
const previousSize = Math.max(this.size, this.contentSize); const previousSize = Math.max(this.size, this._contentSize);
this.size = size; this.size = size;
this.orthogonalSize = orthogonalSize; this.orthogonalSize = orthogonalSize;
@ -675,14 +679,30 @@ export class Splitview {
highPriorityIndexes highPriorityIndexes
); );
} else { } else {
let total = 0;
for (let i = 0; i < this.viewItems.length; i++) { for (let i = 0; i < this.viewItems.length; i++) {
const item = this.viewItems[i]; const item = this.viewItems[i];
const proportion = this.proportions[i];
item.size = clamp( if (typeof proportion === 'number') {
Math.round(this.proportions[i] * size), total += proportion;
item.minimumSize, } else {
item.maximumSize size -= item.size;
); }
}
for (let i = 0; i < this.viewItems.length; i++) {
const item = this.viewItems[i];
const proportion = this.proportions[i];
if (typeof proportion === 'number' && total > 0) {
item.size = clamp(
Math.round((proportion * size) / total),
item.minimumSize,
item.maximumSize
);
}
} }
} }
@ -747,15 +767,15 @@ export class Splitview {
} }
private saveProportions(): void { private saveProportions(): void {
if (this.proportionalLayout && this.contentSize > 0) { if (this.proportionalLayout && this._contentSize > 0) {
this._proportions = this.viewItems.map( this._proportions = this.viewItems.map((i) =>
(i) => i.size / this.contentSize i.visible ? i.size / this._contentSize : undefined
); );
} }
} }
private layoutViews(): void { private layoutViews(): void {
this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); this._contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
let sum = 0; let sum = 0;
const x: number[] = []; const x: number[] = [];
@ -880,7 +900,7 @@ export class Splitview {
} else if ( } else if (
snappedAfter && snappedAfter &&
collapsesDown[index] && collapsesDown[index] &&
(position < this.contentSize || this.endSnappingEnabled) (position < this._contentSize || this.endSnappingEnabled)
) { ) {
this.updateSash(sash, SashState.MAXIMUM); this.updateSash(sash, SashState.MAXIMUM);
} else { } else {

View File

@ -13,7 +13,12 @@ import './app.scss';
const components = { const components = {
default: (props: IDockviewPanelProps<{ title: string }>) => { default: (props: IDockviewPanelProps<{ title: string }>) => {
return <div style={{ padding: '20px' }}>{props.params.title}</div>; return (
<div style={{ padding: '20px' }}>
{props.params.title}
<input value={'hi'} />
</div>
);
}, },
}; };
@ -105,29 +110,6 @@ const Icon = (props: {
); );
}; };
const Button = () => {
const [position, setPosition] = React.useState<
{ x: number; y: number } | undefined
>(undefined);
const close = () => setPosition(undefined);
const onClick = (event: React.MouseEvent) => {
setPosition({ x: event.pageX, y: event.pageY });
};
return (
<>
<Icon icon="more_vert" onClick={onClick} />
{position && (
<Popover position={position} close={close}>
<div>hello</div>
</Popover>
)}
</>
);
};
const groupControlsComponents = { const groupControlsComponents = {
panel_1: () => { panel_1: () => {
return <Icon icon="file_download" />; return <Icon icon="file_download" />;
@ -143,6 +125,34 @@ const RightControls = (props: IDockviewHeaderActionsProps) => {
return groupControlsComponents[props.activePanel.id]; return groupControlsComponents[props.activePanel.id];
}, [props.isGroupActive, props.activePanel]); }, [props.isGroupActive, props.activePanel]);
const [icon, setIcon] = React.useState<string>(
props.containerApi.hasMaximizedGroup()
? 'collapse_content'
: 'expand_content'
);
React.useEffect(() => {
const disposable = props.containerApi.onDidMaxmizedGroupChange(() => {
setIcon(
props.containerApi.hasMaximizedGroup()
? 'collapse_content'
: 'expand_content'
);
});
return () => {
disposable.dispose();
};
}, [props.containerApi]);
const onClick = () => {
if (props.containerApi.hasMaximizedGroup()) {
props.containerApi.exitMaxmizedGroup();
} else {
props.activePanel?.api.maximize();
}
};
return ( return (
<div <div
className="group-control" className="group-control"
@ -156,7 +166,7 @@ const RightControls = (props: IDockviewHeaderActionsProps) => {
> >
{props.isGroupActive && <Icon icon="star" />} {props.isGroupActive && <Icon icon="star" />}
{Component && <Component />} {Component && <Component />}
<Button /> <Icon icon={icon} onClick={onClick} />
</div> </div>
); );
}; };