From 48e684335ee7da31b8fdd775b63dea6cb5686e81 Mon Sep 17 00:00:00 2001
From: fufesou <shuanglongchen@yeah.net>
Date: Tue, 27 Dec 2022 16:45:13 +0800
Subject: [PATCH 1/3] choose keyboard layout type, mid commit

Signed-off-by: fufesou <shuanglongchen@yeah.net>
---
 flutter/lib/desktop/pages/remote_page.dart    |   5 +
 .../widgets/kb_layout_type_chooser.dart       | 227 ++++++++++++++++++
 flutter/lib/desktop/widgets/login.dart        |  10 +-
 .../lib/desktop/widgets/remote_menubar.dart   |  53 +++-
 flutter/lib/models/model.dart                 |  10 +
 libs/hbb_common/src/config.rs                 |  10 +
 src/flutter_ffi.rs                            |   8 +
 src/keyboard.rs                               |  18 +-
 src/lang/cn.rs                                |   2 +
 src/lang/tw.rs                                |   2 +
 src/ui_interface.rs                           |  10 +
 11 files changed, 345 insertions(+), 10 deletions(-)
 create mode 100644 flutter/lib/desktop/widgets/kb_layout_type_chooser.dart

diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart
index dd569a110..5da5c066c 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<bool> _firstEnterImage = SimpleWrapper(false);
@@ -95,6 +96,10 @@ class _RemotePageState extends State<RemotePage>
     _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..35ab4c81e
