diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart
index 4b0215703..f236a7828 100644
--- a/flutter/lib/desktop/widgets/remote_menubar.dart
+++ b/flutter/lib/desktop/widgets/remote_menubar.dart
@@ -920,6 +920,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
disableClipboard(),
lockAfterSessionEnd(),
privacyMode(),
+ swapKey(),
]);
}
@@ -1528,6 +1529,23 @@ class _DisplayMenuState extends State<_DisplayMenu> {
ffi: widget.ffi,
child: Text(translate('Privacy mode')));
}
+
+ swapKey() {
+ final visible = perms['keyboard'] != false &&
+ ((Platform.isMacOS && pi.platform != kPeerPlatformMacOS) ||
+ (!Platform.isMacOS && pi.platform == kPeerPlatformMacOS));
+ if (!visible) return Offstage();
+ final option = 'allow_swap_key';
+ final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
+ return _CheckboxMenuButton(
+ value: value,
+ onChanged: (value) {
+ if (value == null) return;
+ bind.sessionToggleOption(id: widget.id, value: option);
+ },
+ ffi: widget.ffi,
+ child: Text(translate('Swap control-command key')));
+ }
}
class _KeyboardMenu extends StatelessWidget {
diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs
index 3bfc885c5..7bc82ed95 100644
--- a/libs/hbb_common/src/config.rs
+++ b/libs/hbb_common/src/config.rs
@@ -217,6 +217,8 @@ pub struct PeerConfig {
pub lock_after_session_end: LockAfterSessionEnd,
#[serde(flatten)]
pub privacy_mode: PrivacyMode,
+ #[serde(flatten)]
+ pub allow_swap_key: AllowSwapKey,
#[serde(default)]
pub port_forwards: Vec<(i32, String, i32)>,
#[serde(default)]
@@ -1060,6 +1062,8 @@ serde_field_bool!(
);
serde_field_bool!(PrivacyMode, "privacy_mode", default_privacy_mode);
+serde_field_bool!(AllowSwapKey, "allow_swap_key", default_swap_key);
+
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct LocalConfig {
#[serde(default)]
diff --git a/src/client.rs b/src/client.rs
index ebfda7283..40a9f05b0 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -1230,6 +1230,8 @@ impl LoginConfigHandler {
option.block_input = BoolOption::No.into();
} else if name == "show-quality-monitor" {
config.show_quality_monitor.v = !config.show_quality_monitor.v;
+ } else if name == "allow_swap_key" {
+ config.allow_swap_key.v = !config.allow_swap_key.v;
} else {
let is_set = self
.options
@@ -1383,6 +1385,8 @@ impl LoginConfigHandler {
self.config.disable_clipboard.v
} else if name == "show-quality-monitor" {
self.config.show_quality_monitor.v
+ } else if name == "allow_swap_key" {
+ self.config.allow_swap_key.v
} else {
!self.get_option(name).is_empty()
}
@@ -1807,6 +1811,7 @@ pub fn send_mouse(
if check_scroll_on_mac(mask, x, y) {
mouse_event.modifiers.push(ControlKey::Scroll.into());
}
+ interface.swap_modifier_mouse(&mut mouse_event);
msg_out.set_mouse_event(mouse_event);
interface.send(Data::Message(msg_out));
}
@@ -2033,6 +2038,7 @@ pub trait Interface: Send + Clone + 'static + Sized {
fn is_force_relay(&self) -> bool {
self.get_login_config_handler().read().unwrap().force_relay
}
+ fn swap_modifier_mouse(&self, _msg : &mut hbb_common::protos::message::MouseEvent) {}
}
/// Data used by the client interface.
diff --git a/src/ui/header.tis b/src/ui/header.tis
index e25c0d544..257ba417e 100644
--- a/src/ui/header.tis
+++ b/src/ui/header.tis
@@ -198,6 +198,7 @@ class Header: Reactor.Component {
{keyboard_enabled && clipboard_enabled ?
{svg_checkmark}{translate('Disable clipboard')} : ""}
{keyboard_enabled ? {svg_checkmark}{translate('Lock after session end')} : ""}
{keyboard_enabled && pi.platform == "Windows" ? {svg_checkmark}{translate('Privacy mode')} : ""}
+ {keyboard_enabled && ((is_osx && pi.platform != "Mac OS") || (!is_osx && pi.platform == "Mac OS")) ? {svg_checkmark}{translate('Swap control-command key')} : ""}
;
}
@@ -440,7 +441,7 @@ function toggleMenuState() {
for (var el in $$(menu#keyboard-options>li)) {
el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0);
}
- for (var id in ["show-remote-cursor", "show-quality-monitor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end"]) {
+ for (var id in ["show-remote-cursor", "show-quality-monitor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end", "allow_swap_key"]) {
var el = self.select('#' + id);
if (el) {
var value = handler.get_toggle_option(id);
diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs
index f726ed526..11bcff925 100644
--- a/src/ui_session_interface.rs
+++ b/src/ui_session_interface.rs
@@ -373,10 +373,87 @@ impl Session {
return "".to_owned();
}
+ pub fn swab_modifier_key(&self, msg: &mut KeyEvent) {
+
+ let allow_swap_key = self.get_toggle_option("allow_swap_key".to_string());
+ if allow_swap_key {
+ if let Some(key_event::Union::ControlKey(ck)) = msg.union {
+ let ck = ck.enum_value_or_default();
+ let ck = match ck {
+ ControlKey::Control => ControlKey::Meta,
+ ControlKey::Meta => ControlKey::Control,
+ ControlKey::RControl => ControlKey::Meta,
+ ControlKey::RWin => ControlKey::Control,
+ _ => ck,
+ };
+ msg.set_control_key(ck);
+ }
+ msg.modifiers = msg.modifiers.iter().map(|ck| {
+ let ck = ck.enum_value_or_default();
+ let ck = match ck {
+ ControlKey::Control => ControlKey::Meta,
+ ControlKey::Meta => ControlKey::Control,
+ ControlKey::RControl => ControlKey::Meta,
+ ControlKey::RWin => ControlKey::Control,
+ _ => ck,
+ };
+ hbb_common::protobuf::EnumOrUnknown::new(ck)
+ }).collect();
+
+
+ let code = msg.chr();
+ if code != 0 {
+ let mut peer = self.peer_platform().to_lowercase();
+ peer.retain(|c| !c.is_whitespace());
+
+ let key = match peer.as_str() {
+ "windows" => {
+ let key = rdev::win_key_from_scancode(code);
+ let key = match key {
+ rdev::Key::ControlLeft => rdev::Key::MetaLeft,
+ rdev::Key::MetaLeft => rdev::Key::ControlLeft,
+ rdev::Key::ControlRight => rdev::Key::MetaLeft,
+ rdev::Key::MetaRight => rdev::Key::ControlLeft,
+ _ => key,
+ };
+ rdev::win_scancode_from_key(key).unwrap_or_default()
+ }
+ "macos" => {
+ let key = rdev::macos_key_from_code(code);
+ let key = match key {
+ rdev::Key::ControlLeft => rdev::Key::MetaLeft,
+ rdev::Key::MetaLeft => rdev::Key::ControlLeft,
+ rdev::Key::ControlRight => rdev::Key::MetaLeft,
+ rdev::Key::MetaRight => rdev::Key::ControlLeft,
+ _ => key,
+ };
+ rdev::macos_keycode_from_key(key).unwrap_or_default()
+ }
+ _ => {
+ let key = rdev::linux_key_from_code(code);
+ let key = match key {
+ rdev::Key::ControlLeft => rdev::Key::MetaLeft,
+ rdev::Key::MetaLeft => rdev::Key::ControlLeft,
+ rdev::Key::ControlRight => rdev::Key::MetaLeft,
+ rdev::Key::MetaRight => rdev::Key::ControlLeft,
+ _ => key,
+ };
+ rdev::linux_keycode_from_key(key).unwrap_or_default()
+ }
+ };
+ msg.set_chr(key);
+ }
+ }
+
+ }
+
pub fn send_key_event(&self, evt: &KeyEvent) {
// mode: legacy(0), map(1), translate(2), auto(3)
+
+ let mut msg = evt.clone();
+ self.swab_modifier_key(&mut msg);
let mut msg_out = Message::new();
- msg_out.set_key_event(evt.clone());
+ msg_out.set_key_event(msg);
self.send(Data::Message(msg_out));
}
@@ -934,6 +1011,23 @@ impl Interface for Session {
handle_test_delay(t, peer).await;
}
}
+
+ fn swap_modifier_mouse(&self, msg : &mut hbb_common::protos::message::MouseEvent) {
+ let allow_swap_key = self.get_toggle_option("allow_swap_key".to_string());
+ if allow_swap_key {
+ msg.modifiers = msg.modifiers.iter().map(|ck| {
+ let ck = ck.enum_value_or_default();
+ let ck = match ck {
+ ControlKey::Control => ControlKey::Meta,
+ ControlKey::Meta => ControlKey::Control,
+ ControlKey::RControl => ControlKey::Meta,
+ ControlKey::RWin => ControlKey::Control,
+ _ => ck,
+ };
+ hbb_common::protobuf::EnumOrUnknown::new(ck)
+ }).collect();
+ };
+ }
}
impl Session {