Skip to content

Commit 02ec7c4

Browse files
committed
fix: restore RDP input after application focus changes
1 parent acdd829 commit 02ec7c4

2 files changed

Lines changed: 93 additions & 40 deletions

File tree

‎ui/lion/hooks/useGuacamoleClient.ts‎

Lines changed: 85 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import type { SessionShareOnlineUser } from "@jumpserver/connectors-core";
22
import type { MaybeRefOrGetter } from "vue";
33
import type { LionUploadCustomRequestOptions } from "@/lion/types/upload";
44
import type { GuacamoleConnectionErrorDetails } from "@/lion/utils/status";
5-
import { useDebounceFn } from "@vueuse/core";
5+
import { useDebounceFn, useEventListener } from "@vueuse/core";
66

77
import * as Guacamole from "guacamole-common-js-jumpserver/dist/guacamole-common";
8-
import { computed, nextTick, ref, shallowRef, toValue } from "vue";
8+
import { computed, ref, shallowRef, toValue, watch } from "vue";
99
import { LUNA_MESSAGE_TYPE } from "@/lion/types/postmessage.type";
1010
import { withLionUrl } from "@/lion/utils/base";
1111
import { readClipboardText, writeClipboardBlob, writeClipboardText } from "@/lion/utils/clipboard";
@@ -206,6 +206,7 @@ export function useGuacamoleClient(
206206
const currentHeight = ref(window.innerHeight);
207207
const pixelDensity = 1;
208208
const sink = new Guacamole.InputSink();
209+
sink.getElement().disabled = true;
209210
const keyboard = new Guacamole.Keyboard();
210211
const pressedKeys = ref<Set<number>>(new Set());
211212
const isRemoteApp = ref<boolean>(false);
@@ -442,38 +443,85 @@ export function useGuacamoleClient(
442443
}
443444
}, 300);
444445

