fix(vnc): Tastatur ueber echtes RN-TextInput statt WebView-Hidden-Input
Der versteckte WebView-Input oeffnete die Android-Software-Tastatur unzuverlaessig. Jetzt haelt VncTile ein echtes RN-<TextInput> (keyboardType=visible-password), der ⌨-Button fokussiert es. Getippte Zeichen gehen per injectJavaScript an window.ariaVncKey.char/keysym → rfb.sendKey. Prefix-Diff fuer Druckbares, onKeyPress fuer Backspace/Enter. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -77,20 +77,13 @@ export const NOVNC_HTML = `<!doctype html><html><head><meta charset="utf-8">
|
|||||||
|
|
||||||
var msg=document.getElementById('msg');
|
var msg=document.getElementById('msg');
|
||||||
|
|
||||||
// (Fast) unsichtbares Eingabefeld → Software-Tastatur des Handys tippt in die
|
// Tastatur laeuft NICHT mehr ueber ein verstecktes WebView-Feld (Android
|
||||||
// VM. WICHTIG: on-screen (nicht off-screen), sonst oeffnet Android die Tastatur
|
// oeffnet die Software-Tastatur dafuer unzuverlaessig). Stattdessen haelt die
|
||||||
// oft nicht bzw. liefert keine Events. opacity:0 + pointer-events:none = sichtbar
|
// App ein echtes RN-<TextInput> und ruft window.ariaVncKey.* per
|
||||||
// fuer den Fokus, aber unsichtbar und klaut keine VM-Touches. font-size:16px
|
// injectJavaScript auf → wird unten (nach RFB-Init) definiert.
|
||||||
// verhindert Auto-Zoom.
|
// cp<0x100 → Keysym == Codepoint (Latin-1)
|
||||||
var kbd=document.createElement('input');
|
// sonst → X11-Unicode-Keysym 0x01000000+cp
|
||||||
kbd.setAttribute('autocomplete','off'); kbd.setAttribute('autocorrect','off');
|
function cpToKeysym(cp){ return cp < 0x100 ? cp : 0x01000000 + cp; }
|
||||||
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};
|
|
||||||
|
|
||||||
import('https://cdn.jsdelivr.net/npm/@novnc/novnc@1.4.0/core/rfb.js').then(function(mod){
|
import('https://cdn.jsdelivr.net/npm/@novnc/novnc@1.4.0/core/rfb.js').then(function(mod){
|
||||||
var RFB = mod.default;
|
var RFB = mod.default;
|
||||||
@@ -105,30 +98,19 @@ export const NOVNC_HTML = `<!doctype html><html><head><meta charset="utf-8">
|
|||||||
});
|
});
|
||||||
window.__rfb = rfb;
|
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(_){} }
|
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){
|
// Empfaenger-API: die App (RN-<TextInput>) ruft das per injectJavaScript.
|
||||||
if(SPECIAL[e.key]!==undefined){ tap(SPECIAL[e.key], e.code); e.preventDefault(); }
|
// char(cp) druckbares Zeichen (Codepoint)
|
||||||
});
|
// keysym(ks) Sondertaste als fertiges X11-Keysym (Backspace/Enter/…)
|
||||||
// Druckbare Zeichen + Editieren: beforeinput ist auf Android robuster als
|
window.ariaVncKey = {
|
||||||
// ein input-Diff. preventDefault haelt das Feld leer → kein Doppel-Senden.
|
char: function(cp){ tap(cpToKeysym(cp)); },
|
||||||
kbd.addEventListener('beforeinput', function(e){
|
keysym: function(ks){ tap(ks); }
|
||||||
var t=e.inputType||'';
|
};
|
||||||
if(t==='insertText' && e.data){ for(var i=0;i<e.data.length;i++) tap(e.data.charCodeAt(i)); e.preventDefault(); }
|
|
||||||
else if(t.indexOf('insertLineBreak')>=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<kbd.value.length;i++){ tap(kbd.value.charCodeAt(i)); } kbd.value=''; }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Steuerungs-API fuer die App (per injectJavaScript).
|
// Steuerungs-API fuer die App (per injectJavaScript).
|
||||||
window.ariaVncCtl = {
|
window.ariaVncCtl = {
|
||||||
focusKeyboard: function(){ try{ kbd.value=''; kbd.focus(); }catch(_){} },
|
|
||||||
blurKeyboard: function(){ try{ kbd.blur(); }catch(_){} },
|
|
||||||
cad: function(){ try{ rfb.sendCtrlAltDel(); }catch(_){} },
|
cad: function(){ try{ rfb.sendCtrlAltDel(); }catch(_){} },
|
||||||
toggleFit: function(){ fit=!fit; rfb.scaleViewport=fit; rfb.clipViewport=!fit; post({event:'vnc_fit', fit:fit}); }
|
toggleFit: function(){ fit=!fit; rfb.scaleViewport=fit; rfb.clipViewport=!fit; post({event:'vnc_fit', fit:fit}); }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
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 { WebView, WebViewMessageEvent } from 'react-native-webview';
|
||||||
import desktop from '../../services/desktop';
|
import desktop from '../../services/desktop';
|
||||||
import { NOVNC_HTML } from '../assets/novncHtml';
|
import { NOVNC_HTML } from '../assets/novncHtml';
|
||||||
@@ -19,9 +19,15 @@ interface Props {
|
|||||||
port?: number; // VNC-Port der zu verbindenden VM (Default 5901 = Display :1)
|
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<Props> = ({ projectId, focused, port = 5901 }) => {
|
const VncTile: React.FC<Props> = ({ projectId, focused, port = 5901 }) => {
|
||||||
const webRef = useRef<WebView>(null);
|
const webRef = useRef<WebView>(null);
|
||||||
|
const kbdRef = useRef<TextInput>(null);
|
||||||
|
const bufRef = useRef(''); // Spiegel des TextInput-Textes
|
||||||
const [status, setStatus] = useState<'idle' | 'connecting' | 'connected' | 'disconnected'>('idle');
|
const [status, setStatus] = useState<'idle' | 'connecting' | 'connected' | 'disconnected'>('idle');
|
||||||
|
const [kbdOn, setKbdOn] = useState(false);
|
||||||
const unsubDataRef = useRef<null | (() => void)>(null);
|
const unsubDataRef = useRef<null | (() => void)>(null);
|
||||||
|
|
||||||
const teardown = useCallback(() => {
|
const teardown = useCallback(() => {
|
||||||
@@ -38,6 +44,40 @@ const VncTile: React.FC<Props> = ({ projectId, focused, port = 5901 }) => {
|
|||||||
webRef.current?.injectJavaScript(`window.ariaVncCtl && window.ariaVncCtl.${fn}(); true;`);
|
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-<TextInput> 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<TextInputChangeEventData>) => {
|
||||||
|
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<TextInputKeyPressEventData>) => {
|
||||||
|
const k = e.nativeEvent.key;
|
||||||
|
if (k === 'Backspace') sendKeysym(KEYSYM.Backspace);
|
||||||
|
else if (k === 'Enter') sendKeysym(KEYSYM.Enter);
|
||||||
|
}, [sendKeysym]);
|
||||||
|
|
||||||
const onMessage = useCallback((e: WebViewMessageEvent) => {
|
const onMessage = useCallback((e: WebViewMessageEvent) => {
|
||||||
let m: any;
|
let m: any;
|
||||||
try { m = JSON.parse(e.nativeEvent.data); } catch { return; }
|
try { m = JSON.parse(e.nativeEvent.data); } catch { return; }
|
||||||
@@ -84,10 +124,29 @@ const VncTile: React.FC<Props> = ({ projectId, focused, port = 5901 }) => {
|
|||||||
keyboardDisplayRequiresUserAction={false}
|
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. */}
|
||||||
|
<TextInput
|
||||||
|
ref={kbdRef}
|
||||||
|
style={styles.hiddenInput}
|
||||||
|
onChange={onKbdChange}
|
||||||
|
onKeyPress={onKbdKeyPress}
|
||||||
|
onBlur={() => setKbdOn(false)}
|
||||||
|
keyboardType="visible-password"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
spellCheck={false}
|
||||||
|
blurOnSubmit={false}
|
||||||
|
caretHidden
|
||||||
|
contextMenuHidden
|
||||||
|
multiline={false}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Steuerungs-Leiste — nur wenn verbunden */}
|
{/* Steuerungs-Leiste — nur wenn verbunden */}
|
||||||
{connected && (
|
{connected && (
|
||||||
<View style={styles.ctlBar}>
|
<View style={styles.ctlBar}>
|
||||||
<TouchableOpacity style={styles.ctlBtn} onPress={() => ctl('focusKeyboard')} activeOpacity={0.7}>
|
<TouchableOpacity style={[styles.ctlBtn, kbdOn && styles.ctlBtnOn]} onPress={toggleKbd} activeOpacity={0.7}>
|
||||||
<Text style={styles.ctlText}>⌨</Text>
|
<Text style={styles.ctlText}>⌨</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={styles.ctlBtn} onPress={() => ctl('cad')} activeOpacity={0.7}>
|
<TouchableOpacity style={styles.ctlBtn} onPress={() => ctl('cad')} activeOpacity={0.7}>
|
||||||
@@ -135,8 +194,11 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
|
ctlBtnOn: { backgroundColor: 'rgba(0,150,255,0.85)', borderColor: '#0096FF' },
|
||||||
ctlText: { color: '#E0E0F0', fontSize: 16, fontWeight: '700' },
|
ctlText: { color: '#E0E0F0', fontSize: 16, fontWeight: '700' },
|
||||||
ctlTextSmall: { color: '#E0E0F0', fontSize: 11, 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' },
|
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' },
|
overlayText: { color: '#9090B0', fontSize: 12, backgroundColor: 'rgba(0,0,0,0.6)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 10, overflow: 'hidden' },
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user