From 011ec3d509b0933c2e0962f278e20e9032814055 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Wed, 22 Apr 2026 00:05:38 +0200 Subject: [PATCH] Improve keyboard input user-experience and support Shift --- server/2015Remote/WebPage.h | 420 +++++++++++++++++++++++++++++++++--- 1 file changed, 395 insertions(+), 25 deletions(-) diff --git a/server/2015Remote/WebPage.h b/server/2015Remote/WebPage.h index c9d7bc2..28f91a5 100644 --- a/server/2015Remote/WebPage.h +++ b/server/2015Remote/WebPage.h @@ -449,6 +449,15 @@ inline std::string GetWebPageHTML() { height: 100dvh !important; max-height: none !important; } + /* Portrait fullscreen: align canvas to top */ + @media (orientation: portrait) { + #screen-page:fullscreen .canvas-container, + #screen-page:-webkit-full-screen .canvas-container, + #screen-page.pseudo-fullscreen .canvas-container { + align-items: flex-start !important; + padding-top: 56px !important; + } + } #screen-page.pseudo-fullscreen #screen-canvas { max-width: 100vw !important; max-height: 100vh !important; max-height: 100dvh !important; @@ -554,6 +563,65 @@ inline std::string GetWebPageHTML() { transform: translate(-50%, -50%); z-index: 999; } + /* Utility class for hiding elements */ + .ui-hidden { display: none !important; } + /* Quick control buttons - always visible on touch devices */ + .quick-controls { + position: absolute; + display: none; + gap: 8px; + z-index: 100; + padding: 8px; + } + .quick-controls.visible { display: flex; } + .quick-controls .qc-btn { + width: 44px; + height: 44px; + border-radius: 50%; + border: none; + background: rgba(0,0,0,0.6); + color: #fff; + font-size: 20px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s; + } + .quick-controls .qc-btn:hover { background: rgba(0,0,0,0.8); } + .quick-controls .qc-btn:active { transform: scale(0.9); } + .quick-controls .qc-btn.active { background: rgba(52,199,89,0.9); } + .quick-controls .qc-btn:disabled { opacity: 0.4; } + /* Portrait: horizontal at top center */ + @media (orientation: portrait) { + .quick-controls { + top: 4px; + left: 50%; + transform: translateX(-50%); + flex-direction: row; + padding: 4px; + gap: 6px; + } + .quick-controls .qc-btn { + width: 40px; + height: 40px; + font-size: 18px; + } + /* Portrait: align canvas to top, leave space for controls */ + .canvas-container { + align-items: flex-start; + padding-top: 56px; + } + } + /* Landscape: vertical at right center */ + @media (orientation: landscape) { + .quick-controls { + right: 8px; + top: 8px; + transform: none; + flex-direction: column; + } + } .cursor-overlay { position: fixed; width: 24px; @@ -567,6 +635,37 @@ inline std::string GetWebPageHTML() { transform-origin: 0 0; } .cursor-overlay.active { display: block; } + /* Input shortcut bar - below canvas, portrait mode only */ + .input-shortcuts { + position: absolute; + left: 50%; + transform: translateX(-50%); + display: none; + justify-content: center; + gap: 4px; + padding: 6px 4px; + z-index: 101; + } + .input-shortcuts.visible { display: flex; } + .input-shortcuts .shortcut-btn { + min-width: 36px; + height: 36px; + padding: 0 10px; + border-radius: 6px; + border: 1px solid rgba(128,128,128,0.5); + background: rgba(128,128,128,0.4); + color: #fff; + font-size: 16px; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.1s; + text-shadow: 0 0 2px #000, 0 0 4px #000, 1px 1px 1px #000; + } + .input-shortcuts .shortcut-btn:hover { background: rgba(128,128,128,0.5); } + .input-shortcuts .shortcut-btn:active { transform: scale(0.95); background: rgba(128,128,128,0.6); } /* Mobile responsive */ @media (max-width: 768px) { .page { padding: 10px; } @@ -655,7 +754,26 @@ inline std::string GetWebPageHTML() { -
+
+ +
+
+ + + + +
+
+ + + + + + + + +
+
@@ -907,6 +1025,9 @@ inline std::string GetWebPageHTML() { ctx.fillStyle = '#000'; ctx.fillRect(0, 0, width, height); + // Update input shortcuts position after canvas resize + requestAnimationFrame(updateInputShortcutsPosition); + // Set up vertical flip transform once (BMP is bottom-up) ctx.setTransform(1, 0, 0, -1, 0, height); if (decoder) { try { decoder.close(); } catch(e) {} } @@ -1198,16 +1319,17 @@ inline std::string GetWebPageHTML() { function toggleFullscreen() { const el = document.getElementById('screen-page'); - const isFullscreen = document.fullscreenElement || document.webkitFullscreenElement; + const isFs = document.fullscreenElement || document.webkitFullscreenElement; const isPseudo = el.classList.contains('pseudo-fullscreen'); // Try native fullscreen first if (!isPseudo && (el.requestFullscreen || el.webkitRequestFullscreen)) { - if (!isFullscreen) { + if (!isFs) { (el.requestFullscreen || el.webkitRequestFullscreen).call(el).catch(() => { // Native fullscreen failed (iOS), use pseudo-fullscreen el.classList.add('pseudo-fullscreen'); window.scrollTo(0, 1); + updateUIForOrientation(); }); } else { if (document.exitFullscreen) document.exitFullscreen(); @@ -1217,6 +1339,7 @@ inline std::string GetWebPageHTML() { // Toggle pseudo-fullscreen (iOS fallback) el.classList.toggle('pseudo-fullscreen'); if (el.classList.contains('pseudo-fullscreen')) window.scrollTo(0, 1); + updateUIForOrientation(); } } )HTML"; @@ -1235,6 +1358,7 @@ inline std::string GetWebPageHTML() { const now = Date.now(); if (now - lastTapTime < 300 && e.changedTouches.length === 1) { el.classList.remove('pseudo-fullscreen'); + updateUIForOrientation(); e.preventDefault(); } lastTapTime = now; @@ -1254,14 +1378,25 @@ inline std::string GetWebPageHTML() { let toolbarVisible = false; let toolbarHideTimer = null; + function isFullscreen() { + return !!(document.fullscreenElement || document.webkitFullscreenElement || + document.getElementById('screen-page')?.classList.contains('pseudo-fullscreen')); + } + function toggleFloatingToolbar() { const toolbar = document.getElementById('floating-toolbar'); toolbarVisible = !toolbarVisible; toolbar.classList.toggle('visible', toolbarVisible); - // Auto-hide after 4 seconds - if (toolbarHideTimer) clearTimeout(toolbarHideTimer); - if (toolbarVisible) { + // Clear any existing timer + if (toolbarHideTimer) { + clearTimeout(toolbarHideTimer); + toolbarHideTimer = null; + } + + // Only auto-hide when NOT in fullscreen (desktop behavior) + // In fullscreen (touch device), user must click again to close + if (toolbarVisible && !isFullscreen()) { toolbarHideTimer = setTimeout(() => { toolbarVisible = false; toolbar.classList.remove('visible'); @@ -1269,6 +1404,88 @@ inline std::string GetWebPageHTML() { } } + function isLandscape() { + return window.innerWidth > window.innerHeight; + } + + // Cached DOM elements (initialized in window.onload) + let uiElements = null; + + function initUIElements() { + uiElements = { + quickControls: document.getElementById('quick-controls'), + toolbarToggle: document.getElementById('toolbar-toggle'), + floatingToolbar: document.getElementById('floating-toolbar'), + btnRdpResetBar: document.getElementById('btn-rdp-reset-bar'), + btnMouseBar: document.getElementById('btn-mouse-bar'), + btnKeyboardBar: document.getElementById('btn-keyboard-bar'), + inputShortcuts: document.getElementById('input-shortcuts') + }; + } + + function updateUIForOrientation() { + if (!isTouchDevice || !uiElements) return; + + // Clear any pending toolbar hide timer when orientation/fullscreen changes + if (toolbarHideTimer) { + clearTimeout(toolbarHideTimer); + toolbarHideTimer = null; + } + + const { quickControls, toolbarToggle, floatingToolbar, + btnRdpResetBar, btnMouseBar, btnKeyboardBar, inputShortcuts } = uiElements; + + if (isLandscape()) { + // Landscape mode + quickControls.classList.remove('visible'); + inputShortcuts.classList.remove('visible'); + + if (isFullscreen()) { + // Landscape fullscreen: show three-dot menu + toolbarToggle.classList.remove('ui-hidden'); + btnRdpResetBar.classList.add('ui-hidden'); + btnMouseBar.classList.add('ui-hidden'); + btnKeyboardBar.classList.add('ui-hidden'); + } else { + // Landscape non-fullscreen: show top toolbar buttons + toolbarToggle.classList.add('ui-hidden'); + floatingToolbar.classList.remove('visible'); + toolbarVisible = false; + btnRdpResetBar.classList.remove('ui-hidden'); + btnMouseBar.classList.remove('ui-hidden'); + btnKeyboardBar.classList.remove('ui-hidden'); + } + } else { + // Portrait mode: show quick controls + quickControls.classList.add('visible'); + // Input shortcuts only visible when keyboard is open + const keyboardOpen = document.activeElement === mobileKeyboard; + inputShortcuts.classList.toggle('visible', keyboardOpen); + toolbarToggle.classList.add('ui-hidden'); + floatingToolbar.classList.remove('visible'); + toolbarVisible = false; + btnRdpResetBar.classList.add('ui-hidden'); + btnMouseBar.classList.add('ui-hidden'); + btnKeyboardBar.classList.add('ui-hidden'); + } + // Position input shortcuts below canvas + updateInputShortcutsPosition(); + } + + // Position input shortcuts bar below the canvas + function updateInputShortcutsPosition() { + if (!isTouchDevice || !uiElements || !uiElements.inputShortcuts) return; + if (isLandscape()) return; // Only for portrait mode + + const canvas = document.getElementById('screen-canvas'); + const shortcuts = uiElements.inputShortcuts; + if (!canvas || canvas.offsetHeight === 0) return; + + // Position shortcuts 8px below canvas + const canvasBottom = canvas.offsetTop + canvas.offsetHeight; + shortcuts.style.top = (canvasBottom + 8) + 'px'; + } + function sendRdpReset() { if (ws && ws.readyState === WebSocket.OPEN && token) { ws.send(JSON.stringify({ cmd: 'rdp_reset', token })); @@ -1290,14 +1507,20 @@ inline std::string GetWebPageHTML() { controlEnabled = !controlEnabled; // Update floating toolbar buttons const btnMouse = document.getElementById('btn-mouse'); - const btnKeyboard = document.getElementById('btn-keyboard'); btnMouse.classList.toggle('active', controlEnabled); - btnKeyboard.disabled = !controlEnabled; // Update top toolbar buttons (sync state) const btnMouseBar = document.getElementById('btn-mouse-bar'); - const btnKeyboardBar = document.getElementById('btn-keyboard-bar'); btnMouseBar.classList.toggle('active', controlEnabled); - btnKeyboardBar.disabled = !controlEnabled; + // Update quick controls buttons + const qcMouse = document.getElementById('qc-mouse'); + if (qcMouse) qcMouse.classList.toggle('active', controlEnabled); + // Desktop only: keyboard requires mouse control enabled + if (!isTouchDevice) { + const btnKeyboard = document.getElementById('btn-keyboard'); + const btnKeyboardBar = document.getElementById('btn-keyboard-bar'); + if (btnKeyboard) btnKeyboard.disabled = !controlEnabled; + if (btnKeyboardBar) btnKeyboardBar.disabled = !controlEnabled; + } // Cursor handling const canvas = document.getElementById('screen-canvas'); const cursorOverlay = document.getElementById('cursor-overlay'); @@ -1314,15 +1537,14 @@ inline std::string GetWebPageHTML() { const canvas = document.getElementById('screen-canvas'); const cursorOverlay = document.getElementById('cursor-overlay'); - // Get canvas base rect (without transform, use container) + // Get canvas base position (without transform, use container + offset) + // offsetLeft/offsetTop are layout positions, unaffected by CSS transform const container = document.querySelector('.canvas-container'); const containerRect = container.getBoundingClientRect(); - - // Calculate canvas position within container (centered) const canvasDisplayWidth = canvas.offsetWidth; const canvasDisplayHeight = canvas.offsetHeight; - const canvasLeft = containerRect.left + (containerRect.width - canvasDisplayWidth) / 2; - const canvasTop = containerRect.top + (containerRect.height - canvasDisplayHeight) / 2; + const canvasLeft = containerRect.left + canvas.offsetLeft; + const canvasTop = containerRect.top + canvas.offsetTop; // Convert canvas coords to position on unzoomed canvas // Map [0, width-1] to [0, 1] for proper edge-to-edge display @@ -1354,13 +1576,13 @@ inline std::string GetWebPageHTML() { // Used to recalculate cursor position after zoom/pan function screenToCanvas(screenX, screenY) { const canvas = document.getElementById('screen-canvas'); + // Get canvas base position (without transform, use container + offset) const container = document.querySelector('.canvas-container'); const containerRect = container.getBoundingClientRect(); - const canvasDisplayWidth = canvas.offsetWidth; const canvasDisplayHeight = canvas.offsetHeight; - const canvasLeft = containerRect.left + (containerRect.width - canvasDisplayWidth) / 2; - const canvasTop = containerRect.top + (containerRect.height - canvasDisplayHeight) / 2; + const canvasLeft = containerRect.left + canvas.offsetLeft; + const canvasTop = containerRect.top + canvas.offsetTop; // Reverse the transform chain const scaledX = screenX - canvasLeft; @@ -1937,10 +2159,49 @@ inline std::string GetWebPageHTML() { touchState.touchCount = e.touches.length; }, { passive: false }); - function toggleKeyboard() { - mobileKeyboard.focus(); + let lastKeyboardBtnTouch = 0; // Timestamp of last keyboard button touch + let wasKeyboardFocused = false; // Capture focus state at touchstart (before blur) + + function updateKeyboardButtons(active) { + // Update all keyboard buttons across different toolbars + const qcKeyboard = document.getElementById('qc-keyboard'); + const btnKeyboard = document.getElementById('btn-keyboard'); + const btnKeyboardBar = document.getElementById('btn-keyboard-bar'); + if (qcKeyboard) qcKeyboard.classList.toggle('active', active); + if (btnKeyboard) btnKeyboard.classList.toggle('active', active); + if (btnKeyboardBar) btnKeyboardBar.classList.toggle('active', active); + // Show/hide input shortcuts based on keyboard state (portrait only) + const shortcuts = document.getElementById('input-shortcuts'); + if (shortcuts && !isLandscape()) { + shortcuts.classList.toggle('visible', active); + if (active) updateInputShortcutsPosition(); + } } + function toggleKeyboard() { + // Use focus state captured at touchstart (more reliable than button's active class) + const isOpen = wasKeyboardFocused; + wasKeyboardFocused = false; // Reset + + if (isOpen) { + mobileKeyboard.blur(); + updateKeyboardButtons(false); + } else { + mobileKeyboard.focus(); + updateKeyboardButtons(true); + } + } + + mobileKeyboard.addEventListener('focus', function() { + updateKeyboardButtons(true); + }); + + mobileKeyboard.addEventListener('blur', function() { + // Skip if keyboard button was touched within last 300ms + if (Date.now() - lastKeyboardBtnTouch < 300) return; + updateKeyboardButtons(false); + }); + function sendRightClick() { // Use cursor position (canvas coordinates), not touch position (screen coordinates) const x = Math.round(cursorState.x); @@ -2029,9 +2290,32 @@ inline std::string GetWebPageHTML() { mobileKeyboard.addEventListener('input', function(e) { const char = e.data; if (char) { - const keyCode = char.toUpperCase().charCodeAt(0); + // Check if character needs Shift key + const isUpperCase = char >= 'A' && char <= 'Z'; + const shiftSymbols = '~!@#$%^&*()_+{}|:"<>?'; + const needsShift = isUpperCase || shiftSymbols.includes(char); + + // Map symbols to their base keys + const symbolMap = { + '~': 192, '!': 49, '@': 50, '#': 51, '$': 52, '%': 53, + '^': 54, '&': 55, '*': 56, '(': 57, ')': 48, '_': 189, + '+': 187, '{': 219, '}': 221, '|': 220, ':': 186, + '"': 222, '<': 188, '>': 190, '?': 191 + }; + + let keyCode; + if (symbolMap[char]) { + keyCode = symbolMap[char]; + } else { + keyCode = char.toUpperCase().charCodeAt(0); + } + + // Send Shift down if needed + if (needsShift) sendKey(16, true); // VK_SHIFT = 16 sendKey(keyCode, true); sendKey(keyCode, false); + // Send Shift up if needed + if (needsShift) sendKey(16, false); } mobileKeyboard.value = ''; }); @@ -2052,16 +2336,24 @@ inline std::string GetWebPageHTML() { function disconnect() { // Reset control mode controlEnabled = false; + // Reset keyboard state (blur event will update button state) + mobileKeyboard.blur(); // Reset floating toolbar buttons const btnMouse = document.getElementById('btn-mouse'); - const btnKeyboard = document.getElementById('btn-keyboard'); if (btnMouse) btnMouse.classList.remove('active'); - if (btnKeyboard) btnKeyboard.disabled = true; // Reset top toolbar buttons const btnMouseBar = document.getElementById('btn-mouse-bar'); - const btnKeyboardBar = document.getElementById('btn-keyboard-bar'); if (btnMouseBar) btnMouseBar.classList.remove('active'); - if (btnKeyboardBar) btnKeyboardBar.disabled = true; + // Desktop only: disable keyboard button + if (!isTouchDevice) { + const btnKeyboard = document.getElementById('btn-keyboard'); + const btnKeyboardBar = document.getElementById('btn-keyboard-bar'); + if (btnKeyboard) btnKeyboard.disabled = true; + if (btnKeyboardBar) btnKeyboardBar.disabled = true; + } + // Reset quick control buttons + const qcMouse = document.getElementById('qc-mouse'); + if (qcMouse) qcMouse.classList.remove('active'); document.getElementById('screen-canvas').style.cursor = 'default'; document.getElementById('cursor-overlay').classList.remove('active'); @@ -2121,11 +2413,89 @@ inline std::string GetWebPageHTML() { }); window.onload = function() { + // Initialize cached DOM elements + initUIElements(); + const compat = checkWebCodecs(); if (!compat.supported) { document.body.insertAdjacentHTML('afterbegin', '
Warning: Your browser may not support H264 decoding.
'); } + // On touch devices: setup UI based on orientation and fullscreen state + if (isTouchDevice) { + // Initial UI setup + updateUIForOrientation(); + + // Touch devices: keyboard is independent from mouse, enable keyboard buttons + const btnKeyboard = document.getElementById('btn-keyboard'); + const btnKeyboardBar = document.getElementById('btn-keyboard-bar'); + if (btnKeyboard) btnKeyboard.disabled = false; + if (btnKeyboardBar) btnKeyboardBar.disabled = false; + + // Listen for orientation changes (with debounce) + let resizeTimer = null; + window.addEventListener('resize', function() { + if (resizeTimer) clearTimeout(resizeTimer); + resizeTimer = setTimeout(updateUIForOrientation, 100); + }); + + // Listen for fullscreen changes + document.addEventListener('fullscreenchange', updateUIForOrientation); + document.addEventListener('webkitfullscreenchange', updateUIForOrientation); + + // Input shortcut buttons event handlers + // Shortcuts only visible when keyboard is open, so no extra check needed + function sendShortcutKey(keyCode, isDown) { + if (ws && ws.readyState === WebSocket.OPEN && token) { + ws.send(JSON.stringify({ cmd: 'key', token, keyCode, down: isDown, alt: false })); + } + } + const shortcutBtns = document.querySelectorAll('.shortcut-btn'); + shortcutBtns.forEach(function(btn) { + btn.addEventListener('touchstart', function(e) { + e.preventDefault(); + const keyCode = parseInt(btn.dataset.key); + const needShift = btn.dataset.shift === '1'; + if (needShift) sendShortcutKey(16, true); // Shift down + sendShortcutKey(keyCode, true); + sendShortcutKey(keyCode, false); + if (needShift) sendShortcutKey(16, false); // Shift up + }); + btn.addEventListener('click', function(e) { + e.preventDefault(); + // Only handle click for non-touch (mouse) + if (!('ontouchstart' in window)) { + const keyCode = parseInt(btn.dataset.key); + const needShift = btn.dataset.shift === '1'; + if (needShift) sendShortcutKey(16, true); + sendShortcutKey(keyCode, true); + sendShortcutKey(keyCode, false); + if (needShift) sendShortcutKey(16, false); + } + }); + }); + } else { + // Desktop: hide keyboard buttons (physical keyboard available) + const btnKeyboard = document.getElementById('btn-keyboard'); + const btnKeyboardBar = document.getElementById('btn-keyboard-bar'); + if (btnKeyboard) btnKeyboard.classList.add('ui-hidden'); + if (btnKeyboardBar) btnKeyboardBar.classList.add('ui-hidden'); + } + // Keyboard buttons: capture focus state and prevent blur from updating button state + function bindKeyboardBtnEvents(btn) { + if (!btn) return; + btn.addEventListener('touchstart', function() { + lastKeyboardBtnTouch = Date.now(); + wasKeyboardFocused = (document.activeElement === mobileKeyboard); + }, { passive: true }); + btn.addEventListener('mousedown', function() { + lastKeyboardBtnTouch = Date.now(); + wasKeyboardFocused = (document.activeElement === mobileKeyboard); + }); + } + bindKeyboardBtnEvents(document.getElementById('qc-keyboard')); + bindKeyboardBtnEvents(document.getElementById('btn-keyboard')); + bindKeyboardBtnEvents(document.getElementById('btn-keyboard-bar')); // Restore token from sessionStorage token = sessionStorage.getItem('token'); connectWebSocket();