445-
const registerMouseAndKeyboardHandler = () => {
446+
const registerMouseAndKeyboardHandler = (isActive: () => boolean = () => true) => {
446447
const client = guaClient.value as any;
447448
if (!client || !client.getDisplay) {
448449
return console.warn("Guacamole client is not initialized or does not support mouse and keyboard events");
449450
}
450451
inputCleanup?.();
451452
inputCleanup = null;
452-
const mouse = registerMouse(client);
453-
const touchScreen = registerTouchScreen(client);
454-
455-
registerKeyboard(client);
456453
const display = client.getDisplay();
457454
const displayEl = display.getElement();
458-
455+
const sinkEl = sink.getElement() as HTMLTextAreaElement;
456+
const inputEnabled = () =>
457+
guaClient.value === client &&
458+
isActive() &&
459+
currentUser.value.writable !== false &&
460+
[2, 3].includes(connectStatus.value);
461+
const isPageActive = () => !document.hidden && document.hasFocus();
462+
const canInput = () => inputEnabled() && isPageActive();
463+
const canUseSink = () => canInput() && !sinkEl.disabled;
464+
const mouse = registerMouse(client, canUseSink, () => focusInput(true));
465+
const touchScreen = registerTouchScreen(client, canInput);
466+
registerKeyboard(client, canUseSink);
467+
468+
let restoreOnFocus = false;
469+
const releaseInput = () => {
470+
keyboard.reset();
471+
// ponytail: The legacy dependency lacks upstream Mouse.reset(); use that API when upgrading.
472+
for (const source of [mouse, touchScreen]) {
473+
const state = source?.currentState;
474+
if (!state || !(state.left || state.middle || state.right || state.up || state.down)) continue;
475+
state.left = state.middle = state.right = state.up = state.down = false;
476+
sendScaledMouseState(client, state);
477+
}
478+
};
479+
const suspendInput = () => {
480+
releaseInput();
481+
// Block Guacamole's pending/document-keydown refocusing while inactive.
482+
sinkEl.disabled = true;
483+
};
484+
function focusInput(force = false) {
485+
if (!inputEnabled() || !isPageActive() || !displayEl.isConnected || !displayEl.getClientRects().length) return;
486+
const focused = document.activeElement;
487+
if (!force && focused !== sinkEl && focused !== document.body && focused !== null) return;
488+
restoreOnFocus = true;
489+
sinkEl.disabled = false;
490+
sink.focus();
491+
}
492+
const handleFocusIn = (event: FocusEvent) => {
493+
if (event.target === document.body) return;
494+
restoreOnFocus = event.target === sinkEl;
495+
if (!restoreOnFocus) suspendInput();
496+
};
497+
const handlePageFocus = (event: Event) => {
498+
if (event.type === "blur" || !isPageActive()) suspendInput();
499+
else if (restoreOnFocus) focusInput();
500+
};
459501
const handleMouseEnter = () => {
460-
document.body.focus();
461502
display.showCursor(false);
462-
nextTick(() => {
463-
sink.focus();
464-
});
465-
};
466-
const handleMouseLeave = () => {
467-
nextTick(() => {
468-
keyboard.reset();
469-
});
503+
focusInput();
470504
};
471-
displayEl.addEventListener("mouseenter", handleMouseEnter);
472-
displayEl.addEventListener("mouseleave", handleMouseLeave);
505+
const stopListeners = [
506+
useEventListener(displayEl, "mouseenter", handleMouseEnter),
507+
useEventListener(displayEl, "mouseleave", releaseInput),
508+
useEventListener(sinkEl, "blur", releaseInput),
509+
useEventListener(document, "focusin", handleFocusIn),
510+
useEventListener(document, "visibilitychange", handlePageFocus),
511+
useEventListener(window, ["blur", "focus"], handlePageFocus)
512+
];
513+
const stopWatch = watch(
514+
inputEnabled,
515+
(enabled) => {
516+
if (enabled) sinkEl.disabled = false;
517+
else suspendInput();
518+
},
519+
{ immediate: true, flush: "sync" }
520+
);
473521
inputCleanup = () => {
474-
displayEl.removeEventListener("mouseenter", handleMouseEnter);
475-
displayEl.removeEventListener("mouseleave", handleMouseLeave);
476-
keyboard.reset();
522+
stopWatch();
523+
stopListeners.forEach((stop) => stop());
524+
suspendInput();
477525
keyboard.onkeydown = null;
478526
keyboard.onkeyup = null;
479527
if (mouse) {
@@ -645,7 +693,7 @@ export function useGuacamoleClient(
645693
}
646694
return false;
647695
};
648-
function registerKeyboard(client: any) {
696+
function registerKeyboard(client: any, canInput: () => boolean = () => true) {
649697
if (!client || !client.getDisplay) {
650698
console.warn("Guacamole client is not initialized or does not support keyboard events");
651699
return;
@@ -661,6 +709,7 @@ export function useGuacamoleClient(
661709
}
662710

663711
keyboard.onkeydown = (keysym: any) => {
712+
if (!canInput()) return true;
664713
if (isBlockedCombination(keysym)) {
665714
console.warn("Keydown Blocked key combination detected:", keysym);
666715
return;
@@ -670,17 +719,14 @@ export function useGuacamoleClient(
670719
lunaCommunicator.sendLuna(LUNA_MESSAGE_TYPE.KEYBOARDEVENT, "");
671720
};
672721
keyboard.onkeyup = (keysym: any) => {
673-
if (keysym !== commandKeySym && isBlockedCombination(keysym)) {
674-
console.warn("Keyup Blocked key combination detected:", keysym);
675-
return;
676-
}
677-
pressedKeys.value.delete(keysym);
722+
// A key sent before a blocked combination still needs its matching release.
723+
if (!pressedKeys.value.delete(keysym)) return;
678724
client.sendKeyEvent(0, keysym);
679725
};
680726
display.getElement().appendChild(sink.getElement());
681727
}
682728

683-
function registerTouchScreen(client: any) {
729+
function registerTouchScreen(client: any, canInput: () => boolean = () => true) {
684730
if (!client || !client.getDisplay) {
685731
console.warn("Guacamole client is not initialized or does not support screen events");
686732
return null;
@@ -693,7 +739,7 @@ export function useGuacamoleClient(
693739
const touchScreen = new Guacamole.Mouse.Touchscreen(display.getElement());
694740
const handleEmulatedMouseDown = (mouseState: any) => {
695741
// Emulate mouse down event
696-
if (!client || !display) {
742+
if (!client || !display || !canInput()) {
697743
return;
698744
}
699745
lunaCommunicator.sendLuna(LUNA_MESSAGE_TYPE.MOUSE_EVENT, "");
@@ -704,7 +750,7 @@ export function useGuacamoleClient(
704750

705751
const handleEmulatedMouseState = (mouseState: any) => {
706752
// Emulate mouse move/up event
707-
if (!client || !display) {
753+
if (!client || !display || !canInput()) {
708754
return;
709755
}
710756
lunaCommunicator.sendLuna(LUNA_MESSAGE_TYPE.MOUSE_EVENT, "");
@@ -740,7 +786,7 @@ export function useGuacamoleClient(
740786
guaClient.value.sendKeyEvent(released, keysym);
741787
};
742788

743-
function registerMouse(client: any) {
789+
function registerMouse(client: any, canInput: () => boolean = () => true, beforeMouseDown?: () => void) {
744790
if (!client || !client.getDisplay) {
745791
console.warn("Guacamole client is not initialized or does not support mouse events");
746792
return null;
@@ -751,16 +797,16 @@ export function useGuacamoleClient(
751797
return null;
752798
}
753799
const sendMouseState = (mouseState: any) => {
800+
if (!canInput()) return;
754801
sendScaledMouseState(client, mouseState);
755802
};
756803
const mouse = new Guacamole.Mouse(display.getElement());
757-
mouse.onmousedown =
758-
mouse.onmouseup =
759-
mouse.onmousemove =
760-
(mouseState: any) => {
761-
// Send mouse state, hide cursor if necessary
762-
sendMouseState(mouseState);
763-
};
804+
mouse.onmousedown = (mouseState: any) => {
805+
// Follow upstream guacClient: request focus before forwarding the mouse down.
806+
beforeMouseDown?.();
807+
sendMouseState(mouseState);
808+
};
809+
mouse.onmouseup = mouse.onmousemove = sendMouseState;
764810
mouse.onmouseout = (_mouseState: any) => {
765811
// Send mouse state, hide cursor if necessary
766812
display.showCursor(false);

‎ui/lion/views/ConnectView.vue‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ const emit = defineEmits<{
2626
disconnected: [message: string, details?: GuacamoleConnectionErrorDetails, dismissible?: boolean];
2727
}>();
2828
29+
const { activePaneId, setActivePane } = useWorkspaceTabs();
30+
const isInputActive = () => !props.tabId || activePaneId.value === props.tabId;
31+
const activateInputPane = () => {
32+
if (props.tabId) setActivePane(props.tabId);
33+
};
34+
2935
const toast = useToast();
3036
const { addErrorToast } = useErrorToast();
3137
const { t } = useI18n();
@@ -348,7 +354,7 @@ onMounted(async () => {
348354
displayEl.addEventListener("drop", fileDrop, false);
349355
displayEl.addEventListener("contextmenu", preventDefault, false);
350356
351-
registerMouseAndKeyboardHandler();
357+
registerMouseAndKeyboardHandler(isInputActive);
352358
window.addEventListener("focus", debouncedSendClipboardToRemote);
353359
});
354360
@@ -489,6 +495,7 @@ watch(
489495
ref="displayRef"
490496
class="relative flex h-full w-full min-h-0 justify-center"
491497
:class="[shouldEnableScroll ? 'overflow-auto' : 'overflow-hidden']"
498+
@pointerdown.capture="activateInputPane"
492499
/>
493500

494501
<Osk v-if="showOsk" :keyboard="keyboardLayout" @keyboard-change="handleScreenKeyboard" />

0 commit comments

Comments
 (0)