diff --git a/android/src/workspace/assets/novncHtml.ts b/android/src/workspace/assets/novncHtml.ts index 1e22338..9c26085 100644 --- a/android/src/workspace/assets/novncHtml.ts +++ b/android/src/workspace/assets/novncHtml.ts @@ -77,20 +77,13 @@ export const NOVNC_HTML = ` var msg=document.getElementById('msg'); - // (Fast) unsichtbares Eingabefeld → Software-Tastatur des Handys tippt in die - // VM. WICHTIG: on-screen (nicht off-screen), sonst oeffnet Android die Tastatur - // oft nicht bzw. liefert keine Events. opacity:0 + pointer-events:none = sichtbar - // fuer den Fokus, aber unsichtbar und klaut keine VM-Touches. font-size:16px - // verhindert Auto-Zoom. - var kbd=document.createElement('input'); - kbd.setAttribute('autocomplete','off'); kbd.setAttribute('autocorrect','off'); - kbd.setAttribute('autocapitalize','off'); kbd.spellcheck=false; - kbd.style.cssText='position:fixed;bottom:0;left:0;width:100%;height:1px;opacity:0;' - +'border:0;padding:0;margin:0;background:transparent;color:transparent;' - +'caret-color:transparent;font-size:16px;pointer-events:none;'; - document.body.appendChild(kbd); - var SPECIAL={Enter:0xff0d,Backspace:0xff08,Tab:0xff09,Escape:0xff1b,Delete:0xffff, - ArrowLeft:0xff51,ArrowUp:0xff52,ArrowRight:0xff53,ArrowDown:0xff54,Home:0xff50,End:0xff57}; + // Tastatur laeuft NICHT mehr ueber ein verstecktes WebView-Feld (Android + // oeffnet die Software-Tastatur dafuer unzuverlaessig). Stattdessen haelt die + // App ein echtes RN- und ruft window.ariaVncKey.* per + // injectJavaScript auf → wird unten (nach RFB-Init) definiert. + // cp<0x100 → Keysym == Codepoint (Latin-1) + // sonst → X11-Unicode-Keysym 0x01000000+cp + function cpToKeysym(cp){ return cp < 0x100 ? cp : 0x01000000 + cp; } import('https://cdn.jsdelivr.net/npm/@novnc/novnc@1.4.0/core/rfb.js').then(function(mod){ var RFB = mod.default; @@ -105,30 +98,19 @@ export const NOVNC_HTML = ` }); window.__rfb = rfb; - // Tasten aus dem versteckten Feld an die VM schicken. + // Down+Up einer Taste an die VM schicken. function tap(keysym, code){ try{ rfb.sendKey(keysym, code||null, true); rfb.sendKey(keysym, code||null, false); }catch(_){} } - // Sondertasten (feuern zuverlaessig als keydown). - kbd.addEventListener('keydown', function(e){ - if(SPECIAL[e.key]!==undefined){ tap(SPECIAL[e.key], e.code); e.preventDefault(); } - }); - // Druckbare Zeichen + Editieren: beforeinput ist auf Android robuster als - // ein input-Diff. preventDefault haelt das Feld leer → kein Doppel-Senden. - kbd.addEventListener('beforeinput', function(e){ - var t=e.inputType||''; - if(t==='insertText' && e.data){ for(var i=0;i=0 || t.indexOf('insertParagraph')>=0){ tap(0xff0d); e.preventDefault(); } - else if(t.indexOf('deleteContentBackward')>=0){ tap(0xff08); e.preventDefault(); } - kbd.value=''; - }); - // Fallback, falls beforeinput nicht unterstuetzt wird. - kbd.addEventListener('input', function(){ - if(kbd.value){ for(var i=0;i) ruft das per injectJavaScript. + // char(cp) druckbares Zeichen (Codepoint) + // keysym(ks) Sondertaste als fertiges X11-Keysym (Backspace/Enter/…) + window.ariaVncKey = { + char: function(cp){ tap(cpToKeysym(cp)); }, + keysym: function(ks){ tap(ks); } + }; // Steuerungs-API fuer die App (per injectJavaScript). window.ariaVncCtl = { - focusKeyboard: function(){ try{ kbd.value=''; kbd.focus(); }catch(_){} }, - blurKeyboard: function(){ try{ kbd.blur(); }catch(_){} }, cad: function(){ try{ rfb.sendCtrlAltDel(); }catch(_){} }, toggleFit: function(){ fit=!fit; rfb.scaleViewport=fit; rfb.clipViewport=!fit; post({event:'vnc_fit', fit:fit}); } }; diff --git a/android/src/workspace/tiles/VncTile.tsx b/android/src/workspace/tiles/VncTile.tsx index 2538b6f..fb42ad9 100644 --- a/android/src/workspace/tiles/VncTile.tsx +++ b/android/src/workspace/tiles/VncTile.tsx @@ -8,7 +8,7 @@ */ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { NativeSyntheticEvent, StyleSheet, Text, TextInput, TextInputChangeEventData, TextInputKeyPressEventData, TouchableOpacity, View } from 'react-native'; import { WebView, WebViewMessageEvent } from 'react-native-webview'; import desktop from '../../services/desktop'; import { NOVNC_HTML } from '../assets/novncHtml'; @@ -19,9 +19,15 @@ interface Props { port?: number; // VNC-Port der zu verbindenden VM (Default 5901 = Display :1) } +// X11-Keysyms fuer Sondertasten, die kein druckbares Zeichen liefern. +const KEYSYM = { Backspace: 0xff08, Enter: 0xff0d, Tab: 0xff09 }; + const VncTile: React.FC = ({ projectId, focused, port = 5901 }) => { const webRef = useRef(null); + const kbdRef = useRef(null); + const bufRef = useRef(''); // Spiegel des TextInput-Textes const [status, setStatus] = useState<'idle' | 'connecting' | 'connected' | 'disconnected'>('idle'); + const [kbdOn, setKbdOn] = useState(false); const unsubDataRef = useRef void)>(null); const teardown = useCallback(() => { @@ -38,6 +44,40 @@ const VncTile: React.FC = ({ projectId, focused, port = 5901 }) => { webRef.current?.injectJavaScript(`window.ariaVncCtl && window.ariaVncCtl.${fn}(); true;`); }, []); + const sendChar = useCallback((cp: number) => { + webRef.current?.injectJavaScript(`window.ariaVncKey && window.ariaVncKey.char(${cp}); true;`); + }, []); + const sendKeysym = useCallback((ks: number) => { + webRef.current?.injectJavaScript(`window.ariaVncKey && window.ariaVncKey.keysym(${ks}); true;`); + }, []); + + // Tastatur ein-/ausblenden: echtes RN- fokussieren → Android oeffnet + // die Software-Tastatur zuverlaessig (anders als ein verstecktes WebView-Feld). + const toggleKbd = useCallback(() => { + if (kbdOn) { kbdRef.current?.blur(); setKbdOn(false); } + else { setKbdOn(true); setTimeout(() => kbdRef.current?.focus(), 0); } + }, [kbdOn]); + + // Druckbare Zeichen: Prefix-Diff des (wachsenden) Feldes → nur neu Getipptes an + // die VM. Loeschungen kommen ueber onKeyPress(Backspace), daher hier nur Inserts. + const onKbdChange = useCallback((e: NativeSyntheticEvent) => { + const text = e.nativeEvent.text || ''; + const prev = bufRef.current; + let i = 0; + const min = Math.min(prev.length, text.length); + while (i < min && prev.charCodeAt(i) === text.charCodeAt(i)) i++; + for (const ch of text.slice(i)) { const cp = ch.codePointAt(0); if (cp) sendChar(cp); } + bufRef.current = text; + if (text.length > 200) { bufRef.current = ''; kbdRef.current?.setNativeProps({ text: '' }); } + }, [sendChar]); + + // Sondertasten: Backspace/Enter feuern auf Android zuverlaessig als keyPress. + const onKbdKeyPress = useCallback((e: NativeSyntheticEvent) => { + const k = e.nativeEvent.key; + if (k === 'Backspace') sendKeysym(KEYSYM.Backspace); + else if (k === 'Enter') sendKeysym(KEYSYM.Enter); + }, [sendKeysym]); + const onMessage = useCallback((e: WebViewMessageEvent) => { let m: any; try { m = JSON.parse(e.nativeEvent.data); } catch { return; } @@ -84,10 +124,29 @@ const VncTile: React.FC = ({ projectId, focused, port = 5901 }) => { keyboardDisplayRequiresUserAction={false} /> + {/* Verstecktes Eingabefeld: fokussiert → Android-Tastatur tippt in die VM. + keyboardType=visible-password schaltet Autokorrektur/Vorschlaege ab und + liefert saubere Einzelzeichen. Offscreen, aber fokussierbar. */} + setKbdOn(false)} + keyboardType="visible-password" + autoCapitalize="none" + autoCorrect={false} + spellCheck={false} + blurOnSubmit={false} + caretHidden + contextMenuHidden + multiline={false} + /> + {/* Steuerungs-Leiste — nur wenn verbunden */} {connected && ( - ctl('focusKeyboard')} activeOpacity={0.7}> + ctl('cad')} activeOpacity={0.7}> @@ -135,8 +194,11 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, + ctlBtnOn: { backgroundColor: 'rgba(0,150,255,0.85)', borderColor: '#0096FF' }, ctlText: { color: '#E0E0F0', fontSize: 16, fontWeight: '700' }, ctlTextSmall: { color: '#E0E0F0', fontSize: 11, fontWeight: '700' }, + // Fokussierbar (nicht display:none), aber aus dem Sichtfeld geschoben. + hiddenInput: { position: 'absolute', width: 1, height: 1, top: -100, left: -100, opacity: 0, padding: 0 }, overlay: { position: 'absolute', top: 12, left: 0, right: 0, alignItems: 'center' }, overlayText: { color: '#9090B0', fontSize: 12, backgroundColor: 'rgba(0,0,0,0.6)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 10, overflow: 'hidden' }, });