From 3e1871213359eaf58d16ecc1a2e8dc3d2378fd86 Mon Sep 17 00:00:00 2001 From: dominik003 <info@dominik-lammers.de> Date: Tue, 31 Oct 2023 17:47:28 +0100 Subject: [PATCH] feat: Add tiling window manager with slider --- frontend/src/app/app.module.ts | 4 + .../src/app/helpers/default-value.pipe.ts | 15 ++ .../floating-window-manager.component.css | 4 +- .../floating-window-manager.component.html | 7 + .../floating-window-manager.component.ts | 2 +- .../sessions/session/session.component.html | 25 ++- .../app/sessions/session/session.component.ts | 6 +- .../tiling-window-manager.component.css | 18 ++ .../tiling-window-manager.component.html | 75 +++++++ .../tiling-window-manager.component.ts | 199 ++++++++++++++++++ 10 files changed, 342 insertions(+), 13 deletions(-) create mode 100644 frontend/src/app/helpers/default-value.pipe.ts create mode 100644 frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.css create mode 100644 frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.html create mode 100644 frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 99d8fcf77f..cbb6e5c5c8 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -63,6 +63,7 @@ import { VersionComponent } from './general/metadata/version/version.component'; import { NavBarMenuComponent } from './general/nav-bar-menu/nav-bar-menu.component'; import { NoticeComponent } from './general/notice/notice.component'; import { ConfirmationDialogComponent } from './helpers/confirmation-dialog/confirmation-dialog.component'; +import { DefaultValuePipe } from './helpers/default-value.pipe'; import { DisplayValueComponent } from './helpers/display-value/display-value.component'; import { InputDialogComponent } from './helpers/input-dialog/input-dialog.component'; import { MatIconComponent } from './helpers/mat-icon/mat-icon.component'; @@ -109,6 +110,7 @@ import { WhitespaceUrlInterceptor } from './services/encoder/encoder.interceptor import { DeleteSessionDialogComponent } from './sessions/delete-session-dialog/delete-session-dialog.component'; import { FloatingWindowManagerComponent } from './sessions/session/floating-window-manager/floating-window-manager.component'; import { SessionComponent } from './sessions/session/session.component'; +import { TilingWindowManagerComponent } from './sessions/session/tiling-window-manager/tiling-window-manager.component'; import { SessionOverviewComponent } from './sessions/session-overview/session-overview.component'; import { SessionsComponent } from './sessions/sessions.component'; import { ActiveSessionsComponent } from './sessions/user-sessions-wrapper/active-sessions/active-sessions.component'; @@ -164,6 +166,7 @@ import { SettingsComponent } from './settings/settings.component'; CreateReadonlySessionComponent, CreateReadonlySessionDialogComponent, CreateT4cModelNewRepositoryComponent, + DefaultValuePipe, DeleteGitSettingsDialogComponent, DeleteSessionDialogComponent, DisplayValueComponent, @@ -222,6 +225,7 @@ import { SettingsComponent } from './settings/settings.component'; T4CSettingsComponent, T4CSettingsWrapperComponent, TextLineSkeletonLoaderComponent, + TilingWindowManagerComponent, ToolDeletionDialogComponent, ToolDetailsComponent, ToolIntegrationsComponent, diff --git a/frontend/src/app/helpers/default-value.pipe.ts b/frontend/src/app/helpers/default-value.pipe.ts new file mode 100644 index 0000000000..ab31513185 --- /dev/null +++ b/frontend/src/app/helpers/default-value.pipe.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'defaultValue', +}) +export class DefaultValuePipe implements PipeTransform { + transform(value: string | undefined, defaultValue: string): string { + return value ?? defaultValue; + } +} diff --git a/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.css b/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.css index 00d1f55736..56719f4d28 100644 --- a/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.css +++ b/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.css @@ -7,12 +7,10 @@ height: calc(100vh - 2vh - 65px - 110px); } -.iframe-overlay { +#iframe-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; - - z-index: 40; } diff --git a/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.html b/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.html index f82e079b10..336f59455a 100644 --- a/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.html +++ b/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.html @@ -48,6 +48,13 @@ ></div> <div [ngClass]="draggingActive ? 'iframe-overlay' : ''"></div> <iframe + [title]=" + (session.version?.tool?.name | defaultValue: 'unknown tool name') + + '-' + + (session.version?.name | defaultValue: 'unknown version name') + + '-' + + session.type + " [id]="'session-' + session.id" [src]="session.safeResourceURL" class="h-full w-full" diff --git a/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.ts b/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.ts index dfdc2cb375..2ac1f13c7b 100644 --- a/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.ts +++ b/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.ts @@ -39,7 +39,7 @@ export class FloatingWindowManagerComponent implements OnInit { unfocusSession(focusedSession: Session) { this.sessions .filter((session) => session !== focusedSession) - .map((session) => (session.focused = false)); + .forEach((session) => (session.focused = false)); } dragStart() { diff --git a/frontend/src/app/sessions/session/session.component.html b/frontend/src/app/sessions/session/session.component.html index 1485192ff7..f48118d1ca 100644 --- a/frontend/src/app/sessions/session/session.component.html +++ b/frontend/src/app/sessions/session/session.component.html @@ -51,8 +51,10 @@ </div> <div class="mt-2"> <mat-radio-group [(ngModel)]="selectedWindowType"> - <mat-radio-button value="floating">Floating window</mat-radio-button> - <mat-radio-button value="tailing">Tailing window</mat-radio-button> + <mat-radio-button value="floating" + >Floating window manager</mat-radio-button + > + <mat-radio-button value="tiling">Tiling window manager</mat-radio-button> </mat-radio-group> </div> @@ -69,10 +71,21 @@ </div> </div> -<app-floating-window-manager - *ngIf="selectedSessions.length" - [sessions]="selectedSessions" -></app-floating-window-manager> +<ng-container *ngIf="selectedSessions.length"> + <app-floating-window-manager + *ngIf="isFloatingWindowManager; else tilingWindow" + [sessions]="selectedSessions" + > + </app-floating-window-manager> + + <ng-template #tilingWindow> + <app-tiling-window-manager + *ngIf="isTilingWindowManager" + [sessions]="selectedSessions" + > + </app-tiling-window-manager> + </ng-template> +</ng-container> <div class="fixed bottom-4 right-4 z-50"> <button diff --git a/frontend/src/app/sessions/session/session.component.ts b/frontend/src/app/sessions/session/session.component.ts index d2350426a7..edf205cc98 100644 --- a/frontend/src/app/sessions/session/session.component.ts +++ b/frontend/src/app/sessions/session/session.component.ts @@ -39,11 +39,11 @@ export class SessionComponent implements OnInit { return this.cachedSessions?.filter((session) => session.checked); } - get isTailingWindow(): boolean { - return this.selectedWindowType === 'tailing'; + get isTilingWindowManager(): boolean { + return this.selectedWindowType === 'tiling'; } - get isFloatingWindow() { + get isFloatingWindowManager(): boolean { return this.selectedWindowType === 'floating'; } diff --git a/frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.css b/frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.css new file mode 100644 index 0000000000..782c0293cc --- /dev/null +++ b/frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.css @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +.height { + height: calc(100vh - 2vh - 65px - 110px); +} + +.iframe-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + + cursor: col-resize; +} diff --git a/frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.html b/frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.html new file mode 100644 index 0000000000..02e5368c24 --- /dev/null +++ b/frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.html @@ -0,0 +1,75 @@ +<!-- + ~ SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors + ~ SPDX-License-Identifier: Apache-2.0 + --> + +<div class="flex gap-0.5"> + <ng-container *ngFor="let session of sessions"> + <div + class="flex flex-col" + [ngClass]="{ + height: (fullscreenService.isFullscreen$ | async) === false, + 'h-screen': fullscreenService.isFullscreen$ | async + }" + [style.width]="session.width + 'px'" + > + <div + class="flex items-center justify-between gap-2 rounded-t bg-slate-100 p-1" + > + <span> + {{ session.version?.tool?.name }} {{ session.version?.name }}, + {{ session.type }} + <span *ngIf="session.type === 'readonly'" + >(project {{ session.project!.name }})</span + > + {{ session.id }} + </span> + + <div class="flex items-center"> + <mat-icon + *ngIf="session.index !== 0" + class="cursor-pointer text-lg text-gray-600 hover:text-gray-800" + (click)="onLeftClick(session)" + > + arrow_back + </mat-icon> + + <mat-icon + *ngIf="session.index !== sessions.length - 1" + class="cursor-pointer text-lg text-gray-600 hover:text-gray-800" + (click)="onRightClick(session)" + > + arrow_forward + </mat-icon> + </div> + </div> + + <div class="relative grow"> + <div + class="iframe-overlay" + *ngIf="isValidResizeState(this.resizeState)" + ></div> + <iframe + [title]=" + (session.version?.tool?.name | defaultValue: 'unknown tool name') + + '-' + + (session.version?.name | defaultValue: 'unknown version name') + + '-' + + session.type + " + [id]="'session-' + session.id" + [src]="session.safeResourceURL" + class="h-full w-full" + allow="clipboard-read; clipboard-write" + > + </iframe> + </div> + </div> + + <div + *ngIf="session.index < sessions.length - 1" + class="my-2 w-2 cursor-col-resize bg-gray-300" + (mousedown)="onMouseDown($event, session.index)" + ></div> + </ng-container> +</div> diff --git a/frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.ts b/frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.ts new file mode 100644 index 0000000000..07b3c9c7fa --- /dev/null +++ b/frontend/src/app/sessions/session/tiling-window-manager/tiling-window-manager.component.ts @@ -0,0 +1,199 @@ +/* + * SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Component, HostListener, Input, OnInit } from '@angular/core'; +import { Session } from 'src/app/schemes'; +import { FullscreenService } from '../../service/fullscreen.service'; + +@Component({ + selector: 'app-tiling-window-manager', + templateUrl: './tiling-window-manager.component.html', + styleUrls: ['./tiling-window-manager.component.css'], +}) +export class TilingWindowManagerComponent implements OnInit { + private _tilingSessions: TilingSession[] = []; + + @Input() + set sessions(sessions: Session[]) { + this._tilingSessions = sessions.map((session, index) => ({ + ...session, + index: index, + width: -1, + fullscreen: false, + })); + this.resetWidths(); + } + + get sessions(): TilingSession[] { + return this._tilingSessions.sort((a, b) => a.index - b.index); + } + + public resizeState: ResizeState = {}; + + private minimumSessionWidth = 0; + private existingMargin = 0; + + constructor(public fullscreenService: FullscreenService) {} + + ngOnInit(): void { + this.fullscreenService.isFullscreen$.subscribe((isFullscreen) => { + this.existingMargin = isFullscreen ? 0 : 27.48; + this.resetWidths(); + }); + } + + onMouseDown(event: MouseEvent, index: number): void { + const leftSession = this.getSessionByIndex(index); + const rightSession = this.getSessionByIndex(index + 1); + + if (leftSession && rightSession) { + this.resizeState = { + index: index, + startX: event.clientX, + leftSession: leftSession, + rightSession: rightSession, + startWidthLeft: leftSession.width, + startWidthRight: rightSession.width, + }; + this.toggleIFrameEvents(false); + } + } + + @HostListener('window:mousemove', ['$event']) + onMouseMove(event: MouseEvent): void { + if (this.isValidResizeState(this.resizeState)) { + const delta = event.clientX - this.resizeState.startX; + const [newWidthLeft, newWidthRight] = this.calculateNewWidths( + this.resizeState, + delta, + ); + + this.resizeState.leftSession.width = newWidthLeft; + this.resizeState.rightSession.width = newWidthRight; + } + } + + @HostListener('window:mouseup') + onMouseUp(): void { + this.resizeState = {}; + this.toggleIFrameEvents(true); + } + + @HostListener('window:resize') + onResize() { + this.resetWidths(); + } + + onLeftClick(session: TilingSession) { + const leftIndexSession = this.getSessionByIndex(session.index - 1); + + if (leftIndexSession) { + this.swapSessionIndices(session, leftIndexSession); + } + } + + onRightClick(session: TilingSession) { + const rightIndexSession = this.getSessionByIndex(session.index + 1); + + if (rightIndexSession) { + this.swapSessionIndices(session, rightIndexSession); + } + } + + isValidResizeState(state: ResizeState): state is ValidResizeState { + return ( + state.index !== undefined && + state.startX !== undefined && + state.leftSession !== undefined && + state.rightSession !== undefined && + state.startWidthLeft !== undefined && + state.startWidthRight !== undefined + ); + } + + private calculateNewWidths( + validResizeState: ValidResizeState, + delta: number, + ): [number, number] { + let newWidthLeft = validResizeState.startWidthLeft + delta; + let newWidthRight = validResizeState.startWidthRight - delta; + + [newWidthLeft, newWidthRight] = this.adjustWidthsWithMinimum( + validResizeState, + newWidthLeft, + newWidthRight, + ); + return [newWidthLeft, newWidthRight]; + } + + private adjustWidthsWithMinimum( + validResizeState: ValidResizeState, + newWidthLeft: number, + newWidthRight: number, + ): [number, number] { + if (newWidthLeft < this.minimumSessionWidth) { + newWidthLeft = this.minimumSessionWidth; + newWidthRight = + validResizeState.startWidthLeft + + validResizeState.startWidthRight - + newWidthLeft; + } else if (newWidthRight < this.minimumSessionWidth) { + newWidthRight = this.minimumSessionWidth; + newWidthLeft = + validResizeState.startWidthLeft + + validResizeState.startWidthRight - + newWidthRight; + } + return [newWidthLeft, newWidthRight]; + } + + private resetWidths() { + this.minimumSessionWidth = (window.innerWidth - this.existingMargin) * 0.15; + this._tilingSessions.forEach( + (session) => + (session.width = + (window.innerWidth - this.existingMargin) / this.sessions.length), + ); + } + + private getSessionByIndex(index: number): TilingSession | undefined { + return this._tilingSessions.find((session) => session.index === index); + } + + private toggleIFrameEvents(enable: boolean): void { + const action = enable ? 'auto' : 'none'; + document.querySelectorAll('iframe').forEach((iframe) => { + iframe.style.pointerEvents = action; + iframe.style.userSelect = action; + }); + } + + private swapSessionIndices( + firstSession: TilingSession, + secondSession: TilingSession, + ) { + const firstSessionIndex = firstSession.index; + + firstSession.index = secondSession.index; + secondSession.index = firstSessionIndex; + } +} + +type ResizeState = { + index?: number; + startX?: number; + leftSession?: TilingSession; + rightSession?: TilingSession; + startWidthLeft?: number; + startWidthRight?: number; +}; + +type ValidResizeState = Required<ResizeState>; + +type TilingSession = Session & { + index: number; + width: number; + fullscreen: boolean; +};