--- /dev/null
+++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
@@ -0,0 +1,227 @@
+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 = bool 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) {
+    return Column(
+      children: [
+        TextButton(
+          onPressed: () {
+            choosedType.value = 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: (String? newValue) {
+                      if (newValue != null) {
+                        if (cb(newValue)) {
+                          choosedType.value = newValue;
+                        }
+                      }
+                    },
+                  )),
+              Text(kbLayoutType),
+            ],
+          ),
+          onPressed: () {
+            if (cb(kbLayoutType)) {
+              choosedType.value = 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 Container(
+      color: Colors.white,
+      child: 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) {
+            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<RemoteMenubar> {
   }
 
   List<MenuEntryBase<String>> _getKeyboardMenu() {
-    final keyboardMenu = [
+    final List<MenuEntryBase<String>> keyboardMenu = [
       MenuEntryRadios<String>(
         text: translate('Ratio'),
         optionsGetter: () => [
@@ -1203,7 +1204,55 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
         },
       )
     ];
-
+    final localPlatform =
+        getLocalPlatformForKBLayoutType(widget.ffi.ffiModel.pi.platform);
+    if (localPlatform != '') {
+      keyboardMenu.add(MenuEntryDivider());
+      keyboardMenu.add(
+        MenuEntryButton<String>(
+          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>[
+                            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<FFI> parent;
 
+  final List<Function(String)> _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..d86ac3463 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<String>,
@@ -1017,6 +1019,14 @@ 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) {
+        LOCAL_CONFIG.write().unwrap().kb_layout_type = kb_layout_type
+    }
+
     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<String> {
+    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<String> {
     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<KeyEv
     #[cfg(target_os = "windows")]
     let keycode = match peer.as_str() {
         "windows" => 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<KeyEv
     #[cfg(target_os = "linux")]
     let keycode = match peer.as_str() {
         "windows" => 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()

From ebdead8766b5ace96922c3a39bb57034f0d07c41 Mon Sep 17 00:00:00 2001
From: fufesou <shuanglongchen@yeah.net>
Date: Tue, 27 Dec 2022 16:46:56 +0800
Subject: [PATCH 2/3] add svg

Signed-off-by: fufesou <shuanglongchen@yeah.net>
---
 flutter/assets/kb_layout_iso.svg                        | 1 +
 flutter/assets/kb_layout_not_iso.svg                    | 1 +
 flutter/lib/desktop/widgets/kb_layout_type_chooser.dart | 4 ++--
 3 files changed, 4 insertions(+), 2 deletions(-)
 create mode 100644 flutter/assets/kb_layout_iso.svg
 create mode 100644 flutter/assets/kb_layout_not_iso.svg

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 @@
+<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261 253" width="261" height="253"><path fill="#0ff" d="m1 217c0-5.5 4.5-10 10-10h60c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-60c-5.5 0-10-4.5-10-10z"/><path fill="#487997" d="m89 216c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#c0ff00" d="m3 166c0-5.5 4.5-10 10-10h34c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-34c-5.5 0-10-4.5-10-10z"/><path fill="#00f" d="m63 166c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m140 215c0-5.5 4.5-10 10-10h26c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-26c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m190 215c0-5.5 4.5-10 10-10h26c5.5 0 10 4.5 10 10v28c0 5.5-4.5 10-10 10h-26c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m112 166c0-5.5 4.5-10 10-10h27c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-27c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m165 165c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m216 164c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m0 115c0-5.5 4.5-10 10-10h61c5.5 0 10 4.5 10 10v23c0 5.5-4.5 10-10 10h-61c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m90 116c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m139 116c0-5.5 4.5-10 10-10h27c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-27c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m191 115c0-5.5 4.5-10 10-10h23c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-23c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m2 62c0-5.5 4.5-10 10-10h50c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-50c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m79 62c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m131 64c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m182 63c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m0 10c0-5.5 4.5-10 10-10h28c5.5 0 10 4.5 10 10v27c0 5.5-4.5 10-10 10h-28c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m54 11c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m105 12c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m156 13c0-5.5 4.5-10 10-10h27c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-27c-5.5 0-10-4.5-10-10z"/><path  d="m16.7 171.9v1.9q-1.1-0.5-2.1-0.8-1-0.2-1.9-0.2-1.7 0-2.5 0.6-0.9 0.6-0.9 1.8 0 0.9 0.6 1.4 0.6 0.5 2.2 0.8l1.2 0.3q2.2 0.4 3.2 1.4 1.1 1.1 1.1 2.9 0 2.1-1.4 3.2-1.5 1.1-4.2 1.1-1 0-2.2-0.3-1.2-0.2-2.4-0.6v-2.1q1.2 0.7 2.3 1 1.2 0.4 2.3 0.4 1.7 0 2.6-0.7 0.9-0.6 0.9-1.9 0-1.1-0.6-1.7-0.7-0.6-2.2-0.9l-1.2-0.2q-2.2-0.4-3.2-1.4-1-0.9-1-2.6 0-1.9 1.4-3 1.3-1.1 3.7-1.1 1.1 0 2.1 0.1 1.1 0.2 2.2 0.6zm13 7.5v6.6h-1.8v-6.5q0-1.6-0.6-2.4-0.6-0.7-1.8-0.7-1.5 0-2.3 0.9-0.9 0.9-0.9 2.5v6.2h-1.8v-15.2h1.8v6q0.7-1 1.5-1.5 0.9-0.5 2.1-0.5 1.8 0 2.8 1.2 1 1.1 1 3.4zm3.6 6.6v-10.9h1.8v10.9zm0-12.9v-2.3h1.7v2.3zm9.4-2.3h1.7v1.5h-1.7q-0.9 0-1.3 0.4-0.4 0.4-0.4 1.4v1h3v1.4h-3v9.5h-1.8v-9.5h-1.7v-1.4h1.7v-0.8q0-1.8 0.8-2.7 0.9-0.8 2.7-0.8zm2.9 1.2h1.8v3.1h3.7v1.4h-3.7v5.9q0 1.3 0.3 1.7 0.4 0.4 1.5 0.4h1.9v1.5h-1.9q-2.1 0-2.8-0.8-0.8-0.8-0.8-2.8v-5.9h-1.4v-1.4h1.4z"/></svg>
\ 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 @@
+<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 253" width="260" height="253"><path fill="#c0ff00" d="m0 167c0-5.5 4.5-10 10-10h90c5.5 0 10 4.5 10 10v23c0 5.5-4.5 10-10 10h-90c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m3 63c0-5.5 4.5-10 10-10h46c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-46c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m1 114c0-5.5 4.5-10 10-10h62c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-62c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m114 166c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m90 117c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v22c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m81 64c0-5.5 4.5-10 10-10h22c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-22c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m54 10c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v27c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m2 10c0-5.5 4.5-10 10-10h26c5.5 0 10 4.5 10 10v27c0 5.5-4.5 10-10 10h-26c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m2 216c0-5.5 4.5-10 10-10h59c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-59c-5.5 0-10-4.5-10-10z"/><path fill="#487997" d="m89 217c0-5.5 4.5-10 10-10h23c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-23c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m141 217c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m191 216c0-5.5 4.5-10 10-10h23c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-23c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m166 164c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v27c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m215 165c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m138 111c0-5.5 4.5-10 10-10h28c5.5 0 10 4.5 10 10v29c0 5.5-4.5 10-10 10h-28c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m192 112c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v29c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m129 64c0-5.5 4.5-10 10-10h27c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-27c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m182 62c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m105 11c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m154 12c0-5.5 4.5-10 10-10h27c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-27c-5.5 0-10-4.5-10-10z"/><path  d="m42.7 172.9v1.9q-1.1-0.5-2.1-0.8-1-0.2-1.9-0.2-1.7 0-2.5 0.6-0.9 0.6-0.9 1.8 0 0.9 0.6 1.4 0.6 0.5 2.2 0.8l1.2 0.3q2.2 0.4 3.2 1.4 1.1 1.1 1.1 2.9 0 2.1-1.4 3.2-1.5 1.1-4.2 1.1-1 0-2.2-0.3-1.2-0.2-2.4-0.6v-2.1q1.2 0.7 2.3 1 1.2 0.4 2.3 0.4 1.7 0 2.6-0.7 0.9-0.6 0.9-1.9 0-1.1-0.6-1.7-0.7-0.6-2.2-0.9l-1.2-0.2q-2.2-0.4-3.2-1.4-1-0.9-1-2.6 0-1.9 1.4-3 1.3-1.1 3.7-1.1 1.1 0 2.1 0.1 1.1 0.2 2.2 0.6zm13 7.5v6.6h-1.8v-6.5q0-1.6-0.6-2.4-0.6-0.7-1.8-0.7-1.5 0-2.3 0.9-0.9 0.9-0.9 2.5v6.2h-1.8v-15.2h1.8v6q0.7-1 1.5-1.5 0.9-0.5 2.1-0.5 1.8 0 2.8 1.2 1 1.1 1 3.4zm3.6 6.6v-10.9h1.8v10.9zm0-12.9v-2.3h1.7v2.3zm9.4-2.3h1.7v1.5h-1.7q-0.9 0-1.3 0.4-0.4 0.4-0.4 1.4v1h3v1.4h-3v9.5h-1.8v-9.5h-1.7v-1.4h1.7v-0.8q0-1.8 0.8-2.7 0.9-0.8 2.7-0.8zm2.9 1.2h1.8v3.1h3.7v1.4h-3.7v5.9q0 1.3 0.3 1.7 0.4 0.4 1.5 0.4h1.9v1.5h-1.9q-2.1 0-2.8-0.8-0.8-0.8-0.8-2.8v-5.9h-1.4v-1.4h1.4z"/></svg>
\ No newline at end of file
diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
index 35ab4c81e..9269d1a60 100644
--- a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
+++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
@@ -18,8 +18,8 @@ const String _kKBLayoutTypeISO = 'ISO';
 const String _kKBLayoutTypeNotISO = 'Not ISO';
 
 const _kKBLayoutImageMap = {
-  _kKBLayoutTypeISO: 'KB_LAYOUT_ISO',
-  _kKBLayoutTypeNotISO: 'KB_LAYOUT_NOT_ISO',
+  _kKBLayoutTypeISO: 'kb_layout_iso',
+  _kKBLayoutTypeNotISO: 'kb_layout_not_iso',
 };
 
 class _KBImage extends StatelessWidget {

From 50c33450b94de574d9259f44fe69c7eded6a6ef3 Mon Sep 17 00:00:00 2001
From: fufesou <shuanglongchen@yeah.net>
Date: Tue, 27 Dec 2022 17:49:32 +0800
Subject: [PATCH 3/3] fix keyboard type store

Signed-off-by: fufesou <shuanglongchen@yeah.net>
---
 .../widgets/kb_layout_type_chooser.dart       | 75 +++++++++----------
 libs/hbb_common/src/config.rs                 |  4 +-
 2 files changed, 39 insertions(+), 40 deletions(-)

diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
index 9269d1a60..6601160a7 100644
--- a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
+++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
@@ -6,7 +6,7 @@ import 'package:flutter_hbb/models/platform_model.dart';
 
 import '../../common.dart';
 
-typedef KBChoosedCallback = bool Function(String);
+typedef KBChoosedCallback = Future<bool> Function(String);
 
 const double _kImageMarginVertical = 6.0;
 const double _kImageMarginHorizental = 10.0;
@@ -78,11 +78,19 @@ class _KBChooser extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    onChanged(String? v) async {
+      if (v != null) {
+        if (await cb(v)) {
+          choosedType.value = v;
+        }
+      }
+    }
+
     return Column(
       children: [
         TextButton(
           onPressed: () {
-            choosedType.value = kbLayoutType;
+            onChanged(kbLayoutType);
           },
           child: _KBImage(
             kbLayoutType: kbLayoutType,
@@ -98,21 +106,13 @@ class _KBChooser extends StatelessWidget {
                     splashRadius: 0,
                     value: kbLayoutType,
                     groupValue: choosedType.value,
-                    onChanged: (String? newValue) {
-                      if (newValue != null) {
-                        if (cb(newValue)) {
-                          choosedType.value = newValue;
-                        }
-                      }
-                    },
+                    onChanged: onChanged,
                   )),
               Text(kbLayoutType),
             ],
           ),
           onPressed: () {
-            if (cb(kbLayoutType)) {
-              choosedType.value = kbLayoutType;
-            }
+            onChanged(kbLayoutType);
           },
         ),
       ],
@@ -138,31 +138,28 @@ class KBLayoutTypeChooser extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final imageWidth = width / 2 - dividerWidth;
-    return Container(
-      color: Colors.white,
-      child: 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,
-              ),
-            ],
-          ),
+    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,
+            ),
+          ],
         ),
       ),
     );
@@ -215,8 +212,8 @@ showKBLayoutTypeChooser(
           width: 360,
           height: 200,
           dividerWidth: 4.0,
-          cb: (String v) {
-            bind.setLocalKbLayoutType(kbLayoutType: v);
+          cb: (String v) async {
+            await bind.setLocalKbLayoutType(kbLayoutType: v);
             KBLayoutType.value = bind.getLocalKbLayoutType();
             return v == KBLayoutType.value;
           }),
diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs
index d86ac3463..4bc33cda5 100644
--- a/libs/hbb_common/src/config.rs
+++ b/libs/hbb_common/src/config.rs
@@ -1024,7 +1024,9 @@ impl LocalConfig {
     }
 
     pub fn set_kb_layout_type(kb_layout_type: String) {
-        LOCAL_CONFIG.write().unwrap().kb_layout_type = kb_layout_type
+        let mut config = LOCAL_CONFIG.write().unwrap();
+        config.kb_layout_type = kb_layout_type;
+        config.store();
     }
 
     pub fn get_size() -> Size {