Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Component, EventEmitter, Input, Output, TemplateRef } from '@angular/core';
import { FloatingSessionToolbarComponent } from '@shared/components/floating-session-toolbar/floating-session-toolbar.component';
import { ToolbarAction } from '@shared/components/floating-session-toolbar/models/floating-session-toolbar-action.model';
import {
ScreenMode,
ToolbarFeatures,
ToolbarInitialState,
WheelSpeedControl,
} from '@shared/components/floating-session-toolbar/models/floating-session-toolbar-config.model';
import {
ToolbarSessionInfo,
ToolbarSessionInfoTemplateContext,
} from '@shared/components/floating-session-toolbar/models/session-info.model';

/** Thin integration shell for ARD sessions. */
@Component({
selector: 'ard-toolbar-wrapper',
standalone: true,
imports: [FloatingSessionToolbarComponent],
template: `
<floating-session-toolbar
[features]="features"
[initialCursorCrosshair]="initialState?.cursorCrosshair ?? true"
[initialWheelSpeed]="initialState?.wheelSpeed ?? 1"
[wheelSpeedControl]="wheelSpeedControl"
[clipboardActionButtons]="actions"
[sessionInfo]="sessionInfo"
[sessionInfoTemplate]="sessionInfoTemplate"
(sessionClose)="sessionClose.emit()"
(screenModeChange)="screenModeChange.emit($event)"
(cursorCrosshairChange)="cursorCrosshairChange.emit($event)"
(wheelSpeedChange)="wheelSpeedChange.emit($event)">
</floating-session-toolbar>
`,
})
export class ArdToolbarWrapperComponent {
@Input() actions: ToolbarAction[] = [];
@Input() sessionInfo: ToolbarSessionInfo | null = null;
@Input() sessionInfoTemplate: TemplateRef<ToolbarSessionInfoTemplateContext> | null = null;
@Input() initialState?: ToolbarInitialState;
@Input() wheelSpeedControl!: WheelSpeedControl;

@Output() readonly sessionClose = new EventEmitter<void>();
@Output() readonly screenModeChange = new EventEmitter<ScreenMode>();
@Output() readonly cursorCrosshairChange = new EventEmitter<boolean>();
@Output() readonly wheelSpeedChange = new EventEmitter<number>();

protected readonly features: ToolbarFeatures = {
sessionInfo: true,
screenMode: true,
cursorCrosshair: true,
wheelSpeed: true,
};
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
<div #sessionArdContainer class="session-ard-container">
<session-toolbar
[sessionContainerParent]="sessionContainerElement"
[middleButtons]="middleToolbarButtons"
[middleToggleButtons]="middleToolbarToggleButtons"
[rightButtons]="rightToolbarButtons"
[sliders]="sliders"
[clipboardActionButtons]="clipboardActionButtons">
</session-toolbar>

<div class="session-ard-container">
@if (loading) {
<div class="loading-info-container">
<div class="loading-info">
Expand All @@ -20,4 +11,18 @@

<iron-remote-desktop #ironRemoteDesktopElement targetplatform="web" verbose="true" scale="fit" flexcenter="true" [module]="backendRef"></iron-remote-desktop>

@if (!loading && !currentStatus.isDisabled) {
<div class="toolbar-overlay-anchor">
<ard-toolbar-wrapper
[actions]="clipboardActionButtons"
[sessionInfo]="sessionInfo"
[initialState]="{ cursorCrosshair: cursorOverrideActive, wheelSpeed: wheelSpeed }"
[wheelSpeedControl]="wheelSpeedControl"
(sessionClose)="startTerminationProcess()"
(screenModeChange)="handleScreenModeChange($event)"
(cursorCrosshairChange)="onCursorCrosshairChange($event)"
(wheelSpeedChange)="onWheelSpeedChange($event)">
</ard-toolbar-wrapper>
</div>
}
</div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core';
import { Component, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import { DesktopWebClientBaseComponent } from '@shared/bases/desktop-web-client-base.component';
import { GatewayAlertMessageService } from '@shared/components/gateway-alert-message/gateway-alert-message.service';
import { ScreenScale } from '@shared/enums/screen-scale.enum';
Expand All @@ -14,6 +14,7 @@ import '@devolutions/iron-remote-desktop/iron-remote-desktop.js';
import { ardQualityMode, Backend, resolutionQuality, wheelSpeedFactor } from '@devolutions/iron-remote-desktop-vnc';
import { DVL_ARD_ICON, JET_ARD_URL } from '@gateway/app.constants';
import { AnalyticService, ProtocolString } from '@gateway/shared/services/analytic.service';
import { WheelSpeedControl } from '@shared/components/floating-session-toolbar/models/floating-session-toolbar-config.model';
import { ExtractedHostnamePort } from '@shared/services/utils/string.service';
import { v4 as uuidv4 } from 'uuid';

Expand All @@ -25,57 +26,20 @@ import { v4 as uuidv4 } from 'uuid';
})
export class WebClientArdComponent
extends DesktopWebClientBaseComponent<ArdFormDataInput>
implements OnInit, AfterViewInit, OnDestroy
implements OnInit, OnDestroy
{
@ViewChild('sessionArdContainer') sessionContainerElement: ElementRef;

backendRef = Backend;

middleToolbarButtons = [
{
label: 'Full Screen',
icon: 'dvl-icon dvl-icon-fullscreen',
action: () => this.toggleFullscreen(),
},
{
label: 'Fit to Screen',
icon: 'dvl-icon dvl-icon-minimize',
action: () => this.scaleTo(ScreenScale.Fit),
},
{
label: 'Actual Size',
icon: 'dvl-icon dvl-icon-screen',
action: () => this.scaleTo(ScreenScale.Real),
},
];

middleToolbarToggleButtons = [
{
label: 'Toggle Cursor Kind',
icon: 'dvl-icon dvl-icon-cursor',
action: () => this.toggleCursorKind(),
isActive: () => !this.cursorOverrideActive,
},
];

rightToolbarButtons = [
{
label: 'Close Session',
icon: 'dvl-icon dvl-icon-close',
action: () => this.startTerminationProcess(),
},
];

sliders = [
{
label: 'Wheel Speed',
value: 1,
onChange: (value: number) => this.setWheelSpeedFactor(value),
min: 0.1,
max: 3,
step: 0.1,
},
];
// ── Floating toolbar state ─────────────────────────────────────────────────
wheelSpeed = 1;
// sessionInfo / sessionInfoUrl / sessionInfoUsername / refreshSessionInfo() inherited from WebClientBaseComponent
readonly wheelSpeedControl: WheelSpeedControl = {
label: 'Wheel speed',
min: 0.1,
max: 3,
step: 0.1,
};
// ──

constructor(
protected renderer: Renderer2,
Expand All @@ -90,26 +54,35 @@ export class WebClientArdComponent

ngOnInit(): void {
this.webSessionIcon = DVL_ARD_ICON;
this.refreshSessionInfo();

super.ngOnInit();
}

setWheelSpeedFactor(factor: number): void {
this.remoteClient.invokeExtension(wheelSpeedFactor(factor));
// ── Floating toolbar handlers ─────────────────────────────────────────────
onCursorCrosshairChange(enabled: boolean): void {
if (enabled !== this.cursorOverrideActive) {
this.toggleCursorKind();
}
}

protected handleExitFullScreenEvent(): void {
this.isFullScreenMode = false;

const sessionContainerElement = this.sessionContainerElement.nativeElement;
const sessionToolbarElement = sessionContainerElement.querySelector('#sessionToolbar');
onWheelSpeedChange(factor: number): void {
this.setWheelSpeedFactor(factor);
}

if (sessionToolbarElement) {
this.renderer.removeClass(sessionToolbarElement, 'session-toolbar-layer');
private setWheelSpeedFactor(factor: number): void {
this.wheelSpeed = factor;
if (this.remoteClient) {
this.remoteClient.invokeExtension(wheelSpeedFactor(factor));
}
}

protected handleExitFullScreenEvent(): void {
this.isFullScreenMode = false;

this.scaleTo(ScreenScale.Fit);
}
// ──

protected startConnectionProcess(): void {
this.getFormData()
Expand All @@ -132,6 +105,9 @@ export class WebClientArdComponent
return from(this.webSessionService.getWebSession(this.webSessionId)).pipe(
map((currentWebSession) => {
this.formData = currentWebSession.data as ArdFormDataInput;
this.wheelSpeed = this.formData.wheelSpeedFactor ?? 1;
this.sessionInfoUsername = this.formData.username || null;
this.refreshSessionInfo();
}),
);
}
Expand All @@ -142,6 +118,9 @@ export class WebClientArdComponent

const sessionId: string = uuidv4();
const gatewayAddress = this.getGatewayWebSocketUrl(JET_ARD_URL, sessionId);
this.sessionInfoUrl = this.toUserFacingUrl(gatewayAddress);
this.sessionInfoUsername = username || null;
this.refreshSessionInfo();

const connectionParameters: IronARDConnectionParameters = {
username,
Expand Down Expand Up @@ -178,6 +157,8 @@ export class WebClientArdComponent
configBuilder.withExtension(ardQualityMode(connectionParameters.ardQualityMode));
}

configBuilder.withExtension(wheelSpeedFactor(connectionParameters.wheelSpeedFactor));

const config = configBuilder.build();

this.setKeyboardUnicodeMode(true);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Component, EventEmitter, Input, Output, TemplateRef } from '@angular/core';
import { UserInteraction } from '@devolutions/iron-remote-desktop';
import { FloatingSessionToolbarComponent } from '@shared/components/floating-session-toolbar/floating-session-toolbar.component';
import { ToolbarAction } from '@shared/components/floating-session-toolbar/models/floating-session-toolbar-action.model';
import {
ScreenMode,
ToolbarFeatures,
ToolbarInitialState,
} from '@shared/components/floating-session-toolbar/models/floating-session-toolbar-config.model';
import {
ToolbarSessionInfo,
ToolbarSessionInfoTemplateContext,
} from '@shared/components/floating-session-toolbar/models/session-info.model';

/**
* Thin integration shell for the floating toolbar in RDP sessions.
* Owns the static RDP feature config and absorbs remoteClient-direct calls
* (Windows key, Ctrl+Alt+Del, unicode keyboard mode).
* Everything else is bubbled up as @Output() for the protocol component to handle.
*/
@Component({
selector: 'rdp-toolbar-wrapper',
standalone: true,
imports: [FloatingSessionToolbarComponent],
template: `
<floating-session-toolbar
[features]="features"
[dynamicResizeSupported]="dynamicResizeSupported"
[initialDynamicResize]="initialState?.dynamicResize ?? true"
[initialUnicodeKeyboard]="initialState?.unicodeKeyboard ?? true"
[initialCursorCrosshair]="initialState?.cursorCrosshair ?? true"
[clipboardActionButtons]="actions"
[sessionInfo]="sessionInfo"
[sessionInfoTemplate]="sessionInfoTemplate"
(windowsKeyPress)="remoteClient.metaKey()"
(ctrlAltDelPress)="remoteClient.ctrlAltDel()"
(unicodeKeyboardChange)="remoteClient.setKeyboardUnicodeMode($event)"
(sessionClose)="sessionClose.emit()"
(screenModeChange)="screenModeChange.emit($event)"
(dynamicResizeChange)="dynamicResizeChange.emit($event)"
(cursorCrosshairChange)="cursorCrosshairChange.emit($event)">
</floating-session-toolbar>
`,
})
export class RdpToolbarWrapperComponent {
/** The active remote client — available after the 'ready' event fires. */
@Input() remoteClient!: UserInteraction;

/** Clipboard actions built by the protocol component; passed straight through. */
@Input() actions: ToolbarAction[] = [];
@Input() dynamicResizeSupported = true;
@Input() sessionInfo: ToolbarSessionInfo | null = null;
@Input() sessionInfoTemplate: TemplateRef<ToolbarSessionInfoTemplateContext> | null = null;

/** Seed values for stateful toolbar toggles (written once on first bind). */
@Input() initialState?: ToolbarInitialState;

@Output() readonly sessionClose = new EventEmitter<void>();
@Output() readonly screenModeChange = new EventEmitter<ScreenMode>();
@Output() readonly dynamicResizeChange = new EventEmitter<boolean>();
@Output() readonly cursorCrosshairChange = new EventEmitter<boolean>();

/** Static RDP feature set — defined once here, never scattered across callers. */
protected readonly features: ToolbarFeatures = {
windowsKey: true,
sessionInfo: true,
ctrlAltDel: true,
screenMode: true,
dynamicResize: true,
unicodeKeyboard: true,
cursorCrosshair: true,
};
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,4 @@
<div #sessionRdpContainer class="session-rdp-container">
<session-toolbar
[sessionContainerParent]="sessionContainerElement"
[leftButtons]="leftToolbarButtons"
[middleButtons]="middleToolbarButtons"
[middleToggleButtons]="middleToolbarToggleButtons"
[rightButtons]="rightToolbarButtons"
[checkboxes]="checkboxes"
[clipboardActionButtons]="clipboardActionButtons">
</session-toolbar>

<div class="session-rdp-container">
@if (loading) {
<div class="loading-info-container">
<div class="loading-info">
Expand All @@ -21,4 +11,24 @@

<iron-remote-desktop #ironRemoteDesktopElement targetplatform="web" verbose="true" scale="fit" flexcenter="true" [module]="backendRef"></iron-remote-desktop>

<!-- Floating toolbar — only rendered once the session is live.
Keeping it out of the DOM during the initial connect and any
reconnect cycle prevents the toolbar from being positioned
against a wider ancestor while the tab panel is still being
laid out (which caused it to jump to the viewport center). -->
@if (!loading && !currentStatus.isDisabled) {
<div class="toolbar-overlay-anchor">
<rdp-toolbar-wrapper
[remoteClient]="remoteClient"
[actions]="clipboardActionButtons"
[sessionInfo]="sessionInfo"
[dynamicResizeSupported]="dynamicResizeSupported"
[initialState]="{ dynamicResize: dynamicResizeEnabled, unicodeKeyboard: useUnicodeKeyboard, cursorCrosshair: cursorOverrideActive }"
(sessionClose)="startTerminationProcess()"
(screenModeChange)="handleScreenModeChange($event)"
(dynamicResizeChange)="onDynamicResizeChange($event)"
(cursorCrosshairChange)="onCursorCrosshairChange($event)">
</rdp-toolbar-wrapper>
</div>
}
</div>
Loading
Loading