diff --git a/flutter/assets/kb_layout_iso.svg b/flutter/assets/kb_layout_iso.svg
new file mode 100644
index 000000000..69f0c96cb
--- /dev/null
+++ b/flutter/assets/kb_layout_iso.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/kb_layout_not_iso.svg b/flutter/assets/kb_layout_not_iso.svg
new file mode 100644
index 000000000..09a055be3
--- /dev/null
+++ b/flutter/assets/kb_layout_not_iso.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart
index e16a61890..b9f25e0dd 100644
--- a/flutter/lib/desktop/pages/remote_page.dart
+++ b/flutter/lib/desktop/pages/remote_page.dart
@@ -23,6 +23,7 @@ import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
import '../widgets/remote_menubar.dart';
+import '../widgets/kb_layout_type_chooser.dart';
bool _isCustomCursorInited = false;
final SimpleWrapper _firstEnterImage = SimpleWrapper(false);
@@ -95,6 +96,10 @@ class _RemotePageState extends State
_initStates(widget.id);
_ffi = FFI();
Get.put(_ffi, tag: widget.id);
+ _ffi.imageModel.addCallbackOnFirstImage((String peerId) {
+ showKBLayoutTypeChooserIfNeeded(
+ _ffi.ffiModel.pi.platform, _ffi.dialogManager);
+ });
_ffi.start(widget.id);
WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
new file mode 100644
index 000000000..6601160a7
--- /dev/null
+++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
@@ -0,0 +1,224 @@
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:flutter_hbb/models/platform_model.dart';
+
+import '../../common.dart';
+
+typedef KBChoosedCallback = Future Function(String);
+
+const double _kImageMarginVertical = 6.0;
+const double _kImageMarginHorizental = 10.0;
+const double _kImageBoarderWidth = 4.0;
+const double _kImagePaddingWidth = 4.0;
+const Color _kImageBorderColor = Color.fromARGB(125, 202, 247, 2);
+const double _kBorderRadius = 6.0;
+const String _kKBLayoutTypeISO = 'ISO';
+const String _kKBLayoutTypeNotISO = 'Not ISO';
+
+const _kKBLayoutImageMap = {
+ _kKBLayoutTypeISO: 'kb_layout_iso',
+ _kKBLayoutTypeNotISO: 'kb_layout_not_iso',
+};
+
+class _KBImage extends StatelessWidget {
+ final String kbLayoutType;
+ final double imageWidth;
+ final RxString choosedType;
+ const _KBImage({
+ Key? key,
+ required this.kbLayoutType,
+ required this.imageWidth,
+ required this.choosedType,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Obx(() {
+ return Container(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(_kBorderRadius),
+ border: Border.all(
+ color: choosedType.value == kbLayoutType
+ ? _kImageBorderColor
+ : Colors.transparent,
+ width: _kImageBoarderWidth,
+ ),
+ ),
+ margin: EdgeInsets.symmetric(
+ horizontal: _kImageMarginHorizental,
+ vertical: _kImageMarginVertical,
+ ),
+ padding: EdgeInsets.all(_kImagePaddingWidth),
+ child: SvgPicture.asset(
+ 'assets/${_kKBLayoutImageMap[kbLayoutType] ?? ""}.svg',
+ width: imageWidth -
+ _kImageMarginHorizental * 2 -
+ _kImagePaddingWidth * 2 -
+ _kImageBoarderWidth * 2,
+ ),
+ );
+ });
+ }
+}
+
+class _KBChooser extends StatelessWidget {
+ final String kbLayoutType;
+ final double imageWidth;
+ final RxString choosedType;
+ final KBChoosedCallback cb;
+ const _KBChooser({
+ Key? key,
+ required this.kbLayoutType,
+ required this.imageWidth,
+ required this.choosedType,
+ required this.cb,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ onChanged(String? v) async {
+ if (v != null) {
+ if (await cb(v)) {
+ choosedType.value = v;
+ }
+ }
+ }
+
+ return Column(
+ children: [
+ TextButton(
+ onPressed: () {
+ onChanged(kbLayoutType);
+ },
+ child: _KBImage(
+ kbLayoutType: kbLayoutType,
+ imageWidth: imageWidth,
+ choosedType: choosedType,
+ ),
+ style: TextButton.styleFrom(padding: EdgeInsets.zero),
+ ),
+ TextButton(
+ child: Row(
+ children: [
+ Obx(() => Radio(
+ splashRadius: 0,
+ value: kbLayoutType,
+ groupValue: choosedType.value,
+ onChanged: onChanged,
+ )),
+ Text(kbLayoutType),
+ ],
+ ),
+ onPressed: () {
+ onChanged(kbLayoutType);
+ },
+ ),
+ ],
+ );
+ }
+}
+
+class KBLayoutTypeChooser extends StatelessWidget {
+ final RxString choosedType;
+ final double width;
+ final double height;
+ final double dividerWidth;
+ final KBChoosedCallback cb;
+ KBLayoutTypeChooser({
+ Key? key,
+ required this.choosedType,
+ required this.width,
+ required this.height,
+ required this.dividerWidth,
+ required this.cb,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ final imageWidth = width / 2 - dividerWidth;
+ return SizedBox(
+ width: width,
+ height: height,
+ child: Center(
+ child: Row(
+ children: [
+ _KBChooser(
+ kbLayoutType: _kKBLayoutTypeISO,
+ imageWidth: imageWidth,
+ choosedType: choosedType,
+ cb: cb,
+ ),
+ VerticalDivider(
+ width: dividerWidth * 2,
+ ),
+ _KBChooser(
+ kbLayoutType: _kKBLayoutTypeNotISO,
+ imageWidth: imageWidth,
+ choosedType: choosedType,
+ cb: cb,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+RxString KBLayoutType = ''.obs;
+
+String getLocalPlatformForKBLayoutType(String peerPlatform) {
+ String localPlatform = '';
+ if (peerPlatform != 'Mac OS') {
+ return localPlatform;
+ }
+
+ if (Platform.isWindows) {
+ localPlatform = 'Windows';
+ } else if (Platform.isLinux) {
+ localPlatform = 'Linux';
+ }
+ // to-do: web desktop support ?
+ return localPlatform;
+}
+
+showKBLayoutTypeChooserIfNeeded(
+ String peerPlatform,
+ OverlayDialogManager dialogManager,
+) async {
+ final localPlatform = getLocalPlatformForKBLayoutType(peerPlatform);
+ if (localPlatform == '') {
+ return;
+ }
+ KBLayoutType.value = bind.getLocalKbLayoutType();
+ if (KBLayoutType.value == _kKBLayoutTypeISO ||
+ KBLayoutType.value == _kKBLayoutTypeNotISO) {
+ return;
+ }
+ showKBLayoutTypeChooser(localPlatform, dialogManager);
+}
+
+showKBLayoutTypeChooser(
+ String localPlatform,
+ OverlayDialogManager dialogManager,
+) {
+ dialogManager.show((setState, close) {
+ return CustomAlertDialog(
+ title:
+ Text('${translate('Select local keyboard type')} ($localPlatform)'),
+ content: KBLayoutTypeChooser(
+ choosedType: KBLayoutType,
+ width: 360,
+ height: 200,
+ dividerWidth: 4.0,
+ cb: (String v) async {
+ await bind.setLocalKbLayoutType(kbLayoutType: v);
+ KBLayoutType.value = bind.getLocalKbLayoutType();
+ return v == KBLayoutType.value;
+ }),
+ actions: [msgBoxButton(translate('Close'), close)],
+ onCancel: close,
+ );
+ });
+}
diff --git a/flutter/lib/desktop/widgets/login.dart b/flutter/lib/desktop/widgets/login.dart
index 053653ab3..0736f0864 100644
--- a/flutter/lib/desktop/widgets/login.dart
+++ b/flutter/lib/desktop/widgets/login.dart
@@ -9,7 +9,7 @@ import 'package:url_launcher/url_launcher.dart';
import '../../common.dart';
-final kMidButtonPadding = const EdgeInsets.fromLTRB(15, 0, 15, 0);
+final _kMidButtonPadding = const EdgeInsets.fromLTRB(15, 0, 15, 0);
class _IconOP extends StatelessWidget {
final String icon;
@@ -53,7 +53,7 @@ class ButtonOP extends StatelessWidget {
Expanded(
child: Container(
height: height,
- padding: kMidButtonPadding,
+ padding: _kMidButtonPadding,
child: Obx(() => ElevatedButton(
style: ElevatedButton.styleFrom(
primary: curOP.value.isEmpty || curOP.value == op
@@ -315,7 +315,7 @@ class LoginWidgetUserPass extends StatelessWidget {
height: 8.0,
),
Container(
- padding: kMidButtonPadding,
+ padding: _kMidButtonPadding,
child: Row(
children: [
ConstrainedBox(
@@ -343,7 +343,7 @@ class LoginWidgetUserPass extends StatelessWidget {
height: 8.0,
),
Container(
- padding: kMidButtonPadding,
+ padding: _kMidButtonPadding,
child: Row(
children: [
ConstrainedBox(
@@ -377,7 +377,7 @@ class LoginWidgetUserPass extends StatelessWidget {
Expanded(
child: Container(
height: 38,
- padding: kMidButtonPadding,
+ padding: _kMidButtonPadding,
child: Obx(() => ElevatedButton(
style: curOP.value.isEmpty || curOP.value == 'rustdesk'
? null
diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart
index b60fa7128..0cd4ee00f 100644
--- a/flutter/lib/desktop/widgets/remote_menubar.dart
+++ b/flutter/lib/desktop/widgets/remote_menubar.dart
@@ -22,6 +22,7 @@ import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
import './popup_menu.dart';
import './material_mod_popup_menu.dart' as mod_menu;
+import './kb_layout_type_chooser.dart';
class MenubarState {
final kStoreKey = 'remoteMenubarState';
@@ -1187,7 +1188,7 @@ class _RemoteMenubarState extends State {
}
List> _getKeyboardMenu() {
- final keyboardMenu = [
+ final List> keyboardMenu = [
MenuEntryRadios(
text: translate('Ratio'),
optionsGetter: () => [
@@ -1203,7 +1204,55 @@ class _RemoteMenubarState extends State {
},
)
];
-
+ final localPlatform =
+ getLocalPlatformForKBLayoutType(widget.ffi.ffiModel.pi.platform);
+ if (localPlatform != '') {
+ keyboardMenu.add(MenuEntryDivider());
+ keyboardMenu.add(
+ MenuEntryButton(
+ childBuilder: (TextStyle? style) => Container(
+ alignment: AlignmentDirectional.center,
+ height: _MenubarTheme.height,
+ child: Row(
+ children: [
+ Obx(() => RichText(
+ text: TextSpan(
+ text: '${translate('Local keyboard type')}: ',
+ style: DefaultTextStyle.of(context).style,
+ children: [
+ TextSpan(
+ text: KBLayoutType.value,
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ ],
+ ),
+ )),
+ Expanded(
+ child: Align(
+ alignment: Alignment.centerRight,
+ child: Transform.scale(
+ scale: 0.8,
+ child: IconButton(
+ padding: EdgeInsets.zero,
+ icon: const Icon(Icons.settings),
+ onPressed: () {
+ if (Navigator.canPop(context)) {
+ Navigator.pop(context);
+ }
+ showKBLayoutTypeChooser(
+ localPlatform, widget.ffi.dialogManager);
+ },
+ ),
+ ),
+ ))
+ ],
+ )),
+ proc: () {},
+ padding: EdgeInsets.zero,
+ dismissOnClicked: false,
+ ),
+ );
+ }
return keyboardMenu;
}
diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart
index 3659e8d58..63062a928 100644
--- a/flutter/lib/models/model.dart
+++ b/flutter/lib/models/model.dart
@@ -381,12 +381,22 @@ class ImageModel with ChangeNotifier {
WeakReference parent;
+ final List _callbacksOnFirstImage = [];
+
ImageModel(this.parent);
+ addCallbackOnFirstImage(Function(String) cb) =>
+ _callbacksOnFirstImage.add(cb);
+
onRgba(Uint8List rgba) {
if (_waitForImage[id]!) {
_waitForImage[id] = false;
parent.target?.dialogManager.dismissAll();
+ if (isDesktop) {
+ for (final cb in _callbacksOnFirstImage) {
+ cb(id);
+ }
+ }
}
final pid = parent.target?.id;
ui.decodeImageFromPixels(
diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs
index e2592bbea..4bc33cda5 100644
--- a/libs/hbb_common/src/config.rs
+++ b/libs/hbb_common/src/config.rs
@@ -998,6 +998,8 @@ pub struct LocalConfig {
#[serde(default)]
remote_id: String, // latest used one
#[serde(default)]
+ kb_layout_type: String,
+ #[serde(default)]
size: Size,
#[serde(default)]
pub fav: Vec,
@@ -1017,6 +1019,16 @@ impl LocalConfig {
Config::store_(self, "_local");
}
+ pub fn get_kb_layout_type() -> String {
+ LOCAL_CONFIG.read().unwrap().kb_layout_type.clone()
+ }
+
+ pub fn set_kb_layout_type(kb_layout_type: String) {
+ let mut config = LOCAL_CONFIG.write().unwrap();
+ config.kb_layout_type = kb_layout_type;
+ config.store();
+ }
+
pub fn get_size() -> Size {
LOCAL_CONFIG.read().unwrap().size
}
diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs
index dc9d7a04a..3be0c9fed 100644
--- a/src/flutter_ffi.rs
+++ b/src/flutter_ffi.rs
@@ -181,6 +181,14 @@ pub fn set_local_flutter_config(k: String, v: String) {
ui_interface::set_local_flutter_config(k, v);
}
+pub fn get_local_kb_layout_type() -> SyncReturn {
+ SyncReturn(ui_interface::get_kb_layout_type())
+}
+
+pub fn set_local_kb_layout_type(kb_layout_type: String) {
+ ui_interface::set_kb_layout_type(kb_layout_type)
+}
+
pub fn session_get_view_style(id: String) -> Option {
if let Some(session) = SESSIONS.read().unwrap().get(&id) {
Some(session.get_view_style())
diff --git a/src/keyboard.rs b/src/keyboard.rs
index 4b42bdf5d..d22573fbc 100644
--- a/src/keyboard.rs
+++ b/src/keyboard.rs
@@ -6,7 +6,7 @@ use crate::flutter::FlutterHandler;
#[cfg(not(feature = "flutter"))]
use crate::ui::remote::SciterHandler;
use crate::ui_session_interface::Session;
-use hbb_common::{log, message_proto::*};
+use hbb_common::{log, message_proto::*, config::LocalConfig};
use rdev::{Event, EventType, Key};
#[cfg(any(target_os = "windows", target_os = "macos"))]
use std::sync::atomic::{AtomicBool, Ordering};
@@ -620,7 +620,13 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option event.scan_code,
- "macos" => rdev::win_scancode_to_macos_code(event.scan_code)?,
+ "macos" => {
+ if LocalConfig::get_kb_layout_type() == "ISO" {
+ rdev::win_scancode_to_macos_iso_code(event.scan_code)?
+ } else {
+ rdev::win_scancode_to_macos_code(event.scan_code)?
+ }
+ },
_ => rdev::win_scancode_to_linux_code(event.scan_code)?,
};
#[cfg(target_os = "macos")]
@@ -632,7 +638,13 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option rdev::linux_code_to_win_scancode(event.code as _)?,
- "macos" => rdev::linux_code_to_macos_code(event.code as _)?,
+ "macos" => {
+ if LocalConfig::get_kb_layout_type() == "ISO" {
+ rdev::linux_code_to_macos_iso_code(event.scan_code)?
+ } else {
+ rdev::linux_code_to_macos_code(event.code as _)?
+ }
+ },
_ => event.code as _,
};
#[cfg(any(target_os = "android", target_os = "ios"))]
diff --git a/src/lang/cn.rs b/src/lang/cn.rs
index be0d7803e..5d04268b0 100644
--- a/src/lang/cn.rs
+++ b/src/lang/cn.rs
@@ -407,5 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Group", "小组"),
("Search", "搜索"),
("Closed manually by the web console", "被web控制台手动关闭"),
+ ("Local keyboard type", "本地键盘类型"),
+ ("Select local keyboard type", "请选择本地键盘类型"),
].iter().cloned().collect();
}
diff --git a/src/lang/tw.rs b/src/lang/tw.rs
index 6eef3656c..301384ea3 100644
--- a/src/lang/tw.rs
+++ b/src/lang/tw.rs
@@ -406,5 +406,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Group", "小組"),
("Search", "搜索"),
("Closed manually by the web console", "被web控制台手動關閉"),
+ ("Local keyboard type", "本地鍵盤類型"),
+ ("Select local keyboard type", "請選擇本地鍵盤類型"),
].iter().cloned().collect();
}
diff --git a/src/ui_interface.rs b/src/ui_interface.rs
index 604d2e222..3e4cd681f 100644
--- a/src/ui_interface.rs
+++ b/src/ui_interface.rs
@@ -202,6 +202,16 @@ pub fn set_local_flutter_config(key: String, value: String) {
LocalConfig::set_flutter_config(key, value);
}
+#[inline]
+pub fn get_kb_layout_type() -> String {
+ LocalConfig::get_kb_layout_type()
+}
+
+#[inline]
+pub fn set_kb_layout_type(kb_layout_type: String) {
+ LocalConfig::set_kb_layout_type(kb_layout_type);
+}
+
#[inline]
pub fn peer_has_password(id: String) -> bool {
!PeerConfig::load(&id).password.is_empty()