[feat] add customizable mobile shortcuts for Windows, macOS and Linux, refs #153

This commit is contained in:
dijunkun committed 2026-11-13 16:37:55 +08:00
1 parent 72dda9bb90
commit bad4569117
47 files changed
+3314 -70

No files matched your search

+7 -1
View File
@@ -159,7 +159,7 @@ gh secret set ANDROID_KEY_PASSWORD --repo kunkundi/crossdesk
身份按主机名和端口分开保存,备份与设备迁移不包含应用数据。密码不写入连接记录,仅在明确开启“保存密码”后写入独立加密存储;未开启时仅用于当前会话。连接成功通知只处理一次,重复的传输就绪回调不会再次保存密码或重建会话页面;密码存储拒绝空值覆盖。旧版本已被空值覆盖的密码无法恢复,遇到此记录时会提示重新输入一次,并保留“保存密码”开启状态。由于 MiniRTC 的诊断日志包含登录身份,Android 桥接层关闭其会话日志。身份存储损坏会显示错误,不会静默重新注册。
会话中的“剪贴板”发送用户输入的文本;收到的远端文本先留在会话内,点击“复制远端文本”才写入手机剪贴板。退出会话后清除会话内容。每次发送最多 128 KB。电脑键盘的 Ctrl / Win / Alt / Shift 可锁定组合键;macOS 的 Command 对应 Win。系统键盘通过 `supports_text_input` 检测远端能力,支持 UTF-8 的 Windows 和 macOS 被控端可直接输入中文、表情和听写文本;其他端或旧版使用 ASCII 按键回退,不支持的文本会提示使用剪贴板,不会先删除远端内容。输入法尚未确认的组字不会发送,自动纠错仅替换已发送文本的变化后缀;点击远端、收起、切换键盘或进入后台会清除本地输入上下文。
会话中的“剪贴板”发送用户输入的文本;收到的远端文本先留在会话内,点击“复制远端文本”才写入手机剪贴板。退出会话后清除会话内容。每次发送最多 128 KB。电脑键盘的修饰键在本地选中,仅应用于下一个按键,发送后自动复位;名称根据远端显示为 Win / Super / Cmd 和 Alt / Option。系统键盘通过 `supports_text_input` 检测远端能力,支持 UTF-8 的 Windows 和 macOS 被控端可直接输入中文、表情和听写文本;其他端或旧版使用 ASCII 按键回退,不支持的文本会提示使用剪贴板,不会先删除远端内容。输入法尚未确认的组字不会发送,自动纠错仅替换已发送文本的变化后缀;点击远端、收起、切换键盘或进入后台会清除本地输入上下文。
Windows 文本输入使用 Unicode 键盘事件,无需切换电脑输入法或覆盖剪贴板。本次开发版将完整 UTF-8 文本通过 Windows 服务转发到锁屏、登录与 UAC 安全桌面,保留数字、大小写字母、符号、空格及中文等字符,不将密码转换成数字或 ASCII 按键。该通道需要同时更新 Windows 客户端、CrossDesk Service 和会话辅助程序;仅更新 Android,或仅确认旧服务正在运行,均不会启用修复。旧版可能已声明 `supports_text_input`,但只支持普通桌面文本输入,因此“电脑键盘可输入、搜狗等系统输入法不可输入”仍可能出现。锁屏实际输入效果需在更新后的 Windows 环境验证。
@@ -256,3 +256,9 @@ AV1 硬件路径尚无对应实机验证;Retroid 上使用 dav1d 软件解码
设置 → 关于使用与 iOS 相同的分组及入口:版本、软件许可、开源组件、源码与构建说明。软件许可中可分别阅读 GNU GPL 第 3 版及开源软件权利说明;开源组件页单独展示 OpenFEC 告知,其余组件列出名称和许可证,详情包含版本、源码、适用的构建配方和每一份许可文档。内容按 Android 构建生成,不显示未使用的 iOS 依赖,Android 签名说明替换 Apple 签名说明。
APK 保留完整 `THIRD_PARTY_NOTICES.txt`,并生成轻量的 `ThirdPartyLicenses.json` 索引、72 份独立许可文档及 `SourceMetadata.json`。打开文档时才读取正文,长文按有限长度分块显示,支持文本选择、复制与网页链接。构建继续生成并打包许可资料,CI 和 APK 验证不再检查许可证内容、哈希或组件清单。源码页面显示标签或提交版本及未发布修改提示,源码链接指向基础提交。当前 Android 构建说明、构建脚本、MiniRTC、OpenSSL 及 SVT-AV1 配方随 APK 离线提供,避免开发版链接指向基础提交中尚不存在的 Android 文件。
## 快捷键与自定义
系统键盘上方的 44 dp 工具栏提供常用快捷键横向滚动栏(默认 Esc、Tab、复制、粘贴)和“更多”入口。更多面板按“常用 / 按键 / 系统 / 我的”分组,替换原键盘区域并沿用输入法高度。预设根据 Windows、macOS、Linux 远端适配;Linux 系统预设需手动选择 GNOME 或 KDE Plasma,未知平台只提供通用按键。Windows 安全选项沿用 Ctrl+Alt+Del 服务通道。
“管理与自定义”使用独立本地窗口,编辑内容不发送远端。最多保存 16 个自定义组合,每个远端平台最多固定 8 个,支持排序、取消固定、编辑与删除。自定义组合保留用户选择的实际修饰键。组合键整体排队,按反序释放修饰键;快捷键执行前先发送已确认的文本并重置纠错上下文,尚在组词时提示先选词或取消。
@@ -321,6 +321,61 @@ final class LocalizationKeys {
IDS.put("配对信息已复制。", R.string.text_cbacf1eafbca53e0);
IDS.put("无法复制配对信息,请重试。", R.string.text_cde42ccdeacbbbd9);
IDS.put("安全存储不可用,请解锁设备后重试", R.string.text_7383b2ae09252c59);
IDS.put("上一页", R.string.text_c9b9ae7a61444ab7);
IDS.put("下一页", R.string.text_8a8542f6964852dc);
IDS.put("主按键", R.string.text_0c4f690eb398c12e);
IDS.put("任务管理器", R.string.text_62fdb5304cf5cd9b);
IDS.put("保存", R.string.text_a3030bf8f16dc63c);
IDS.put("修饰键", R.string.text_ba0e2ff8201bc6a6);
IDS.put("全选", R.string.text_3a5040b68abf75f9);
IDS.put("切换应用", R.string.text_c2d918002afcc3d4);
IDS.put("切换窗口", R.string.text_a7fe8e2685849c31);
IDS.put("剪切", R.string.text_410a8e8a6bf253ac);
IDS.put("发送组合", R.string.text_6309cd49db59d165);
IDS.put("名称", R.string.text_d44e9b3d3b31d37b);
IDS.put("向上移动", R.string.text_6789dd11afc7c4c5);
IDS.put("固定到键盘上方", R.string.text_352c003784b1c89e);
IDS.put("复制", R.string.text_63d90d977348ab1f);
IDS.put("安全选项", R.string.text_ecb0d13f3f8bfd0d);
IDS.put("已有相同组合的快捷键", R.string.text_dd1b1da65a6c89de);
IDS.put("常用", R.string.text_4563c57b5143deaa);
IDS.put("应用列表", R.string.text_6308bf1373bd0ee8);
IDS.put("强制退出面板", R.string.text_8dc0823d700cf875);
IDS.put("快捷键", R.string.text_ee2638183d3ea860);
IDS.put("我的", R.string.text_7f1d9dd04cd1c7b8);
IDS.put("按键", R.string.text_7c7577d4d4fc6009);
IDS.put("撤销", R.string.text_926a50b98ece2667);
IDS.put("新建", R.string.text_50ef2f4cf6a46924);
IDS.put("新建快捷键", R.string.text_c6904bb36f7f62c5);
IDS.put("显示桌面", R.string.text_5cfe4846aa8a7228);
IDS.put("更多快捷键", R.string.text_feb12adf8e0315e2);
IDS.put("最多保存 16 个自定义快捷键", R.string.text_5d6bce93c0649a63);
IDS.put("最多固定 8 个快捷键", R.string.text_f33cb21422f62ee8);
IDS.put("未知", R.string.text_4d8c1c5b42830791);
IDS.put("本地编辑,输入不会发送到远端", R.string.text_6b1fa1d7159de777);
IDS.put("添加到常用栏", R.string.text_cb9eaa63c25d383e);
IDS.put("移除", R.string.text_6135d4159e892541);
IDS.put("窗口菜单", R.string.text_e693ff5832c7f46d);
IDS.put("等待远端平台信息", R.string.text_096a247cc5455ba7);
IDS.put("管理与自定义", R.string.text_39c7740197c41d44);
IDS.put("管理快捷键", R.string.text_2dc8c27b75796b36);
IDS.put("粘贴", R.string.text_33517926747180e6);
IDS.put("系统", R.string.text_5b50d7c4b5950dc5);
IDS.put("终端复制", R.string.text_45123fa07e22fb3e);
IDS.put("终端粘贴", R.string.text_3936f3d30a763a35);
IDS.put("编辑", R.string.text_051836569928a9f9);
IDS.put("编辑快捷键", R.string.text_0ecda10acc934f22);
IDS.put("聚焦搜索", R.string.text_d32d2a4e0e83d7cf);
IDS.put("请使用系统预设中的安全选项", R.string.text_add54b3ce4b1c661);
IDS.put("请先选词或取消组词", R.string.text_ae066f34095261c9);
IDS.put("请输入有效的快捷键名称", R.string.text_a895bdb6f1f11e99);
IDS.put("请选择有效的按键", R.string.text_6b3213d2afed99e8);
IDS.put("运行窗口", R.string.text_819569ec77acb01e);
IDS.put("还没有自定义快捷键", R.string.text_22c4107f03c3533e);
IDS.put("适用远端", R.string.text_b07f98d4671af3f9);
IDS.put("选择桌面方案", R.string.text_4bdabfba3368869d);
IDS.put("选择桌面方案以加载系统预设", R.string.text_4fa28857c6f253cc);
IDS.put("键盘上方常用栏", R.string.text_fc5f6f7462e8557a);
}
private LocalizationKeys() {}
}
@@ -348,7 +348,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
public void togglePrivacy(){if(ready&&session!=null)session.togglePrivacy();}
public void key(int code){tapKey(code);}
public boolean textEdit(int count,String value){return sendTextEdit(count,value);}
public void chord(int code,int[] modifiers){if(session==null)return;for(int modifier:modifiers)session.key(modifier,true);tapKey(code);for(int i=modifiers.length-1;i>=0;i--)session.key(modifiers[i],false);}public void shortcut(int code){MainActivity.this.shortcut(code);}public void text(String value){sendText(value);}
public void chord(int code,int[] modifiers){if(ready&&session!=null)session.chord(code,modifiers);}public void shortcut(int code){MainActivity.this.shortcut(code);}public void text(String value){sendText(value);}
public void mute(){muted=!muted;if(session!=null)session.control(2,muted?0:1);controls.setMuted(muted);}
public void mouse(){boolean relative=!preferences.getBoolean("relativeMouse",true);preferences.edit().putBoolean("relativeMouse",relative).apply();video.setRelative(relative);controls.setRelative(relative);}
public void displayResolution(RemoteDisplayResolutions.Mode mode){updateDisplayResolution(mode);}
@@ -524,7 +524,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
toast(L10n.tr("当前远端不支持直接输入此文本,请使用支持文本输入的被控端版本,或通过剪贴板发送"));return false;
}
private void tapKey(int code){if(session!=null){session.key(code,true);session.key(code,false);}}
private void shortcut(int code){if(session!=null){int modifier=hostPlatform.equals("macos")?0x5B:0x11;session.key(modifier,true);tapKey(code);session.key(modifier,false);}}
private void shortcut(int code){if(ready&&session!=null)session.chord(code,new int[]{hostPlatform.equals("macos")?0x5B:0x11});}
private void selectDisplay(){
if(!ready)return;int count=Math.min(displays.length(),8);if(count==0){toast(L10n.tr("等待远端屏幕信息"));return;}String[] labels=new String[count];for(int i=0;i<count;i++){JSONObject display=displays.optJSONObject(i);labels[i]=(i+1)+" · "+(display==null?L10n.tr("显示器"):display.optString("name",L10n.tr("显示器")));}new AlertDialog.Builder(this).setTitle(L10n.tr("显示器")).setItems(labels,(d,w)->{if(session!=null){if(w!=selectedDisplay&&video!=null)video.resetViewport();selectedDisplay=w;session.control(4,w);if(controls!=null)controls.displayResolutions(displayResolutions,selectedDisplay);if(controls!=null)controls.resetVideoStatistics();}}).show();
}
@@ -299,6 +299,15 @@ final class NativeSession implements AutoCloseable {
void surface(Surface value) { execute(() -> { surface = value; if (handle != 0) nSurface(handle, value); }); }
void pointer(float x, float y, int flag, int wheel) { execute(() -> { if (connected) nPointer(handle, x, y, flag, wheel); }); }
void key(int code, boolean down) { execute(() -> { if (connected) nKey(handle, code, down); }); }
void chord(int code,int[] modifiers) {
if(closed||!connected)return;
int[] keys=RemoteShortcuts.normalize(modifiers);
execute(()->{
if(!connected)return;
try{for(int modifier:keys)nKey(handle,modifier,true);nKey(handle,code,true);}
finally{nKey(handle,code,false);for(int i=keys.length-1;i>=0;i--)nKey(handle,keys[i],false);}
});
}
boolean textEdit(int deletions,String text) {
byte[] bytes=text.getBytes(StandardCharsets.UTF_8);
boolean unicode=supportsTextInput;
@@ -22,6 +22,12 @@ final class RemoteKeyboard extends LinearLayout {
private final Runnable changed;
private final LinearLayout toolbar,keys;
private final TextView switchButton,lettersButton,functionsButton;
private final LinearLayout quickKeys;
private final HorizontalScrollView quickScroll;
private final TextView panelTitle;
private final RemoteShortcutPanel shortcutPanel;
private String hostPlatform="unknown";
private boolean shortcuts;
private final Set<Integer> modifiers=new LinkedHashSet<>();
private final SparseIntArray heights=new SparseIntArray();
private boolean opened,computer,functions,imeVisible,measuring,switching,targetComputer;
@@ -35,15 +41,20 @@ final class RemoteKeyboard extends LinearLayout {
public boolean edit(int count,String text){return actions.textEdit(count,text);}
public void key(int code){actions.key(code);}
});
shortcutPanel=new RemoteShortcutPanel(context,ui,this::sendShortcut,()->{render();changed.run();});
FrameLayout header=new FrameLayout(context);addView(header,new LayoutParams(-1,ui.dp(44)));
header.addView(editor,new FrameLayout.LayoutParams(1,1,Gravity.BOTTOM));
toolbar=ui.row();toolbar.setGravity(Gravity.CENTER_VERTICAL);toolbar.setPadding(ui.dp(6),0,ui.dp(6),0);
header.addView(toolbar,new FrameLayout.LayoutParams(-1,-1));
switchButton=button(L10n.tr("电脑键盘"),this::switchKeyboard);switchButton.setPadding(ui.dp(8),0,ui.dp(8),0);
switchButton=button(L10n.tr("电脑键盘"),()->{shortcuts=false;switchKeyboard();});switchButton.setPadding(ui.dp(8),0,ui.dp(8),0);
toolbar.addView(switchButton,new LayoutParams(-2,-1));
LinearLayout pages=ui.row();pages.setGravity(Gravity.CENTER);toolbar.addView(pages,new LayoutParams(0,-1,1));
quickScroll=new HorizontalScrollView(context);quickScroll.setHorizontalScrollBarEnabled(false);
quickKeys=ui.row();quickScroll.addView(quickKeys);pages.addView(quickScroll,new LayoutParams(-1,-1));
panelTitle=ui.text(L10n.tr("快捷键"),13,false);panelTitle.setGravity(Gravity.CENTER);pages.addView(panelTitle,new LayoutParams(-1,-1));
lettersButton=button(L10n.tr("字母数字"),()->page(false));functionsButton=button(L10n.tr("符号功能"),()->page(true));
pages.addView(lettersButton,new LayoutParams(-2,ui.dp(32)));pages.addView(functionsButton,new LayoutParams(-2,ui.dp(32)));
TextView more=button("⋯",this::showShortcuts);more.setContentDescription(L10n.tr("更多快捷键"));toolbar.addView(more,new LayoutParams(ui.dp(44),ui.dp(44)));
FrameLayout hide=new FrameLayout(context);hide.setContentDescription(L10n.tr("收起键盘"));hide.setClickable(true);hide.setFocusable(true);
hide.addView(ui.icon("keyboard_hide",foreground()),new FrameLayout.LayoutParams(ui.dp(24),ui.dp(24),Gravity.CENTER));
hide.setOnClickListener(v->{feedback(v);close();});toolbar.addView(hide,new LayoutParams(ui.dp(44),ui.dp(44)));
@@ -60,6 +71,7 @@ final class RemoteKeyboard extends LinearLayout {
private void feedback(View view){view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);}
boolean isOpen(){return opened;}
boolean isComputer(){return computer;}
void setPlatform(String value){hostPlatform=RemoteShortcuts.platform(value);shortcutPanel.platform(hostPlatform);if(opened)render();}
int bottomInset(){return opened&&!computer?imeHeight:0;}
private int sizeKey(){Configuration c=getResources().getConfiguration();return c.screenWidthDp*10000+c.screenHeightDp;}
private int keyAreaHeight(){
@@ -71,11 +83,11 @@ final class RemoteKeyboard extends LinearLayout {
int preferredHeight(){return ui.dp(44)+(computer?keyAreaHeight():0);}
void toggle(){if(opened)close();else open();}
void open(){
opened=true;computer=false;modifiers.clear();editor.start();setVisibility(VISIBLE);render();changed.run();
opened=true;computer=shortcuts=false;modifiers.clear();editor.start();setVisibility(VISIBLE);render();changed.run();
post(()->{if(opened&&!computer)changeIme(true,false,false);});
}
boolean close(){
if(!opened)return false;opened=false;measuring=switching=false;cancelTransition();modifiers.clear();editor.stop();
shortcutPanel.close();if(!opened)return false;opened=false;measuring=switching=false;cancelTransition();modifiers.clear();editor.stop();
getContext().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(getWindowToken(),0);
setVisibility(GONE);changed.run();return true;
}
@@ -83,6 +95,7 @@ final class RemoteKeyboard extends LinearLayout {
private void page(boolean next){functions=next;render();}
private void switchKeyboard(){
if(!opened||switching)return;
if(!computer&&!prepareShortcut())return;
targetComputer=!computer;switching=true;modifiers.clear();
if(targetComputer){
editor.pause();computer=true;render();changed.run();
@@ -99,6 +112,20 @@ final class RemoteKeyboard extends LinearLayout {
invalidate();
}else changeIme(!targetComputer,true,false);
}
private boolean prepareShortcut(){
if(computer)return true;
if(editor.hasPendingComposition()){Toast.makeText(getContext(),L10n.tr("请先选词或取消组词"),Toast.LENGTH_SHORT).show();return false;}
return editor.prepareShortcut();
}
private void showShortcuts(){
if(!opened||switching)return;
if(computer){if(shortcuts)switchKeyboard();else{shortcuts=true;modifiers.clear();render();changed.run();}}
else{shortcuts=true;switchKeyboard();}
}
private void sendShortcut(RemoteShortcuts.Item item){
if(!opened||switching||!item.platform.equals(hostPlatform)||!prepareShortcut())return;
modifiers.clear();if(item.service)actions.secureAttention();else actions.chord(item.key,item.modifiers);
}
private void finishSwitch(){
if(!switching)return;switching=false;
if(computer!=targetComputer){computer=targetComputer;render();}
@@ -158,7 +185,9 @@ final class RemoteKeyboard extends LinearLayout {
private void render(){
setBackgroundColor(dark()?0xFF202124:0xFFE8EAED);
switchButton.setText(computer?L10n.tr("系统键盘"):L10n.tr("电脑键盘"));switchButton.setTextColor(foreground());
lettersButton.setVisibility(computer?VISIBLE:GONE);functionsButton.setVisibility(computer?VISIBLE:GONE);
lettersButton.setVisibility(computer&&!shortcuts?VISIBLE:GONE);functionsButton.setVisibility(computer&&!shortcuts?VISIBLE:GONE);
quickScroll.setVisibility(computer?GONE:VISIBLE);panelTitle.setVisibility(computer&&shortcuts?VISIBLE:GONE);panelTitle.setTextColor(foreground());
quickKeys.removeAllViews();for(RemoteShortcuts.Item item:shortcutPanel.store.pinned(hostPlatform)){TextView key=button(item.title(),()->sendShortcut(item));key.setContentDescription(item.title()+", "+item.chord());quickKeys.addView(key,new LayoutParams(-2,ui.dp(44)));}
for(TextView view:new TextView[]{lettersButton,functionsButton}){
boolean selected=(view==functionsButton)==functions;view.setSelected(selected);
view.setTextColor(selected?(dark()?0xFFA8C7FA:0xFF185ABC):foreground());
@@ -169,6 +198,7 @@ final class RemoteKeyboard extends LinearLayout {
private void renderKeysIfNeeded(int width){
if(!computer)return;int height=keyAreaHeight();
if(layoutWidth==width&&layoutHeight==height)return;layoutWidth=width;layoutHeight=height;keys.removeAllViews();
if(shortcuts){keys.addView(shortcutPanel.create(),new LayoutParams(-1,-1));return;}
boolean landscape=getResources().getConfiguration().screenWidthDp>getResources().getConfiguration().screenHeightDp;
String[][] upper;
if(!functions)upper=new String[][]{{"1","2","3","4","5","6","7","8","9","0"},{"q","w","e","r","t","y","u","i","o","p"},{"a","s","d","f","g","h","j","k","l"}};
@@ -185,6 +215,7 @@ final class RemoteKeyboard extends LinearLayout {
LinearLayout line=ui.row();line.setGravity(Gravity.CENTER);
for(String value:values){
boolean shift=modifiers.contains(16);String shown=shift&&value.length()==1?shifted(value):value;
if(value.equals("Win"))shown=RemoteShortcuts.modifierName(91,hostPlatform);else if(value.equals("Alt"))shown=RemoteShortcuts.modifierName(18,hostPlatform);
TextView key=button(L10n.tr(shown),()->key(value));key.setPadding(0,0,0,0);
key.setTextSize(value.length()>1?12:18);key.setSingleLine(true);
int modifier=modifier(value);boolean selected=modifier!=0&&modifiers.contains(modifier);key.setSelected(selected);
@@ -209,6 +240,7 @@ final class RemoteKeyboard extends LinearLayout {
int modifier=modifier(value);if(modifier!=0){if(!modifiers.add(modifier))modifiers.remove(modifier);render();return;}
int code=switch(value){case "空格"->32;case "Enter"->13;case "⌫"->8;case "Esc"->27;case "Tab"->9;case "Caps"->20;case "Ins"->45;case "Del"->46;case "Home"->36;case "End"->35;case "PgUp"->33;case "PgDn"->34;case "←"->37;case "↑"->38;case "→"->39;case "↓"->40;default->value.startsWith("F")?111+Integer.parseInt(value.substring(1)):KeyMap.ascii(value.charAt(0))&255;};
actions.chord(code,modifiers.stream().mapToInt(Integer::intValue).toArray());
modifiers.clear();render();
}
@Override protected void onMeasure(int widthSpec,int heightSpec){
renderKeysIfNeeded(MeasureSpec.getSize(widthSpec));super.onMeasure(widthSpec,heightSpec);
@@ -52,6 +52,15 @@ final class RemoteKeyboardInput extends EditText {
void pause() { accepting=false;resetContext(); }
void stop() { pause();clearFocus(); }
boolean isAcceptingInput() { return accepting; }
boolean hasPendingComposition() { return batches>0||composing(); }
boolean prepareShortcut() {
if(!synchronize())return false;
// Navigation and shortcuts invalidate the remote correction context.
// Flush committed text first; never send or discard an IME preedit.
if(enterPending)sender.key(13);
resetContext();
return true;
}
void resetContext() {
generation++;batches=0;main.removeCallbacks(synchronize);resetting=true;
// EditText.setText replaces the buffer and internally restarts the IME,
@@ -0,0 +1,107 @@
package cn.crossdesk.mobile;
import android.app.Dialog;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Color;
import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
/** The preset panel occupies the keyboard keybed; editing uses a separate local window. */
final class RemoteShortcutPanel {
private final Context context;
private final MobileUi ui;
final RemoteShortcuts store;
private final Consumer<RemoteShortcuts.Item> send;
private final Runnable changed;
private String platform="unknown";
private int category,page;
private Dialog manager;
RemoteShortcutPanel(Context context,MobileUi ui,Consumer<RemoteShortcuts.Item> send,Runnable changed){this.context=context;this.ui=ui;this.send=send;this.changed=changed;store=new RemoteShortcuts(context);}
void platform(String value){String next=RemoteShortcuts.platform(value);if(!platform.equals(next)){platform=next;page=0;close();}}
private void hideEditor(){if(manager!=null&&manager.getWindow()!=null)context.getSystemService(android.view.inputmethod.InputMethodManager.class).hideSoftInputFromWindow(manager.getWindow().getDecorView().getWindowToken(),0);}
void close(){if(manager!=null){hideEditor();manager.dismiss();manager=null;}}
private boolean dark(){return (context.getResources().getConfiguration().uiMode&Configuration.UI_MODE_NIGHT_MASK)==Configuration.UI_MODE_NIGHT_YES;}
private int ink(){return dark()?Color.WHITE:0xFF202124;}
private int surface(){return dark()?0xFF202124:0xFFF1F3F6;}
private TextView text(String value,int size){TextView text=ui.text(value,size,false);text.setTextColor(ink());return text;}
private TextView button(String value,Runnable action){TextView button=ui.action(value,ink(),action);button.setTextSize(13);button.setMinHeight(ui.dp(44));button.setPadding(ui.dp(8),0,ui.dp(8),0);return button;}
private void refresh(){changed.run();}
View create(){
LinearLayout root=ui.column();root.setPadding(ui.dp(6),0,ui.dp(6),0);
String[] groups={"common","keys","system","mine"},labels={L10n.tr("常用"),L10n.tr("按键"),L10n.tr("系统"),L10n.tr("我的")};
root.addView(new SegmentedControl(ui,labels,category,true,index->{category=index;page=0;refresh();}),new LinearLayout.LayoutParams(-1,ui.dp(36)));
if(category==2&&platform.equals("linux")){
Spinner desktop=spinner(new String[]{L10n.tr("选择桌面方案"),"GNOME","KDE Plasma"},store.desktop().equals("gnome")?1:store.desktop().equals("kde")?2:0,index->{String next=index==1?"gnome":index==2?"kde":"";if(!store.desktop().equals(next)){store.desktop(next);page=0;refresh();}});
root.addView(desktop,new LinearLayout.LayoutParams(-1,ui.dp(40)));
}
List<RemoteShortcuts.Item> items=new ArrayList<>();for(RemoteShortcuts.Item item:store.items(platform))if(item.group.equals(groups[category]))items.add(item);
int count=Math.max(1,(items.size()+5)/6);page=Math.min(page,count-1);
ScrollView gridScroll=new ScrollView(context);gridScroll.setFillViewport(true);root.addView(gridScroll,new LinearLayout.LayoutParams(-1,0,1));
LinearLayout grid=ui.column();grid.setMinimumHeight(ui.dp(items.isEmpty()?44:108));gridScroll.addView(grid,new FrameLayout.LayoutParams(-1,-2));
if(items.isEmpty()){TextView empty=text(L10n.tr(platform.equals("unknown")?"等待远端平台信息":category==2?"选择桌面方案以加载系统预设":"还没有自定义快捷键"),13);empty.setGravity(Gravity.CENTER);grid.addView(empty,new LinearLayout.LayoutParams(-1,-1));}
else for(int row=0;row<2;row++){
LinearLayout line=ui.row();for(int col=0;col<3;col++){
int index=page*6+row*3+col;LinearLayout.LayoutParams params=new LinearLayout.LayoutParams(0,-1,1);params.setMargins(ui.dp(3),ui.dp(3),ui.dp(3),ui.dp(3));
if(index>=items.size()){line.addView(new View(context),params);continue;}
RemoteShortcuts.Item item=items.get(index);LinearLayout tile=ui.column();tile.setGravity(Gravity.CENTER);tile.setPadding(ui.dp(3),0,ui.dp(3),0);tile.setBackground(ui.background(dark()?0xFF353A43:Color.WHITE,7));
TextView name=text(item.title(),13);name.setGravity(Gravity.CENTER);name.setMaxLines(2);tile.addView(name);TextView chord=text(item.chord(),11);chord.setGravity(Gravity.CENTER);chord.setMaxLines(2);tile.addView(chord);
tile.setContentDescription(item.title()+", "+item.chord());tile.setClickable(true);tile.setFocusable(true);tile.setOnClickListener(v->send.accept(item));line.addView(tile,params);
}grid.addView(line,new LinearLayout.LayoutParams(-1,0,1));
}
LinearLayout footer=ui.row();footer.setGravity(Gravity.CENTER_VERTICAL);
if(count>1){TextView previous=button("‹",()->{page--;refresh();});previous.setEnabled(page>0);previous.setContentDescription(L10n.tr("上一页"));footer.addView(previous);footer.addView(text((page+1)+" / "+count,12));TextView next=button("›",()->{page++;refresh();});next.setEnabled(page+1<count);next.setContentDescription(L10n.tr("下一页"));footer.addView(next);}
else footer.addView(text(RemoteShortcuts.platformName(platform),12));
footer.addView(new View(context),new LinearLayout.LayoutParams(0,1,1));TextView edit=button(L10n.tr("管理与自定义"),this::manage);edit.setEnabled(!platform.equals("unknown"));footer.addView(edit);root.addView(footer,new LinearLayout.LayoutParams(-1,ui.dp(44)));
return root;
}
private Spinner spinner(String[] labels,int selected,Consumer<Integer> changed){Spinner spinner=new Spinner(context);ArrayAdapter<String> adapter=new ArrayAdapter<>(context,android.R.layout.simple_spinner_dropdown_item,labels);spinner.setAdapter(adapter);spinner.setSelection(selected);spinner.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener(){public void onNothingSelected(android.widget.AdapterView<?> p){}public void onItemSelected(android.widget.AdapterView<?> p,View v,int position,long id){changed.accept(position);}});return spinner;}
private LinearLayout screen(String title,Runnable back,String action,Runnable submit){
LinearLayout root=ui.column();root.setBackgroundColor(surface());LinearLayout header=ui.row();header.setGravity(Gravity.CENTER_VERTICAL);header.addView(button(L10n.tr("返回"),back));TextView heading=text(title,17);header.addView(heading,new LinearLayout.LayoutParams(0,-2,1));if(action!=null)header.addView(button(action,submit));root.addView(header,new LinearLayout.LayoutParams(-1,ui.dp(52)));
TextView note=text(L10n.tr("本地编辑,输入不会发送到远端"),12);note.setPadding(ui.dp(16),ui.dp(8),ui.dp(16),ui.dp(8));root.addView(note);return root;
}
private LinearLayout scrollBody(LinearLayout root){ScrollView scroll=new ScrollView(context);root.addView(scroll,new LinearLayout.LayoutParams(-1,0,1));LinearLayout body=ui.column();body.setPadding(ui.dp(16),ui.dp(8),ui.dp(16),ui.dp(20));scroll.addView(body);return body;}
private void present(View root){
if(manager==null){manager=new Dialog(context);manager.requestWindowFeature(Window.FEATURE_NO_TITLE);manager.setOnDismissListener(d->{manager=null;refresh();});}
hideEditor();manager.setContentView(root);manager.show();Window window=manager.getWindow();if(window!=null){window.setLayout(-1,-1);window.setBackgroundDrawableResource(android.R.color.transparent);window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);}
}
private void manage(){
LinearLayout root=screen(L10n.tr("管理快捷键"),this::close,L10n.tr("新建"),()->edit(null));LinearLayout body=scrollBody(root);body.addView(text(RemoteShortcuts.platformName(platform)+" · "+L10n.tr("键盘上方常用栏"),14));
List<String> selected=store.pins(platform);
for(RemoteShortcuts.Item item:store.pinned(platform)){
LinearLayout row=ui.row();row.setGravity(Gravity.CENTER_VERTICAL);TextView label=text(item.title()+"\n"+item.chord(),13);row.addView(label,new LinearLayout.LayoutParams(0,-2,1));
TextView up=button("↑",()->{List<String> list=store.pins(platform);int index=list.indexOf(item.id);if(index>0){java.util.Collections.swap(list,index,index-1);store.pins(platform,list);manage();}});up.setContentDescription(L10n.tr("向上移动"));up.setEnabled(selected.indexOf(item.id)>0);row.addView(up);
row.addView(button(L10n.tr("移除"),()->{List<String> list=store.pins(platform);list.remove(item.id);store.pins(platform,list);manage();}));body.addView(row);ui.divider(body);
}
ui.gap(body,16);body.addView(text(L10n.tr("添加到常用栏"),14));
for(RemoteShortcuts.Item item:store.items(platform)){
LinearLayout row=ui.row();row.setGravity(Gravity.CENTER_VERTICAL);TextView label=text(item.title()+"\n"+item.chord(),13);row.addView(label,new LinearLayout.LayoutParams(0,-2,1));
if(!selected.contains(item.id))row.addView(button("+",()->{List<String> list=store.pins(platform);if(list.size()>=RemoteShortcuts.MAX_PINNED){Toast.makeText(context,L10n.tr("最多固定 8 个快捷键"),Toast.LENGTH_SHORT).show();return;}list.add(item.id);store.pins(platform,list);manage();}));
if(item.custom){row.addView(button(L10n.tr("编辑"),()->edit(item)));row.addView(button(L10n.tr("删除"),()->{store.remove(item.id);manage();}));}
body.addView(row);
}
present(root);
}
private void edit(RemoteShortcuts.Item existing){
EditText name=new EditText(context);name.setTextColor(ink());name.setSingleLine();name.setText(existing==null?"":existing.name);name.setHint(L10n.tr("名称"));name.setFilters(new android.text.InputFilter[]{new android.text.InputFilter.LengthFilter(64)});
final String[] target={existing==null?platform:existing.platform};final int[] key={existing==null?83:existing.key};List<Integer> modifiers=new ArrayList<>();for(int code:existing==null?new int[]{platform.equals("macos")?91:17}:existing.modifiers)modifiers.add(code);
CheckBox pin=new CheckBox(context);pin.setTextColor(ink());pin.setText(L10n.tr("固定到键盘上方"));pin.setChecked(existing==null?store.pins(target[0]).size()<RemoteShortcuts.MAX_PINNED:store.pins(existing.platform).contains(existing.id));
TextView error=text("",13);error.setTextColor(0xFFE45757);TextView preview=text("",16);
Runnable update=()->preview.setText(new RemoteShortcuts.Item("","",target[0],"mine",key[0],RemoteShortcuts.normalize(modifiers.stream().mapToInt(Integer::intValue).toArray()),false,true).chord());
LinearLayout root=screen(L10n.tr(existing==null?"新建快捷键":"编辑快捷键"),this::manage,L10n.tr("保存"),()->{String result=store.save(existing==null?null:existing.id,name.getText().toString(),target[0],key[0],modifiers.stream().mapToInt(Integer::intValue).toArray(),pin.isChecked());if(result.isEmpty())manage();else error.setText(result);});
LinearLayout body=scrollBody(root);body.addView(name,new LinearLayout.LayoutParams(-1,ui.dp(52)));ui.gap(body,12);
LinearLayout modifierRow=ui.row();CheckBox[] toggles=new CheckBox[4];for(int i=0;i<4;i++){final int code=RemoteShortcuts.MODIFIERS[i];CheckBox toggle=new CheckBox(context);toggle.setTextColor(ink());toggle.setTextSize(12);toggle.setText(RemoteShortcuts.modifierName(code,target[0]));toggle.setChecked(modifiers.contains(code));toggle.setOnCheckedChangeListener((v,checked)->{if(checked){if(!modifiers.contains(code))modifiers.add(code);}else modifiers.remove(Integer.valueOf(code));update.run();});toggles[i]=toggle;modifierRow.addView(toggle,new LinearLayout.LayoutParams(0,ui.dp(48),1));}
List<Integer> keys=RemoteShortcuts.keys();
Spinner mainKey=spinner(keys.stream().map(code->RemoteShortcuts.keyName(code,target[0])).toArray(String[]::new),keys.indexOf(key[0]),index->{key[0]=keys.get(index);update.run();});
body.addView(text(L10n.tr("适用远端"),13));body.addView(spinner(new String[]{"Windows","macOS","Linux"},Arrays.asList(RemoteShortcuts.PLATFORMS).indexOf(target[0]),index->{target[0]=RemoteShortcuts.PLATFORMS[index];mainKey.setAdapter(new ArrayAdapter<>(context,android.R.layout.simple_spinner_dropdown_item,keys.stream().map(code->RemoteShortcuts.keyName(code,target[0])).toArray(String[]::new)));mainKey.setSelection(keys.indexOf(key[0]));for(int i=0;i<4;i++)toggles[i].setText(RemoteShortcuts.modifierName(RemoteShortcuts.MODIFIERS[i],target[0]));update.run();}),new LinearLayout.LayoutParams(-1,ui.dp(48)));
body.addView(text(L10n.tr("主按键"),13));body.addView(mainKey,new LinearLayout.LayoutParams(-1,ui.dp(48)));
body.addView(text(L10n.tr("修饰键"),13));body.addView(modifierRow);body.addView(text(L10n.tr("发送组合"),13));body.addView(preview);body.addView(pin);body.addView(error);update.run();present(root);
}
}
@@ -0,0 +1,99 @@
package cn.crossdesk.mobile;
import android.content.Context;
import android.content.SharedPreferences;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
/** Local controller preferences. Key codes use the existing cross-platform wire mapping. */
final class RemoteShortcuts {
static final int MAX_CUSTOM=16,MAX_PINNED=8;
static final String[] PLATFORMS={"windows","macos","linux"};
static final int[] MODIFIERS={17,16,18,91};
static final class Item {
final String id,name,platform,group;
final int key;
final int[] modifiers;
final boolean service,custom;
Item(String id,String name,String platform,String group,int key,int[] modifiers,boolean service,boolean custom){
this.id=id;this.name=name;this.platform=platform;this.group=group;this.key=key;
this.modifiers=modifiers.clone();this.service=service;this.custom=custom;
}
String title(){return custom?name:L10n.tr(name);}
String chord(){List<String> parts=new ArrayList<>();for(int modifier:modifiers)parts.add(modifierName(modifier,platform));parts.add(keyName(key,platform));return String.join(" + ",parts);}
}
private final SharedPreferences preferences;
RemoteShortcuts(Context context){preferences=context.getSharedPreferences("remote_shortcuts_v1",Context.MODE_PRIVATE);}
static String platform(String value){return Arrays.asList(PLATFORMS).contains(value)?value:"unknown";}
static String platformName(String value){return switch(value){case "windows"->"Windows";case "macos"->"macOS";case "linux"->"Linux";default->L10n.tr("未知");};}
String desktop(){String value=preferences.getString("linux_desktop","");return value.equals("gnome")||value.equals("kde")?value:"";}
void desktop(String value){preferences.edit().putString("linux_desktop",value.equals("gnome")||value.equals("kde")?value:"").apply();}
static String modifierName(int code,String platform){return switch(code){case 17->"Ctrl";case 16->"Shift";case 18->platform.equals("macos")?"Option":"Alt";case 91->platform.equals("macos")?"Cmd":platform.equals("linux")?"Super":"Win";default->"";};}
static String keyName(int code,String platform){
if(code>=65&&code<=90||code>=48&&code<=57)return Character.toString((char)code);
if(code>=112&&code<=123)return "F"+(code-111);
return switch(code){case 8->platform.equals("macos")?"Delete ⌫":"Backspace";case 9->"Tab";case 13->"Enter";case 27->"Esc";case 32->"Space";case 33->"PageUp";case 34->"PageDown";case 35->"End";case 36->"Home";case 37->"←";case 38->"↑";case 39->"→";case 40->"↓";case 45->"Insert";case 46->platform.equals("macos")?"Delete ⌦":"Delete";default->"";};
}
static List<Integer> keys(){List<Integer> result=new ArrayList<>();for(int i=65;i<=90;i++)result.add(i);for(int i=48;i<=57;i++)result.add(i);for(int i=112;i<=123;i++)result.add(i);for(int i:new int[]{27,9,13,32,8,46,45,36,35,33,34,37,38,39,40})result.add(i);return result;}
static int[] normalize(int[] modifiers){return Arrays.stream(MODIFIERS).filter(code->Arrays.stream(modifiers).anyMatch(value->value==code)).toArray();}
private static Item preset(String id,String name,String platform,String group,int key,int... modifiers){return new Item(id,name,platform,group,key,modifiers,false,false);}
List<Item> items(String remote){
String os=platform(remote);List<Item> result=new ArrayList<>();
if(!os.equals("unknown")){
int primary=os.equals("macos")?91:17;
String[] ids={"copy","paste","cut","all","undo","save"},names={"复制","粘贴","剪切","全选","撤销","保存"};int[] keys={67,86,88,65,90,83};
for(int i=0;i<ids.length;i++)result.add(preset(ids[i],names[i],os,"common",keys[i],primary));
if(os.equals("linux")){result.add(preset("term-copy","终端复制",os,"common",67,17,16));result.add(preset("term-paste","终端粘贴",os,"common",86,17,16));}
}
for(int key:new int[]{27,9,13,8,46,36,35,33,34,37,38,39,40,112,113,114,115,116,117,118,119,120,121,122,123})
result.add(preset(key==27?"esc":key==9?"tab":"key-"+key,keyName(key,os),os,"keys",key));
if(os.equals("windows")){
result.add(preset("switch","切换窗口",os,"system",9,18));result.add(preset("desktop","显示桌面",os,"system",68,91));result.add(preset("task","任务管理器",os,"system",27,17,16));
result.add(new Item("sas","安全选项",os,"system",46,new int[]{17,18},true,false));
}else if(os.equals("macos")){
result.add(preset("switch","切换应用",os,"system",9,91));result.add(preset("search","聚焦搜索",os,"system",32,91));result.add(preset("forcequit","强制退出面板",os,"system",27,18,91));
}else if(os.equals("linux")&&!desktop().isEmpty()){
result.add(preset("switch",desktop().equals("gnome")?"切换应用":"切换窗口",os,"system",9,desktop().equals("gnome")?91:18));result.add(preset("run","运行窗口",os,"system",113,18));
result.add(desktop().equals("gnome")?preset("apps","应用列表",os,"system",65,91):preset("window-menu","窗口菜单",os,"system",114,18));
}
for(Item item:custom())if(item.platform.equals(os))result.add(item);
return result;
}
List<Item> custom(){
List<Item> result=new ArrayList<>();
try{JSONArray entries=new JSONArray(preferences.getString("custom","[]"));for(int i=0;i<entries.length()&&result.size()<MAX_CUSTOM;i++){
JSONObject entry=entries.optJSONObject(i);if(entry==null)continue;String os=platform(entry.optString("platform")),id=entry.optString("id"),name=entry.optString("name").trim();int key=entry.optInt("key");JSONArray modifiers=entry.optJSONArray("modifiers");
if(os.equals("unknown")||!id.startsWith("custom-")||name.isEmpty()||name.length()>64||!keys().contains(key)||modifiers==null||modifiers.length()>4)continue;
int[] values=new int[modifiers.length()];boolean valid=true;for(int j=0;j<values.length;j++){int value=modifiers.optInt(j);values[j]=value;if(!Arrays.stream(MODIFIERS).anyMatch(code->code==value))valid=false;}
if(!valid)continue;int[] normalized=normalize(values);if(os.equals("windows")&&key==46&&Arrays.stream(normalized).anyMatch(v->v==17)&&Arrays.stream(normalized).anyMatch(v->v==18))continue;
if(result.stream().anyMatch(item->item.id.equals(id)))continue;
result.add(new Item(id,name,os,"mine",key,normalized,false,true));
}}catch(Exception ignored){ }
return result;
}
List<String> pins(String os){
List<String> result=new ArrayList<>();String stored=preferences.getString("pins_"+platform(os),null);
if(stored==null)stored="[\"esc\",\"tab\",\"copy\",\"paste\"]";
try{JSONArray array=new JSONArray(stored);for(int i=0;i<array.length()&&result.size()<MAX_PINNED;i++){String id=array.optString(i);if(!id.isEmpty()&&!result.contains(id))result.add(id);}}catch(Exception ignored){result.addAll(Arrays.asList("esc","tab","copy","paste"));}
List<Item> available=items(os);result.removeIf(id->available.stream().noneMatch(item->item.id.equals(id)));
return result;
}
List<Item> pinned(String os){List<Item> all=items(os),result=new ArrayList<>();for(String id:pins(os))for(Item item:all)if(item.id.equals(id)){result.add(item);break;}return result;}
void pins(String os,List<String> values){preferences.edit().putString("pins_"+platform(os),new JSONArray(values.subList(0,Math.min(values.size(),MAX_PINNED))).toString()).apply();}
void remove(String id){List<Item> values=custom();values.removeIf(item->item.id.equals(id));write(values);for(String os:PLATFORMS){List<String> selected=pins(os);selected.remove(id);pins(os,selected);}}
String save(String id,String name,String os,int key,int[] modifiers,boolean pin){
name=name.trim();if(name.isEmpty()||name.length()>64)return L10n.tr("请输入有效的快捷键名称");
if(platform(os).equals("unknown")||!keys().contains(key))return L10n.tr("请选择有效的按键");
int[] normalized=normalize(modifiers);if(os.equals("windows")&&key==46&&Arrays.stream(normalized).anyMatch(v->v==17)&&Arrays.stream(normalized).anyMatch(v->v==18))return L10n.tr("请使用系统预设中的安全选项");
List<Item> values=custom();boolean exists=false;for(Item item:values){if(item.id.equals(id))exists=true;else if(item.platform.equals(os)&&item.key==key&&Arrays.equals(item.modifiers,normalized))return L10n.tr("已有相同组合的快捷键");}
if(!exists&&values.size()>=MAX_CUSTOM)return L10n.tr("最多保存 16 个自定义快捷键");
List<String> selected=pins(os);if(pin&&!selected.contains(id)&&selected.size()>=MAX_PINNED)return L10n.tr("最多固定 8 个快捷键");
String itemId=id==null?"custom-"+UUID.randomUUID():id;values.removeIf(item->item.id.equals(itemId));values.add(new Item(itemId,name,os,"mine",key,normalized,false,true));write(values);
for(String target:PLATFORMS){List<String> list=pins(target);int index=list.indexOf(itemId);list.remove(itemId);if(target.equals(os)&&pin)list.add(index>=0?Math.min(index,list.size()):list.size(),itemId);pins(target,list);}return "";
}
private void write(List<Item> items){JSONArray array=new JSONArray();try{for(Item item:items){JSONArray modifiers=new JSONArray();for(int code:item.modifiers)modifiers.put(code);array.put(new JSONObject().put("id",item.id).put("name",item.name).put("platform",item.platform).put("key",item.key).put("modifiers",modifiers));}}catch(Exception ignored){return;}preferences.edit().putString("custom",array.toString()).apply();}
}
@@ -72,6 +72,7 @@ final class SessionControls extends FrameLayout {
}
void setRelative(boolean value){relative=value;if(menu!=null&&menuPage.equals("controls"))openMenu("controls");}
void setHostPlatform(String platform){
keyboard.setPlatform(platform);
boolean visible="windows".equals(platform);
if(secureAttentionVisible==visible)return;
secureAttentionVisible=visible;
@@ -315,4 +315,59 @@
<string name="text_cbacf1eafbca53e0" formatted="false">"配对信息已复制。"</string>
<string name="text_cde42ccdeacbbbd9" formatted="false">"无法复制配对信息,请重试。"</string>
<string name="text_7383b2ae09252c59" formatted="false">"安全存储不可用,请解锁设备后重试"</string>
<string name="text_c9b9ae7a61444ab7" formatted="false">"上一页"</string>
<string name="text_8a8542f6964852dc" formatted="false">"下一页"</string>
<string name="text_0c4f690eb398c12e" formatted="false">"主按键"</string>
<string name="text_62fdb5304cf5cd9b" formatted="false">"任务管理器"</string>
<string name="text_a3030bf8f16dc63c" formatted="false">"保存"</string>
<string name="text_ba0e2ff8201bc6a6" formatted="false">"修饰键"</string>
<string name="text_3a5040b68abf75f9" formatted="false">"全选"</string>
<string name="text_c2d918002afcc3d4" formatted="false">"切换应用"</string>
<string name="text_a7fe8e2685849c31" formatted="false">"切换窗口"</string>
<string name="text_410a8e8a6bf253ac" formatted="false">"剪切"</string>
<string name="text_6309cd49db59d165" formatted="false">"发送组合"</string>
<string name="text_d44e9b3d3b31d37b" formatted="false">"名称"</string>
<string name="text_6789dd11afc7c4c5" formatted="false">"向上移动"</string>
<string name="text_352c003784b1c89e" formatted="false">"固定到键盘上方"</string>
<string name="text_63d90d977348ab1f" formatted="false">"复制"</string>
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"安全选项"</string>
<string name="text_dd1b1da65a6c89de" formatted="false">"已有相同组合的快捷键"</string>
<string name="text_4563c57b5143deaa" formatted="false">"常用"</string>
<string name="text_6308bf1373bd0ee8" formatted="false">"应用列表"</string>
<string name="text_8dc0823d700cf875" formatted="false">"强制退出面板"</string>
<string name="text_ee2638183d3ea860" formatted="false">"快捷键"</string>
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"我的"</string>
<string name="text_7c7577d4d4fc6009" formatted="false">"按键"</string>
<string name="text_926a50b98ece2667" formatted="false">"撤销"</string>
<string name="text_50ef2f4cf6a46924" formatted="false">"新建"</string>
<string name="text_c6904bb36f7f62c5" formatted="false">"新建快捷键"</string>
<string name="text_5cfe4846aa8a7228" formatted="false">"显示桌面"</string>
<string name="text_feb12adf8e0315e2" formatted="false">"更多快捷键"</string>
<string name="text_5d6bce93c0649a63" formatted="false">"最多保存 16 个自定义快捷键"</string>
<string name="text_f33cb21422f62ee8" formatted="false">"最多固定 8 个快捷键"</string>
<string name="text_4d8c1c5b42830791" formatted="false">"未知"</string>
<string name="text_6b1fa1d7159de777" formatted="false">"本地编辑,输入不会发送到远端"</string>
<string name="text_cb9eaa63c25d383e" formatted="false">"添加到常用栏"</string>
<string name="text_6135d4159e892541" formatted="false">"移除"</string>
<string name="text_e693ff5832c7f46d" formatted="false">"窗口菜单"</string>
<string name="text_096a247cc5455ba7" formatted="false">"等待远端平台信息"</string>
<string name="text_39c7740197c41d44" formatted="false">"管理与自定义"</string>
<string name="text_2dc8c27b75796b36" formatted="false">"管理快捷键"</string>
<string name="text_33517926747180e6" formatted="false">"粘贴"</string>
<string name="text_5b50d7c4b5950dc5" formatted="false">"系统"</string>
<string name="text_45123fa07e22fb3e" formatted="false">"终端复制"</string>
<string name="text_3936f3d30a763a35" formatted="false">"终端粘贴"</string>
<string name="text_051836569928a9f9" formatted="false">"编辑"</string>
<string name="text_0ecda10acc934f22" formatted="false">"编辑快捷键"</string>
<string name="text_d32d2a4e0e83d7cf" formatted="false">"聚焦搜索"</string>
<string name="text_add54b3ce4b1c661" formatted="false">"请使用系统预设中的安全选项"</string>
<string name="text_ae066f34095261c9" formatted="false">"请先选词或取消组词"</string>
<string name="text_a895bdb6f1f11e99" formatted="false">"请输入有效的快捷键名称"</string>
<string name="text_6b3213d2afed99e8" formatted="false">"请选择有效的按键"</string>
<string name="text_819569ec77acb01e" formatted="false">"运行窗口"</string>
<string name="text_22c4107f03c3533e" formatted="false">"还没有自定义快捷键"</string>
<string name="text_b07f98d4671af3f9" formatted="false">"适用远端"</string>
<string name="text_4bdabfba3368869d" formatted="false">"选择桌面方案"</string>
<string name="text_4fa28857c6f253cc" formatted="false">"选择桌面方案以加载系统预设"</string>
<string name="text_fc5f6f7462e8557a" formatted="false">"键盘上方常用栏"</string>
</resources>
@@ -315,4 +315,59 @@
<string name="text_cbacf1eafbca53e0" formatted="false">"配對資訊已複製。"</string>
<string name="text_cde42ccdeacbbbd9" formatted="false">"無法複製配對資訊,請重試。"</string>
<string name="text_7383b2ae09252c59" formatted="false">"安全儲存不可用,請解鎖裝置後重試"</string>
<string name="text_c9b9ae7a61444ab7" formatted="false">"上一頁"</string>
<string name="text_8a8542f6964852dc" formatted="false">"下一頁"</string>
<string name="text_0c4f690eb398c12e" formatted="false">"主按鍵"</string>
<string name="text_62fdb5304cf5cd9b" formatted="false">"工作管理員"</string>
<string name="text_a3030bf8f16dc63c" formatted="false">"儲存"</string>
<string name="text_ba0e2ff8201bc6a6" formatted="false">"修飾鍵"</string>
<string name="text_3a5040b68abf75f9" formatted="false">"全選"</string>
<string name="text_c2d918002afcc3d4" formatted="false">"切換應用程式"</string>
<string name="text_a7fe8e2685849c31" formatted="false">"切換視窗"</string>
<string name="text_410a8e8a6bf253ac" formatted="false">"剪下"</string>
<string name="text_6309cd49db59d165" formatted="false">"傳送組合"</string>
<string name="text_d44e9b3d3b31d37b" formatted="false">"名稱"</string>
<string name="text_6789dd11afc7c4c5" formatted="false">"向上移動"</string>
<string name="text_352c003784b1c89e" formatted="false">"固定到鍵盤上方"</string>
<string name="text_63d90d977348ab1f" formatted="false">"複製"</string>
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"安全性選項"</string>
<string name="text_dd1b1da65a6c89de" formatted="false">"已有相同組合的快捷鍵"</string>
<string name="text_4563c57b5143deaa" formatted="false">"常用"</string>
<string name="text_6308bf1373bd0ee8" formatted="false">"應用程式列表"</string>
<string name="text_8dc0823d700cf875" formatted="false">"強制結束面板"</string>
<string name="text_ee2638183d3ea860" formatted="false">"快速鍵"</string>
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"我的"</string>
<string name="text_7c7577d4d4fc6009" formatted="false">"按鍵"</string>
<string name="text_926a50b98ece2667" formatted="false">"復原"</string>
<string name="text_50ef2f4cf6a46924" formatted="false">"新增"</string>
<string name="text_c6904bb36f7f62c5" formatted="false">"新增快速鍵"</string>
<string name="text_5cfe4846aa8a7228" formatted="false">"顯示桌面"</string>
<string name="text_feb12adf8e0315e2" formatted="false">"更多快速鍵"</string>
<string name="text_5d6bce93c0649a63" formatted="false">"最多儲存 16 個自訂快速鍵"</string>
<string name="text_f33cb21422f62ee8" formatted="false">"最多固定 8 個快速鍵"</string>
<string name="text_4d8c1c5b42830791" formatted="false">"未知"</string>
<string name="text_6b1fa1d7159de777" formatted="false">"本機編輯,輸入不會傳送到遠端"</string>
<string name="text_cb9eaa63c25d383e" formatted="false">"加入常用列"</string>
<string name="text_6135d4159e892541" formatted="false">"移除"</string>
<string name="text_e693ff5832c7f46d" formatted="false">"視窗選單"</string>
<string name="text_096a247cc5455ba7" formatted="false">"等待遠端平台資訊"</string>
<string name="text_39c7740197c41d44" formatted="false">"管理與自訂"</string>
<string name="text_2dc8c27b75796b36" formatted="false">"管理快速鍵"</string>
<string name="text_33517926747180e6" formatted="false">"貼上"</string>
<string name="text_5b50d7c4b5950dc5" formatted="false">"系統"</string>
<string name="text_45123fa07e22fb3e" formatted="false">"終端機複製"</string>
<string name="text_3936f3d30a763a35" formatted="false">"終端機貼上"</string>
<string name="text_051836569928a9f9" formatted="false">"編輯"</string>
<string name="text_0ecda10acc934f22" formatted="false">"編輯快速鍵"</string>
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Spotlight 搜尋"</string>
<string name="text_add54b3ce4b1c661" formatted="false">"請使用系統預設中的安全性選項"</string>
<string name="text_ae066f34095261c9" formatted="false">"請先選字或取消組字"</string>
<string name="text_a895bdb6f1f11e99" formatted="false">"請輸入有效的快速鍵名稱"</string>
<string name="text_6b3213d2afed99e8" formatted="false">"請選擇有效的按鍵"</string>
<string name="text_819569ec77acb01e" formatted="false">"執行視窗"</string>
<string name="text_22c4107f03c3533e" formatted="false">"尚無自訂快速鍵"</string>
<string name="text_b07f98d4671af3f9" formatted="false">"適用遠端"</string>
<string name="text_4bdabfba3368869d" formatted="false">"選擇桌面環境"</string>
<string name="text_4fa28857c6f253cc" formatted="false">"選擇桌面環境以載入系統預設"</string>
<string name="text_fc5f6f7462e8557a" formatted="false">"鍵盤上方常用列"</string>
</resources>
@@ -315,4 +315,59 @@
<string name="text_cbacf1eafbca53e0" formatted="false">"Kopplungsdaten kopiert."</string>
<string name="text_cde42ccdeacbbbd9" formatted="false">"Kopplungsdaten konnten nicht kopiert werden. Bitte erneut versuchen."</string>
<string name="text_7383b2ae09252c59" formatted="false">"Der sichere Speicher ist nicht verfügbar. Bitte entsperren Sie das Gerät und versuchen Sie es erneut"</string>
<string name="text_c9b9ae7a61444ab7" formatted="false">"Vorherige Seite"</string>
<string name="text_8a8542f6964852dc" formatted="false">"Nächste Seite"</string>
<string name="text_0c4f690eb398c12e" formatted="false">"Haupttaste"</string>
<string name="text_62fdb5304cf5cd9b" formatted="false">"Task-Manager"</string>
<string name="text_a3030bf8f16dc63c" formatted="false">"Speichern"</string>
<string name="text_ba0e2ff8201bc6a6" formatted="false">"Modifikatortasten"</string>
<string name="text_3a5040b68abf75f9" formatted="false">"Alles auswählen"</string>
<string name="text_c2d918002afcc3d4" formatted="false">"Apps wechseln"</string>
<string name="text_a7fe8e2685849c31" formatted="false">"Fenster wechseln"</string>
<string name="text_410a8e8a6bf253ac" formatted="false">"Ausschneiden"</string>
<string name="text_6309cd49db59d165" formatted="false">"Tastenkombination"</string>
<string name="text_d44e9b3d3b31d37b" formatted="false">"Name"</string>
<string name="text_6789dd11afc7c4c5" formatted="false">"Nach oben"</string>
<string name="text_352c003784b1c89e" formatted="false">"Über der Tastatur anheften"</string>
<string name="text_63d90d977348ab1f" formatted="false">"Kopieren"</string>
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"Sicherheitsoptionen"</string>
<string name="text_dd1b1da65a6c89de" formatted="false">"Diese Kombination ist bereits vorhanden"</string>
<string name="text_4563c57b5143deaa" formatted="false">"Favoriten"</string>
<string name="text_6308bf1373bd0ee8" formatted="false">"App-Liste"</string>
<string name="text_8dc0823d700cf875" formatted="false">"Dialog zum sofortigen Beenden"</string>
<string name="text_ee2638183d3ea860" formatted="false">"Tastenkürzel"</string>
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"Eigene"</string>
<string name="text_7c7577d4d4fc6009" formatted="false">"Tasten"</string>
<string name="text_926a50b98ece2667" formatted="false">"Rückgängig"</string>
<string name="text_50ef2f4cf6a46924" formatted="false">"Neu"</string>
<string name="text_c6904bb36f7f62c5" formatted="false">"Neues Tastenkürzel"</string>
<string name="text_5cfe4846aa8a7228" formatted="false">"Desktop anzeigen"</string>
<string name="text_feb12adf8e0315e2" formatted="false">"Weitere Tastenkürzel"</string>
<string name="text_5d6bce93c0649a63" formatted="false">"Bis zu 16 eigene Tastenkürzel speichern"</string>
<string name="text_f33cb21422f62ee8" formatted="false">"Bis zu 8 Tastenkürzel anheften"</string>
<string name="text_4d8c1c5b42830791" formatted="false">"Unbekannt"</string>
<string name="text_6b1fa1d7159de777" formatted="false">"Lokale Bearbeitung. Eingaben bleiben auf diesem Gerät."</string>
<string name="text_cb9eaa63c25d383e" formatted="false">"Zur Favoritenleiste hinzufügen"</string>
<string name="text_6135d4159e892541" formatted="false">"Entfernen"</string>
<string name="text_e693ff5832c7f46d" formatted="false">"Fenstermenü"</string>
<string name="text_096a247cc5455ba7" formatted="false">"Warten auf Informationen zur Remote-Plattform"</string>
<string name="text_39c7740197c41d44" formatted="false">"Verwalten und anpassen"</string>
<string name="text_2dc8c27b75796b36" formatted="false">"Tastenkürzel verwalten"</string>
<string name="text_33517926747180e6" formatted="false">"Einfügen"</string>
<string name="text_5b50d7c4b5950dc5" formatted="false">"System"</string>
<string name="text_45123fa07e22fb3e" formatted="false">"Im Terminal kopieren"</string>
<string name="text_3936f3d30a763a35" formatted="false">"Im Terminal einfügen"</string>
<string name="text_051836569928a9f9" formatted="false">"Bearbeiten"</string>
<string name="text_0ecda10acc934f22" formatted="false">"Tastenkürzel bearbeiten"</string>
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Spotlight-Suche"</string>
<string name="text_add54b3ce4b1c661" formatted="false">"Sicherheitsoptionen in den Systemvorgaben verwenden"</string>
<string name="text_ae066f34095261c9" formatted="false">"Zuerst die aktuelle Texteingabe bestätigen oder abbrechen"</string>
<string name="text_a895bdb6f1f11e99" formatted="false">"Namen mit 1–64 Zeichen eingeben"</string>
<string name="text_6b3213d2afed99e8" formatted="false">"Eine gültige Taste auswählen"</string>
<string name="text_819569ec77acb01e" formatted="false">"Ausführen-Dialog"</string>
<string name="text_22c4107f03c3533e" formatted="false">"Noch keine eigenen Tastenkürzel"</string>
<string name="text_b07f98d4671af3f9" formatted="false">"Remote-Plattform"</string>
<string name="text_4bdabfba3368869d" formatted="false">"Desktop-Umgebung auswählen"</string>
<string name="text_4fa28857c6f253cc" formatted="false">"Desktop-Umgebung wählen, um Systemvorgaben zu laden"</string>
<string name="text_fc5f6f7462e8557a" formatted="false">"Favoriten über der Tastatur"</string>
</resources>
@@ -315,4 +315,59 @@
<string name="text_cbacf1eafbca53e0" formatted="false">"Información de emparejamiento copiada."</string>
<string name="text_cde42ccdeacbbbd9" formatted="false">"No se pudo copiar la información de emparejamiento. Inténtelo de nuevo."</string>
<string name="text_7383b2ae09252c59" formatted="false">"El almacenamiento seguro no está disponible. Desbloquee el dispositivo e inténtelo nuevamente."</string>
<string name="text_c9b9ae7a61444ab7" formatted="false">"Página anterior"</string>
<string name="text_8a8542f6964852dc" formatted="false">"Página siguiente"</string>
<string name="text_0c4f690eb398c12e" formatted="false">"Tecla principal"</string>
<string name="text_62fdb5304cf5cd9b" formatted="false">"Administrador de tareas"</string>
<string name="text_a3030bf8f16dc63c" formatted="false">"Guardar"</string>
<string name="text_ba0e2ff8201bc6a6" formatted="false">"Modificadores"</string>
<string name="text_3a5040b68abf75f9" formatted="false">"Seleccionar todo"</string>
<string name="text_c2d918002afcc3d4" formatted="false">"Cambiar de aplicación"</string>
<string name="text_a7fe8e2685849c31" formatted="false">"Cambiar de ventana"</string>
<string name="text_410a8e8a6bf253ac" formatted="false">"Cortar"</string>
<string name="text_6309cd49db59d165" formatted="false">"Combinación de teclas"</string>
<string name="text_d44e9b3d3b31d37b" formatted="false">"Nombre"</string>
<string name="text_6789dd11afc7c4c5" formatted="false">"Subir"</string>
<string name="text_352c003784b1c89e" formatted="false">"Fijar sobre el teclado"</string>
<string name="text_63d90d977348ab1f" formatted="false">"Copiar"</string>
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"Opciones de seguridad"</string>
<string name="text_dd1b1da65a6c89de" formatted="false">"Esta combinación ya existe"</string>
<string name="text_4563c57b5143deaa" formatted="false">"Favoritos"</string>
<string name="text_6308bf1373bd0ee8" formatted="false">"Lista de aplicaciones"</string>
<string name="text_8dc0823d700cf875" formatted="false">"Diálogo Forzar salida"</string>
<string name="text_ee2638183d3ea860" formatted="false">"Atajos"</string>
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"Mis atajos"</string>
<string name="text_7c7577d4d4fc6009" formatted="false">"Teclas"</string>
<string name="text_926a50b98ece2667" formatted="false">"Deshacer"</string>
<string name="text_50ef2f4cf6a46924" formatted="false">"Nuevo"</string>
<string name="text_c6904bb36f7f62c5" formatted="false">"Nuevo atajo"</string>
<string name="text_5cfe4846aa8a7228" formatted="false">"Mostrar escritorio"</string>
<string name="text_feb12adf8e0315e2" formatted="false">"Más atajos"</string>
<string name="text_5d6bce93c0649a63" formatted="false">"Guarda hasta 16 atajos personalizados"</string>
<string name="text_f33cb21422f62ee8" formatted="false">"Fija hasta 8 atajos"</string>
<string name="text_4d8c1c5b42830791" formatted="false">"Desconocido"</string>
<string name="text_6b1fa1d7159de777" formatted="false">"Edición local. Lo escrito permanece en este dispositivo."</string>
<string name="text_cb9eaa63c25d383e" formatted="false">"Añadir a la barra de favoritos"</string>
<string name="text_6135d4159e892541" formatted="false">"Quitar"</string>
<string name="text_e693ff5832c7f46d" formatted="false">"Menú de ventana"</string>
<string name="text_096a247cc5455ba7" formatted="false">"Esperando información del sistema remoto"</string>
<string name="text_39c7740197c41d44" formatted="false">"Gestionar y personalizar"</string>
<string name="text_2dc8c27b75796b36" formatted="false">"Gestionar atajos"</string>
<string name="text_33517926747180e6" formatted="false">"Pegar"</string>
<string name="text_5b50d7c4b5950dc5" formatted="false">"Sistema"</string>
<string name="text_45123fa07e22fb3e" formatted="false">"Copiar en terminal"</string>
<string name="text_3936f3d30a763a35" formatted="false">"Pegar en terminal"</string>
<string name="text_051836569928a9f9" formatted="false">"Editar"</string>
<string name="text_0ecda10acc934f22" formatted="false">"Editar atajo"</string>
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Búsqueda de Spotlight"</string>
<string name="text_add54b3ce4b1c661" formatted="false">"Usa Opciones de seguridad en los ajustes del sistema"</string>
<string name="text_ae066f34095261c9" formatted="false">"Confirma o cancela primero la composición actual"</string>
<string name="text_a895bdb6f1f11e99" formatted="false">"Introduce un nombre de 1 a 64 caracteres"</string>
<string name="text_6b3213d2afed99e8" formatted="false">"Selecciona una tecla válida"</string>
<string name="text_819569ec77acb01e" formatted="false">"Diálogo Ejecutar"</string>
<string name="text_22c4107f03c3533e" formatted="false">"Aún no hay atajos personalizados"</string>
<string name="text_b07f98d4671af3f9" formatted="false">"Sistema remoto"</string>
<string name="text_4bdabfba3368869d" formatted="false">"Elegir entorno de escritorio"</string>
<string name="text_4fa28857c6f253cc" formatted="false">"Elige un entorno para cargar los ajustes del sistema"</string>
<string name="text_fc5f6f7462e8557a" formatted="false">"Favoritos sobre el teclado"</string>
</resources>
@@ -315,4 +315,59 @@
<string name="text_cbacf1eafbca53e0" formatted="false">"Informations d’association copiées."</string>
<string name="text_cde42ccdeacbbbd9" formatted="false">"Impossible de copier les informations d’association. Réessayez."</string>
<string name="text_7383b2ae09252c59" formatted="false">"Le stockage sécurisé n\'est pas disponible, veuillez déverrouiller l\'appareil et réessayer"</string>
<string name="text_c9b9ae7a61444ab7" formatted="false">"Page précédente"</string>
<string name="text_8a8542f6964852dc" formatted="false">"Page suivante"</string>
<string name="text_0c4f690eb398c12e" formatted="false">"Touche principale"</string>
<string name="text_62fdb5304cf5cd9b" formatted="false">"Gestionnaire des tâches"</string>
<string name="text_a3030bf8f16dc63c" formatted="false">"Enregistrer"</string>
<string name="text_ba0e2ff8201bc6a6" formatted="false">"Touches de modification"</string>
<string name="text_3a5040b68abf75f9" formatted="false">"Sélectionner tout"</string>
<string name="text_c2d918002afcc3d4" formatted="false">"Changer d’application"</string>
<string name="text_a7fe8e2685849c31" formatted="false">"Changer de fenêtre"</string>
<string name="text_410a8e8a6bf253ac" formatted="false">"Couper"</string>
<string name="text_6309cd49db59d165" formatted="false">"Combinaison à envoyer"</string>
<string name="text_d44e9b3d3b31d37b" formatted="false">"Nom"</string>
<string name="text_6789dd11afc7c4c5" formatted="false">"Monter"</string>
<string name="text_352c003784b1c89e" formatted="false">"Épingler au-dessus du clavier"</string>
<string name="text_63d90d977348ab1f" formatted="false">"Copier"</string>
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"Options de sécurité"</string>
<string name="text_dd1b1da65a6c89de" formatted="false">"Cette combinaison existe déjà"</string>
<string name="text_4563c57b5143deaa" formatted="false">"Favoris"</string>
<string name="text_6308bf1373bd0ee8" formatted="false">"Liste des applications"</string>
<string name="text_8dc0823d700cf875" formatted="false">"Fenêtre Forcer à quitter"</string>
<string name="text_ee2638183d3ea860" formatted="false">"Raccourcis"</string>
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"Mes raccourcis"</string>
<string name="text_7c7577d4d4fc6009" formatted="false">"Touches"</string>
<string name="text_926a50b98ece2667" formatted="false">"Annuler l’action"</string>
<string name="text_50ef2f4cf6a46924" formatted="false">"Nouveau"</string>
<string name="text_c6904bb36f7f62c5" formatted="false">"Nouveau raccourci"</string>
<string name="text_5cfe4846aa8a7228" formatted="false">"Afficher le bureau"</string>
<string name="text_feb12adf8e0315e2" formatted="false">"Autres raccourcis"</string>
<string name="text_5d6bce93c0649a63" formatted="false">"Enregistrez jusqu’à 16 raccourcis personnalisés"</string>
<string name="text_f33cb21422f62ee8" formatted="false">"Épinglez jusqu’à 8 raccourcis"</string>
<string name="text_4d8c1c5b42830791" formatted="false">"Inconnu"</string>
<string name="text_6b1fa1d7159de777" formatted="false">"Édition locale. La saisie reste sur cet appareil."</string>
<string name="text_cb9eaa63c25d383e" formatted="false">"Ajouter à la barre des favoris"</string>
<string name="text_6135d4159e892541" formatted="false">"Retirer"</string>
<string name="text_e693ff5832c7f46d" formatted="false">"Menu de la fenêtre"</string>
<string name="text_096a247cc5455ba7" formatted="false">"En attente des informations du système distant"</string>
<string name="text_39c7740197c41d44" formatted="false">"Gérer et personnaliser"</string>
<string name="text_2dc8c27b75796b36" formatted="false">"Gérer les raccourcis"</string>
<string name="text_33517926747180e6" formatted="false">"Coller"</string>
<string name="text_5b50d7c4b5950dc5" formatted="false">"Système"</string>
<string name="text_45123fa07e22fb3e" formatted="false">"Copier dans le terminal"</string>
<string name="text_3936f3d30a763a35" formatted="false">"Coller dans le terminal"</string>
<string name="text_051836569928a9f9" formatted="false">"Modifier"</string>
<string name="text_0ecda10acc934f22" formatted="false">"Modifier le raccourci"</string>
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Recherche Spotlight"</string>
<string name="text_add54b3ce4b1c661" formatted="false">"Utilisez les options de sécurité des préréglages système"</string>
<string name="text_ae066f34095261c9" formatted="false">"Validez ou annulez d’abord la composition en cours"</string>
<string name="text_a895bdb6f1f11e99" formatted="false">"Saisissez un nom de 1 à 64 caractères"</string>
<string name="text_6b3213d2afed99e8" formatted="false">"Sélectionnez une touche valide"</string>
<string name="text_819569ec77acb01e" formatted="false">"Fenêtre Exécuter"</string>
<string name="text_22c4107f03c3533e" formatted="false">"Aucun raccourci personnalisé"</string>
<string name="text_b07f98d4671af3f9" formatted="false">"Système distant"</string>
<string name="text_4bdabfba3368869d" formatted="false">"Choisir l’environnement de bureau"</string>
<string name="text_4fa28857c6f253cc" formatted="false">"Choisissez un environnement pour charger les préréglages système"</string>
<string name="text_fc5f6f7462e8557a" formatted="false">"Favoris au-dessus du clavier"</string>
</resources>
@@ -315,4 +315,59 @@
<string name="text_cbacf1eafbca53e0" formatted="false">"ペアリング情報をコピーしました。"</string>
<string name="text_cde42ccdeacbbbd9" formatted="false">"ペアリング情報をコピーできません。もう一度お試しください。"</string>
<string name="text_7383b2ae09252c59" formatted="false">"安全なストレージが利用できません。デバイスのロックを解除して、もう一度お試しください。"</string>
<string name="text_c9b9ae7a61444ab7" formatted="false">"前のページ"</string>
<string name="text_8a8542f6964852dc" formatted="false">"次のページ"</string>
<string name="text_0c4f690eb398c12e" formatted="false">"メインキー"</string>
<string name="text_62fdb5304cf5cd9b" formatted="false">"タスクマネージャー"</string>
<string name="text_a3030bf8f16dc63c" formatted="false">"保存"</string>
<string name="text_ba0e2ff8201bc6a6" formatted="false">"修飾キー"</string>
<string name="text_3a5040b68abf75f9" formatted="false">"全て選択"</string>
<string name="text_c2d918002afcc3d4" formatted="false">"アプリを切り替え"</string>
<string name="text_a7fe8e2685849c31" formatted="false">"ウインドウを切り替え"</string>
<string name="text_410a8e8a6bf253ac" formatted="false">"切り取り"</string>
<string name="text_6309cd49db59d165" formatted="false">"送信するキーの組み合わせ"</string>
<string name="text_d44e9b3d3b31d37b" formatted="false">"名前"</string>
<string name="text_6789dd11afc7c4c5" formatted="false">"上へ移動"</string>
<string name="text_352c003784b1c89e" formatted="false">"キーボードの上に固定"</string>
<string name="text_63d90d977348ab1f" formatted="false">"コピー"</string>
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"セキュリティオプション"</string>
<string name="text_dd1b1da65a6c89de" formatted="false">"この組み合わせは登録済みです"</string>
<string name="text_4563c57b5143deaa" formatted="false">"よく使う項目"</string>
<string name="text_6308bf1373bd0ee8" formatted="false">"アプリ一覧"</string>
<string name="text_8dc0823d700cf875" formatted="false">"強制終了ダイアログ"</string>
<string name="text_ee2638183d3ea860" formatted="false">"ショートカット"</string>
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"マイ項目"</string>
<string name="text_7c7577d4d4fc6009" formatted="false">"キー"</string>
<string name="text_926a50b98ece2667" formatted="false">"取り消す"</string>
<string name="text_50ef2f4cf6a46924" formatted="false">"新規"</string>
<string name="text_c6904bb36f7f62c5" formatted="false">"ショートカットを作成"</string>
<string name="text_5cfe4846aa8a7228" formatted="false">"デスクトップを表示"</string>
<string name="text_feb12adf8e0315e2" formatted="false">"その他のショートカット"</string>
<string name="text_5d6bce93c0649a63" formatted="false">"カスタムショートカットは16個まで保存できます"</string>
<string name="text_f33cb21422f62ee8" formatted="false">"固定できるショートカットは8個までです"</string>
<string name="text_4d8c1c5b42830791" formatted="false">"不明"</string>
<string name="text_6b1fa1d7159de777" formatted="false">"ローカル編集。入力はリモートに送信されません。"</string>
<string name="text_cb9eaa63c25d383e" formatted="false">"お気に入りバーに追加"</string>
<string name="text_6135d4159e892541" formatted="false">"取り除く"</string>
<string name="text_e693ff5832c7f46d" formatted="false">"ウインドウメニュー"</string>
<string name="text_096a247cc5455ba7" formatted="false">"リモートのOS情報を待っています"</string>
<string name="text_39c7740197c41d44" formatted="false">"管理とカスタマイズ"</string>
<string name="text_2dc8c27b75796b36" formatted="false">"ショートカットを管理"</string>
<string name="text_33517926747180e6" formatted="false">"貼り付け"</string>
<string name="text_5b50d7c4b5950dc5" formatted="false">"システム"</string>
<string name="text_45123fa07e22fb3e" formatted="false">"端末でコピー"</string>
<string name="text_3936f3d30a763a35" formatted="false">"端末に貼り付け"</string>
<string name="text_051836569928a9f9" formatted="false">"編集"</string>
<string name="text_0ecda10acc934f22" formatted="false">"ショートカットを編集"</string>
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Spotlight検索"</string>
<string name="text_add54b3ce4b1c661" formatted="false">"システムプリセットのセキュリティオプションを使用してください"</string>
<string name="text_ae066f34095261c9" formatted="false">"先に変換を確定またはキャンセルしてください"</string>
<string name="text_a895bdb6f1f11e99" formatted="false">"ショートカット名を1〜64文字で入力してください"</string>
<string name="text_6b3213d2afed99e8" formatted="false">"有効なキーを選択してください"</string>
<string name="text_819569ec77acb01e" formatted="false">"実行ダイアログ"</string>
<string name="text_22c4107f03c3533e" formatted="false">"カスタムショートカットはまだありません"</string>
<string name="text_b07f98d4671af3f9" formatted="false">"対象リモートOS"</string>
<string name="text_4bdabfba3368869d" formatted="false">"デスクトップ環境を選択"</string>
<string name="text_4fa28857c6f253cc" formatted="false">"デスクトップ環境を選択してプリセットを読み込みます"</string>
<string name="text_fc5f6f7462e8557a" formatted="false">"キーボード上のお気に入り"</string>
</resources>
@@ -315,4 +315,59 @@
<string name="text_cbacf1eafbca53e0" formatted="false">"페어링 정보를 복사했습니다."</string>
<string name="text_cde42ccdeacbbbd9" formatted="false">"페어링 정보를 복사할 수 없습니다. 다시 시도하세요."</string>
<string name="text_7383b2ae09252c59" formatted="false">"보안 저장소를 사용할 수 없습니다. 장치를 잠금 해제한 후 다시 시도해 주세요."</string>
<string name="text_c9b9ae7a61444ab7" formatted="false">"이전 페이지"</string>
<string name="text_8a8542f6964852dc" formatted="false">"다음 페이지"</string>
<string name="text_0c4f690eb398c12e" formatted="false">"기본 키"</string>
<string name="text_62fdb5304cf5cd9b" formatted="false">"작업 관리자"</string>
<string name="text_a3030bf8f16dc63c" formatted="false">"저장"</string>
<string name="text_ba0e2ff8201bc6a6" formatted="false">"보조 키"</string>
<string name="text_3a5040b68abf75f9" formatted="false">"모두 선택"</string>
<string name="text_c2d918002afcc3d4" formatted="false">"앱 전환"</string>
<string name="text_a7fe8e2685849c31" formatted="false">"창 전환"</string>
<string name="text_410a8e8a6bf253ac" formatted="false">"잘라내기"</string>
<string name="text_6309cd49db59d165" formatted="false">"전송할 키 조합"</string>
<string name="text_d44e9b3d3b31d37b" formatted="false">"이름"</string>
<string name="text_6789dd11afc7c4c5" formatted="false">"위로 이동"</string>
<string name="text_352c003784b1c89e" formatted="false">"키보드 위에 고정"</string>
<string name="text_63d90d977348ab1f" formatted="false">"복사"</string>
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"보안 옵션"</string>
<string name="text_dd1b1da65a6c89de" formatted="false">"이미 등록된 키 조합입니다"</string>
<string name="text_4563c57b5143deaa" formatted="false">"자주 사용"</string>
<string name="text_6308bf1373bd0ee8" formatted="false">"앱 목록"</string>
<string name="text_8dc0823d700cf875" formatted="false">"강제 종료 대화상자"</string>
<string name="text_ee2638183d3ea860" formatted="false">"단축키"</string>
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"내 단축키"</string>
<string name="text_7c7577d4d4fc6009" formatted="false">"키"</string>
<string name="text_926a50b98ece2667" formatted="false">"실행 취소"</string>
<string name="text_50ef2f4cf6a46924" formatted="false">"새로 만들기"</string>
<string name="text_c6904bb36f7f62c5" formatted="false">"새 단축키"</string>
<string name="text_5cfe4846aa8a7228" formatted="false">"바탕 화면 표시"</string>
<string name="text_feb12adf8e0315e2" formatted="false">"더 많은 단축키"</string>
<string name="text_5d6bce93c0649a63" formatted="false">"사용자 지정 단축키는 최대 16개까지 저장할 수 있습니다"</string>
<string name="text_f33cb21422f62ee8" formatted="false">"단축키는 최대 8개까지 고정할 수 있습니다"</string>
<string name="text_4d8c1c5b42830791" formatted="false">"알 수 없음"</string>
<string name="text_6b1fa1d7159de777" formatted="false">"로컬 편집 중입니다. 입력은 원격 기기로 전송되지 않습니다."</string>
<string name="text_cb9eaa63c25d383e" formatted="false">"즐겨찾기 모음에 추가"</string>
<string name="text_6135d4159e892541" formatted="false">"제거"</string>
<string name="text_e693ff5832c7f46d" formatted="false">"창 메뉴"</string>
<string name="text_096a247cc5455ba7" formatted="false">"원격 플랫폼 정보를 기다리는 중"</string>
<string name="text_39c7740197c41d44" formatted="false">"관리 및 사용자 지정"</string>
<string name="text_2dc8c27b75796b36" formatted="false">"단축키 관리"</string>
<string name="text_33517926747180e6" formatted="false">"붙여넣기"</string>
<string name="text_5b50d7c4b5950dc5" formatted="false">"시스템"</string>
<string name="text_45123fa07e22fb3e" formatted="false">"터미널 복사"</string>
<string name="text_3936f3d30a763a35" formatted="false">"터미널 붙여넣기"</string>
<string name="text_051836569928a9f9" formatted="false">"편집"</string>
<string name="text_0ecda10acc934f22" formatted="false">"단축키 편집"</string>
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Spotlight 검색"</string>
<string name="text_add54b3ce4b1c661" formatted="false">"시스템 프리셋의 보안 옵션을 사용하세요"</string>
<string name="text_ae066f34095261c9" formatted="false">"먼저 조합 중인 문자를 확정하거나 취소하세요"</string>
<string name="text_a895bdb6f1f11e99" formatted="false">"단축키 이름을 1~64자로 입력하세요"</string>
<string name="text_6b3213d2afed99e8" formatted="false">"유효한 키를 선택하세요"</string>
<string name="text_819569ec77acb01e" formatted="false">"실행 대화상자"</string>
<string name="text_22c4107f03c3533e" formatted="false">"아직 사용자 지정 단축키가 없습니다"</string>
<string name="text_b07f98d4671af3f9" formatted="false">"대상 원격 플랫폼"</string>
<string name="text_4bdabfba3368869d" formatted="false">"데스크톱 환경 선택"</string>
<string name="text_4fa28857c6f253cc" formatted="false">"데스크톱 환경을 선택하여 시스템 프리셋을 불러오세요"</string>
<string name="text_fc5f6f7462e8557a" formatted="false">"키보드 위 즐겨찾기 모음"</string>
</resources>
@@ -315,4 +315,59 @@
<string name="text_cbacf1eafbca53e0" formatted="false">"Informações de pareamento copiadas."</string>
<string name="text_cde42ccdeacbbbd9" formatted="false">"Não foi possível copiar as informações de pareamento. Tente novamente."</string>
<string name="text_7383b2ae09252c59" formatted="false">"O armazenamento seguro não está disponível. Desbloqueie o dispositivo e tente novamente"</string>
<string name="text_c9b9ae7a61444ab7" formatted="false">"Página anterior"</string>
<string name="text_8a8542f6964852dc" formatted="false">"Próxima página"</string>
<string name="text_0c4f690eb398c12e" formatted="false">"Tecla principal"</string>
<string name="text_62fdb5304cf5cd9b" formatted="false">"Gerenciador de Tarefas"</string>
<string name="text_a3030bf8f16dc63c" formatted="false">"Salvar"</string>
<string name="text_ba0e2ff8201bc6a6" formatted="false">"Modificadores"</string>
<string name="text_3a5040b68abf75f9" formatted="false">"Selecionar tudo"</string>
<string name="text_c2d918002afcc3d4" formatted="false">"Alternar aplicativos"</string>
<string name="text_a7fe8e2685849c31" formatted="false">"Alternar janelas"</string>
<string name="text_410a8e8a6bf253ac" formatted="false">"Recortar"</string>
<string name="text_6309cd49db59d165" formatted="false">"Combinação de teclas"</string>
<string name="text_d44e9b3d3b31d37b" formatted="false">"Nome"</string>
<string name="text_6789dd11afc7c4c5" formatted="false">"Mover para cima"</string>
<string name="text_352c003784b1c89e" formatted="false">"Fixar acima do teclado"</string>
<string name="text_63d90d977348ab1f" formatted="false">"Copiar"</string>
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"Opções de segurança"</string>
<string name="text_dd1b1da65a6c89de" formatted="false">"Esta combinação já existe"</string>
<string name="text_4563c57b5143deaa" formatted="false">"Favoritos"</string>
<string name="text_6308bf1373bd0ee8" formatted="false">"Lista de aplicativos"</string>
<string name="text_8dc0823d700cf875" formatted="false">"Janela Forçar Encerrar"</string>
<string name="text_ee2638183d3ea860" formatted="false">"Atalhos"</string>
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"Meus atalhos"</string>
<string name="text_7c7577d4d4fc6009" formatted="false">"Teclas"</string>
<string name="text_926a50b98ece2667" formatted="false">"Desfazer"</string>
<string name="text_50ef2f4cf6a46924" formatted="false">"Novo"</string>
<string name="text_c6904bb36f7f62c5" formatted="false">"Novo atalho"</string>
<string name="text_5cfe4846aa8a7228" formatted="false">"Mostrar área de trabalho"</string>
<string name="text_feb12adf8e0315e2" formatted="false">"Mais atalhos"</string>
<string name="text_5d6bce93c0649a63" formatted="false">"Salve até 16 atalhos personalizados"</string>
<string name="text_f33cb21422f62ee8" formatted="false">"Fixe até 8 atalhos"</string>
<string name="text_4d8c1c5b42830791" formatted="false">"Desconhecido"</string>
<string name="text_6b1fa1d7159de777" formatted="false">"Edição local. A entrada fica neste dispositivo."</string>
<string name="text_cb9eaa63c25d383e" formatted="false">"Adicionar à barra de favoritos"</string>
<string name="text_6135d4159e892541" formatted="false">"Remover"</string>
<string name="text_e693ff5832c7f46d" formatted="false">"Menu da janela"</string>
<string name="text_096a247cc5455ba7" formatted="false">"Aguardando informações do sistema remoto"</string>
<string name="text_39c7740197c41d44" formatted="false">"Gerenciar e personalizar"</string>
<string name="text_2dc8c27b75796b36" formatted="false">"Gerenciar atalhos"</string>
<string name="text_33517926747180e6" formatted="false">"Colar"</string>
<string name="text_5b50d7c4b5950dc5" formatted="false">"Sistema"</string>
<string name="text_45123fa07e22fb3e" formatted="false">"Copiar no terminal"</string>
<string name="text_3936f3d30a763a35" formatted="false">"Colar no terminal"</string>
<string name="text_051836569928a9f9" formatted="false">"Editar"</string>
<string name="text_0ecda10acc934f22" formatted="false">"Editar atalho"</string>
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Busca do Spotlight"</string>
<string name="text_add54b3ce4b1c661" formatted="false">"Use Opções de segurança nas predefinições do sistema"</string>
<string name="text_ae066f34095261c9" formatted="false">"Confirme ou cancele primeiro a composição atual"</string>
<string name="text_a895bdb6f1f11e99" formatted="false">"Insira um nome de 1 a 64 caracteres"</string>
<string name="text_6b3213d2afed99e8" formatted="false">"Selecione uma tecla válida"</string>
<string name="text_819569ec77acb01e" formatted="false">"Janela Executar"</string>
<string name="text_22c4107f03c3533e" formatted="false">"Ainda não há atalhos personalizados"</string>
<string name="text_b07f98d4671af3f9" formatted="false">"Sistema remoto"</string>
<string name="text_4bdabfba3368869d" formatted="false">"Escolher ambiente de trabalho"</string>
<string name="text_4fa28857c6f253cc" formatted="false">"Escolha um ambiente para carregar as predefinições do sistema"</string>
<string name="text_fc5f6f7462e8557a" formatted="false">"Favoritos acima do teclado"</string>
</resources>
@@ -315,4 +315,59 @@
<string name="text_cbacf1eafbca53e0" formatted="false">"Данные сопряжения скопированы."</string>
<string name="text_cde42ccdeacbbbd9" formatted="false">"Не удалось скопировать данные сопряжения. Повторите попытку."</string>
<string name="text_7383b2ae09252c59" formatted="false">"Безопасное хранилище недоступно. Разблокируйте устройство и повторите попытку."</string>
<string name="text_c9b9ae7a61444ab7" formatted="false">"Предыдущая страница"</string>
<string name="text_8a8542f6964852dc" formatted="false">"Следующая страница"</string>
<string name="text_0c4f690eb398c12e" formatted="false">"Основная клавиша"</string>
<string name="text_62fdb5304cf5cd9b" formatted="false">"Диспетчер задач"</string>
<string name="text_a3030bf8f16dc63c" formatted="false">"Сохранить"</string>
<string name="text_ba0e2ff8201bc6a6" formatted="false">"Клавиши-модификаторы"</string>
<string name="text_3a5040b68abf75f9" formatted="false">"Выбрать все"</string>
<string name="text_c2d918002afcc3d4" formatted="false">"Переключить приложение"</string>
<string name="text_a7fe8e2685849c31" formatted="false">"Переключить окно"</string>
<string name="text_410a8e8a6bf253ac" formatted="false">"Вырезать"</string>
<string name="text_6309cd49db59d165" formatted="false">"Сочетание клавиш"</string>
<string name="text_d44e9b3d3b31d37b" formatted="false">"Название"</string>
<string name="text_6789dd11afc7c4c5" formatted="false">"Переместить вверх"</string>
<string name="text_352c003784b1c89e" formatted="false">"Закрепить над клавиатурой"</string>
<string name="text_63d90d977348ab1f" formatted="false">"Копировать"</string>
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"Параметры безопасности"</string>
<string name="text_dd1b1da65a6c89de" formatted="false">"Такое сочетание уже существует"</string>
<string name="text_4563c57b5143deaa" formatted="false">"Избранное"</string>
<string name="text_6308bf1373bd0ee8" formatted="false">"Список приложений"</string>
<string name="text_8dc0823d700cf875" formatted="false">"Окно принудительного завершения"</string>
<string name="text_ee2638183d3ea860" formatted="false">"Сочетания клавиш"</string>
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"Мои"</string>
<string name="text_7c7577d4d4fc6009" formatted="false">"Клавиши"</string>
<string name="text_926a50b98ece2667" formatted="false">"Отменить"</string>
<string name="text_50ef2f4cf6a46924" formatted="false">"Создать"</string>
<string name="text_c6904bb36f7f62c5" formatted="false">"Новое сочетание"</string>
<string name="text_5cfe4846aa8a7228" formatted="false">"Показать рабочий стол"</string>
<string name="text_feb12adf8e0315e2" formatted="false">"Другие сочетания"</string>
<string name="text_5d6bce93c0649a63" formatted="false">"Можно сохранить до 16 своих сочетаний"</string>
<string name="text_f33cb21422f62ee8" formatted="false">"Можно закрепить до 8 сочетаний"</string>
<string name="text_4d8c1c5b42830791" formatted="false">"Неизвестно"</string>
<string name="text_6b1fa1d7159de777" formatted="false">"Локальное редактирование. Ввод не отправляется на удалённое устройство."</string>
<string name="text_cb9eaa63c25d383e" formatted="false">"Добавить на панель избранного"</string>
<string name="text_6135d4159e892541" formatted="false">"Убрать"</string>
<string name="text_e693ff5832c7f46d" formatted="false">"Меню окна"</string>
<string name="text_096a247cc5455ba7" formatted="false">"Ожидание сведений об удалённой системе"</string>
<string name="text_39c7740197c41d44" formatted="false">"Управление и настройка"</string>
<string name="text_2dc8c27b75796b36" formatted="false">"Управление сочетаниями"</string>
<string name="text_33517926747180e6" formatted="false">"Вставить"</string>
<string name="text_5b50d7c4b5950dc5" formatted="false">"Система"</string>
<string name="text_45123fa07e22fb3e" formatted="false">"Копировать в терминале"</string>
<string name="text_3936f3d30a763a35" formatted="false">"Вставить в терминале"</string>
<string name="text_051836569928a9f9" formatted="false">"Изменить"</string>
<string name="text_0ecda10acc934f22" formatted="false">"Изменить сочетание"</string>
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Поиск Spotlight"</string>
<string name="text_add54b3ce4b1c661" formatted="false">"Используйте параметры безопасности из системных наборов"</string>
<string name="text_ae066f34095261c9" formatted="false">"Сначала подтвердите или отмените текущий ввод"</string>
<string name="text_a895bdb6f1f11e99" formatted="false">"Введите название от 1 до 64 символов"</string>
<string name="text_6b3213d2afed99e8" formatted="false">"Выберите допустимую клавишу"</string>
<string name="text_819569ec77acb01e" formatted="false">"Окно запуска"</string>
<string name="text_22c4107f03c3533e" formatted="false">"Своих сочетаний пока нет"</string>
<string name="text_b07f98d4671af3f9" formatted="false">"Удалённая система"</string>
<string name="text_4bdabfba3368869d" formatted="false">"Выберите окружение рабочего стола"</string>
<string name="text_4fa28857c6f253cc" formatted="false">"Выберите окружение, чтобы загрузить системные наборы"</string>
<string name="text_fc5f6f7462e8557a" formatted="false">"Избранное над клавиатурой"</string>
</resources>
@@ -315,4 +315,59 @@
<string name="text_cbacf1eafbca53e0" formatted="false">"Pairing information copied."</string>
<string name="text_cde42ccdeacbbbd9" formatted="false">"Unable to copy pairing information. Please try again."</string>
<string name="text_7383b2ae09252c59" formatted="false">"Secure storage is unavailable, please unlock the device and try again"</string>
<string name="text_c9b9ae7a61444ab7" formatted="false">"Previous page"</string>
<string name="text_8a8542f6964852dc" formatted="false">"Next page"</string>
<string name="text_0c4f690eb398c12e" formatted="false">"Main key"</string>
<string name="text_62fdb5304cf5cd9b" formatted="false">"Task Manager"</string>
<string name="text_a3030bf8f16dc63c" formatted="false">"Save"</string>
<string name="text_ba0e2ff8201bc6a6" formatted="false">"Modifiers"</string>
<string name="text_3a5040b68abf75f9" formatted="false">"Select all"</string>
<string name="text_c2d918002afcc3d4" formatted="false">"Switch apps"</string>
<string name="text_a7fe8e2685849c31" formatted="false">"Switch windows"</string>
<string name="text_410a8e8a6bf253ac" formatted="false">"Cut"</string>
<string name="text_6309cd49db59d165" formatted="false">"Key combination"</string>
<string name="text_d44e9b3d3b31d37b" formatted="false">"Name"</string>
<string name="text_6789dd11afc7c4c5" formatted="false">"Move up"</string>
<string name="text_352c003784b1c89e" formatted="false">"Pin above keyboard"</string>
<string name="text_63d90d977348ab1f" formatted="false">"Copy"</string>
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"Security options"</string>
<string name="text_dd1b1da65a6c89de" formatted="false">"This combination already exists"</string>
<string name="text_4563c57b5143deaa" formatted="false">"Favorites"</string>
<string name="text_6308bf1373bd0ee8" formatted="false">"App list"</string>
<string name="text_8dc0823d700cf875" formatted="false">"Force Quit dialog"</string>
<string name="text_ee2638183d3ea860" formatted="false">"Shortcuts"</string>
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"Mine"</string>
<string name="text_7c7577d4d4fc6009" formatted="false">"Keys"</string>
<string name="text_926a50b98ece2667" formatted="false">"Undo"</string>
<string name="text_50ef2f4cf6a46924" formatted="false">"New"</string>
<string name="text_c6904bb36f7f62c5" formatted="false">"New shortcut"</string>
<string name="text_5cfe4846aa8a7228" formatted="false">"Show desktop"</string>
<string name="text_feb12adf8e0315e2" formatted="false">"More shortcuts"</string>
<string name="text_5d6bce93c0649a63" formatted="false">"Save up to 16 custom shortcuts"</string>
<string name="text_f33cb21422f62ee8" formatted="false">"Pin up to 8 shortcuts"</string>
<string name="text_4d8c1c5b42830791" formatted="false">"Unknown"</string>
<string name="text_6b1fa1d7159de777" formatted="false">"Local editing. Input stays on this device."</string>
<string name="text_cb9eaa63c25d383e" formatted="false">"Add to favorites bar"</string>
<string name="text_6135d4159e892541" formatted="false">"Remove"</string>
<string name="text_e693ff5832c7f46d" formatted="false">"Window menu"</string>
<string name="text_096a247cc5455ba7" formatted="false">"Waiting for remote platform information"</string>
<string name="text_39c7740197c41d44" formatted="false">"Manage &amp; customize"</string>
<string name="text_2dc8c27b75796b36" formatted="false">"Manage shortcuts"</string>
<string name="text_33517926747180e6" formatted="false">"Paste"</string>
<string name="text_5b50d7c4b5950dc5" formatted="false">"System"</string>
<string name="text_45123fa07e22fb3e" formatted="false">"Terminal copy"</string>
<string name="text_3936f3d30a763a35" formatted="false">"Terminal paste"</string>
<string name="text_051836569928a9f9" formatted="false">"Edit"</string>
<string name="text_0ecda10acc934f22" formatted="false">"Edit shortcut"</string>
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Spotlight search"</string>
<string name="text_add54b3ce4b1c661" formatted="false">"Use Security options in the system presets"</string>
<string name="text_ae066f34095261c9" formatted="false">"Confirm or cancel the current composition first"</string>
<string name="text_a895bdb6f1f11e99" formatted="false">"Enter a shortcut name (1–64 characters)"</string>
<string name="text_6b3213d2afed99e8" formatted="false">"Select a valid key"</string>
<string name="text_819569ec77acb01e" formatted="false">"Run dialog"</string>
<string name="text_22c4107f03c3533e" formatted="false">"No custom shortcuts yet"</string>
<string name="text_b07f98d4671af3f9" formatted="false">"Remote platform"</string>
<string name="text_4bdabfba3368869d" formatted="false">"Choose desktop environment"</string>
<string name="text_4fa28857c6f253cc" formatted="false">"Choose a desktop environment to load system presets"</string>
<string name="text_fc5f6f7462e8557a" formatted="false">"Favorites above keyboard"</string>
</resources>
+6
View File
@@ -11,6 +11,12 @@
手机控制端的“设置 → 屏幕方向 → 连接建立后锁定横屏”默认关闭并保存在本机。开启后,手机连接时进入并保持横屏;关闭时跟随设备方向。平板和多窗口不受此选项影响。退出会话恢复首页方向,返回后台会话沿用该会话的方向设置。已移除检测手机姿态后解除初始横屏的逻辑。
## 移动端快捷键
手机和平板在现有悬浮键盘上提供常用快捷键栏和“更多”入口;更多面板替换按键区域,包含“常用 / 按键 / 系统 / 我的”。预设根据远端 Windows、macOS、Linux 适配,Linux 系统预设需选择 GNOME 或 KDE Plasma。当前输入入口仍是 ArkUI 悬浮键盘,未改为系统输入法。
管理页独立处理本地输入,支持 16 个自定义组合、每个远端平台 8 个固定项,以及排序、编辑和删除。配置保存在本机,组合键使用同一条原生队列任务发送,按反序释放修饰键,并校验连接代次。
## 移动端可信设备鉴权
手机和平板的“设置 → 本机配对信息”支持查看指纹和复制配对信息。在被控电脑的“管理可信设备 → 添加可信设备”中粘贴并核对指纹后,可以连接开启“仅允许可信设备控制”的电脑;连接密码仍需验证。
@@ -248,4 +248,58 @@ export const translations: Record<string, string[]> = {
"复制配对信息": ["复制配对信息", "Copy pairing info", "Копировать данные", "複製配對資訊", "ペアリング情報をコピー", "페어링 정보 복사", "Kopplungsdaten kopieren", "Copier les informations", "Copiar información", "Copiar informações"],
"配对信息已复制。": ["配对信息已复制。", "Pairing information copied.", "Данные сопряжения скопированы.", "配對資訊已複製。", "ペアリング情報をコピーしました。", "페어링 정보를 복사했습니다.", "Kopplungsdaten kopiert.", "Informations d’association copiées.", "Información de emparejamiento copiada.", "Informações de pareamento copiadas."],
"无法复制配对信息,请重试。": ["无法复制配对信息,请重试。", "Unable to copy pairing information. Please try again.", "Не удалось скопировать данные сопряжения. Повторите попытку.", "無法複製配對資訊,請重試。", "ペアリング情報をコピーできません。もう一度お試しください。", "페어링 정보를 복사할 수 없습니다. 다시 시도하세요.", "Kopplungsdaten konnten nicht kopiert werden. Bitte erneut versuchen.", "Impossible de copier les informations d’association. Réessayez.", "No se pudo copiar la información de emparejamiento. Inténtelo de nuevo.", "Não foi possível copiar as informações de pareamento. Tente novamente."],
"上一页": ["上一页", "Previous page", "Предыдущая страница", "上一頁", "前のページ", "이전 페이지", "Vorherige Seite", "Page précédente", "Página anterior", "Página anterior"],
"下一页": ["下一页", "Next page", "Следующая страница", "下一頁", "次のページ", "다음 페이지", "Nächste Seite", "Page suivante", "Página siguiente", "Próxima página"],
"主按键": ["主按键", "Main key", "Основная клавиша", "主按鍵", "メインキー", "기본 키", "Haupttaste", "Touche principale", "Tecla principal", "Tecla principal"],
"任务管理器": ["任务管理器", "Task Manager", "Диспетчер задач", "工作管理員", "タスクマネージャー", "작업 관리자", "Task-Manager", "Gestionnaire des tâches", "Administrador de tareas", "Gerenciador de Tarefas"],
"保存": ["保存", "Save", "Сохранить", "儲存", "保存", "저장", "Speichern", "Enregistrer", "Guardar", "Salvar"],
"修饰键": ["修饰键", "Modifiers", "Клавиши-модификаторы", "修飾鍵", "修飾キー", "보조 키", "Modifikatortasten", "Touches de modification", "Modificadores", "Modificadores"],
"切换应用": ["切换应用", "Switch apps", "Переключить приложение", "切換應用程式", "アプリを切り替え", "앱 전환", "Apps wechseln", "Changer d’application", "Cambiar de aplicación", "Alternar aplicativos"],
"切换窗口": ["切换窗口", "Switch windows", "Переключить окно", "切換視窗", "ウインドウを切り替え", "창 전환", "Fenster wechseln", "Changer de fenêtre", "Cambiar de ventana", "Alternar janelas"],
"剪切": ["剪切", "Cut", "Вырезать", "剪下", "切り取り", "잘라내기", "Ausschneiden", "Couper", "Cortar", "Recortar"],
"发送组合": ["发送组合", "Key combination", "Сочетание клавиш", "傳送組合", "送信するキーの組み合わせ", "전송할 키 조합", "Tastenkombination", "Combinaison à envoyer", "Combinación de teclas", "Combinação de teclas"],
"名称": ["名称", "Name", "Название", "名稱", "名前", "이름", "Name", "Nom", "Nombre", "Nome"],
"向上移动": ["向上移动", "Move up", "Переместить вверх", "向上移動", "上へ移動", "위로 이동", "Nach oben", "Monter", "Subir", "Mover para cima"],
"固定到键盘上方": ["固定到键盘上方", "Pin above keyboard", "Закрепить над клавиатурой", "固定到鍵盤上方", "キーボードの上に固定", "키보드 위에 고정", "Über der Tastatur anheften", "Épingler au-dessus du clavier", "Fijar sobre el teclado", "Fixar acima do teclado"],
"安全选项": ["安全选项", "Security options", "Параметры безопасности", "安全性選項", "セキュリティオプション", "보안 옵션", "Sicherheitsoptionen", "Options de sécurité", "Opciones de seguridad", "Opções de segurança"],
"已有相同组合的快捷键": ["已有相同组合的快捷键", "This combination already exists", "Такое сочетание уже существует", "已有相同組合的快捷鍵", "この組み合わせは登録済みです", "이미 등록된 키 조합입니다", "Diese Kombination ist bereits vorhanden", "Cette combinaison existe déjà", "Esta combinación ya existe", "Esta combinação já existe"],
"常用": ["常用", "Favorites", "Избранное", "常用", "よく使う項目", "자주 사용", "Favoriten", "Favoris", "Favoritos", "Favoritos"],
"应用列表": ["应用列表", "App list", "Список приложений", "應用程式列表", "アプリ一覧", "앱 목록", "App-Liste", "Liste des applications", "Lista de aplicaciones", "Lista de aplicativos"],
"强制退出面板": ["强制退出面板", "Force Quit dialog", "Окно принудительного завершения", "強制結束面板", "強制終了ダイアログ", "강제 종료 대화상자", "Dialog zum sofortigen Beenden", "Fenêtre Forcer à quitter", "Diálogo Forzar salida", "Janela Forçar Encerrar"],
"快捷键": ["快捷键", "Shortcuts", "Сочетания клавиш", "快速鍵", "ショートカット", "단축키", "Tastenkürzel", "Raccourcis", "Atajos", "Atalhos"],
"我的": ["我的", "Mine", "Мои", "我的", "マイ項目", "내 단축키", "Eigene", "Mes raccourcis", "Mis atajos", "Meus atalhos"],
"按键": ["按键", "Keys", "Клавиши", "按鍵", "キー", "키", "Tasten", "Touches", "Teclas", "Teclas"],
"撤销": ["撤销", "Undo", "Отменить", "復原", "取り消す", "실행 취소", "Rückgängig", "Annuler l’action", "Deshacer", "Desfazer"],
"新建": ["新建", "New", "Создать", "新增", "新規", "새로 만들기", "Neu", "Nouveau", "Nuevo", "Novo"],
"新建快捷键": ["新建快捷键", "New shortcut", "Новое сочетание", "新增快速鍵", "ショートカットを作成", "새 단축키", "Neues Tastenkürzel", "Nouveau raccourci", "Nuevo atajo", "Novo atalho"],
"显示桌面": ["显示桌面", "Show desktop", "Показать рабочий стол", "顯示桌面", "デスクトップを表示", "바탕 화면 표시", "Desktop anzeigen", "Afficher le bureau", "Mostrar escritorio", "Mostrar área de trabalho"],
"更多快捷键": ["更多快捷键", "More shortcuts", "Другие сочетания", "更多快速鍵", "その他のショートカット", "더 많은 단축키", "Weitere Tastenkürzel", "Autres raccourcis", "Más atajos", "Mais atalhos"],
"最多保存 16 个自定义快捷键": ["最多保存 16 个自定义快捷键", "Save up to 16 custom shortcuts", "Можно сохранить до 16 своих сочетаний", "最多儲存 16 個自訂快速鍵", "カスタムショートカットは16個まで保存できます", "사용자 지정 단축키는 최대 16개까지 저장할 수 있습니다", "Bis zu 16 eigene Tastenkürzel speichern", "Enregistrez jusqu’à 16 raccourcis personnalisés", "Guarda hasta 16 atajos personalizados", "Salve até 16 atalhos personalizados"],
"最多固定 8 个快捷键": ["最多固定 8 个快捷键", "Pin up to 8 shortcuts", "Можно закрепить до 8 сочетаний", "最多固定 8 個快速鍵", "固定できるショートカットは8個までです", "단축키는 최대 8개까지 고정할 수 있습니다", "Bis zu 8 Tastenkürzel anheften", "Épinglez jusqu’à 8 raccourcis", "Fija hasta 8 atajos", "Fixe até 8 atalhos"],
"未知": ["未知", "Unknown", "Неизвестно", "未知", "不明", "알 수 없음", "Unbekannt", "Inconnu", "Desconocido", "Desconhecido"],
"本地编辑,输入不会发送到远端": ["本地编辑,输入不会发送到远端", "Local editing. Input stays on this device.", "Локальное редактирование. Ввод не отправляется на удалённое устройство.", "本機編輯,輸入不會傳送到遠端", "ローカル編集。入力はリモートに送信されません。", "로컬 편집 중입니다. 입력은 원격 기기로 전송되지 않습니다.", "Lokale Bearbeitung. Eingaben bleiben auf diesem Gerät.", "Édition locale. La saisie reste sur cet appareil.", "Edición local. Lo escrito permanece en este dispositivo.", "Edição local. A entrada fica neste dispositivo."],
"添加到常用栏": ["添加到常用栏", "Add to favorites bar", "Добавить на панель избранного", "加入常用列", "お気に入りバーに追加", "즐겨찾기 모음에 추가", "Zur Favoritenleiste hinzufügen", "Ajouter à la barre des favoris", "Añadir a la barra de favoritos", "Adicionar à barra de favoritos"],
"电脑键盘": ["电脑键盘", "Computer keyboard", "Компьютерная клавиатура", "電腦鍵盤", "コンピューターのキーボード", "컴퓨터 키보드", "Computertastatur", "Clavier d'ordinateur", "Teclado de computadora", "Teclado de computador"],
"移除": ["移除", "Remove", "Убрать", "移除", "取り除く", "제거", "Entfernen", "Retirer", "Quitar", "Remover"],
"窗口菜单": ["窗口菜单", "Window menu", "Меню окна", "視窗選單", "ウインドウメニュー", "창 메뉴", "Fenstermenü", "Menu de la fenêtre", "Menú de ventana", "Menu da janela"],
"等待远端平台信息": ["等待远端平台信息", "Waiting for remote platform information", "Ожидание сведений об удалённой системе", "等待遠端平台資訊", "リモートのOS情報を待っています", "원격 플랫폼 정보를 기다리는 중", "Warten auf Informationen zur Remote-Plattform", "En attente des informations du système distant", "Esperando información del sistema remoto", "Aguardando informações do sistema remoto"],
"管理与自定义": ["管理与自定义", "Manage & customize", "Управление и настройка", "管理與自訂", "管理とカスタマイズ", "관리 및 사용자 지정", "Verwalten und anpassen", "Gérer et personnaliser", "Gestionar y personalizar", "Gerenciar e personalizar"],
"管理快捷键": ["管理快捷键", "Manage shortcuts", "Управление сочетаниями", "管理快速鍵", "ショートカットを管理", "단축키 관리", "Tastenkürzel verwalten", "Gérer les raccourcis", "Gestionar atajos", "Gerenciar atalhos"],
"系统": ["系统", "System", "Система", "系統", "システム", "시스템", "System", "Système", "Sistema", "Sistema"],
"系统键盘": ["系统键盘", "System keyboard", "Системная клавиатура", "系統鍵盤", "システムキーボード", "시스템 키보드", "Systemtastatur", "Clavier système", "Teclado del sistema", "Teclado do sistema"],
"终端复制": ["终端复制", "Terminal copy", "Копировать в терминале", "終端機複製", "端末でコピー", "터미널 복사", "Im Terminal kopieren", "Copier dans le terminal", "Copiar en terminal", "Copiar no terminal"],
"终端粘贴": ["终端粘贴", "Terminal paste", "Вставить в терминале", "終端機貼上", "端末に貼り付け", "터미널 붙여넣기", "Im Terminal einfügen", "Coller dans le terminal", "Pegar en terminal", "Colar no terminal"],
"编辑": ["编辑", "Edit", "Изменить", "編輯", "編集", "편집", "Bearbeiten", "Modifier", "Editar", "Editar"],
"编辑快捷键": ["编辑快捷键", "Edit shortcut", "Изменить сочетание", "編輯快速鍵", "ショートカットを編集", "단축키 편집", "Tastenkürzel bearbeiten", "Modifier le raccourci", "Editar atajo", "Editar atalho"],
"聚焦搜索": ["聚焦搜索", "Spotlight search", "Поиск Spotlight", "Spotlight 搜尋", "Spotlight検索", "Spotlight 검색", "Spotlight-Suche", "Recherche Spotlight", "Búsqueda de Spotlight", "Busca do Spotlight"],
"请使用系统预设中的安全选项": ["请使用系统预设中的安全选项", "Use Security options in the system presets", "Используйте параметры безопасности из системных наборов", "請使用系統預設中的安全性選項", "システムプリセットのセキュリティオプションを使用してください", "시스템 프리셋의 보안 옵션을 사용하세요", "Sicherheitsoptionen in den Systemvorgaben verwenden", "Utilisez les options de sécurité des préréglages système", "Usa Opciones de seguridad en los ajustes del sistema", "Use Opções de segurança nas predefinições do sistema"],
"请先选词或取消组词": ["请先选词或取消组词", "Confirm or cancel the current composition first", "Сначала подтвердите или отмените текущий ввод", "請先選字或取消組字", "先に変換を確定またはキャンセルしてください", "먼저 조합 중인 문자를 확정하거나 취소하세요", "Zuerst die aktuelle Texteingabe bestätigen oder abbrechen", "Validez ou annulez d’abord la composition en cours", "Confirma o cancela primero la composición actual", "Confirme ou cancele primeiro a composição atual"],
"请输入有效的快捷键名称": ["请输入有效的快捷键名称", "Enter a shortcut name (1–64 characters)", "Введите название от 1 до 64 символов", "請輸入有效的快速鍵名稱", "ショートカット名を1〜64文字で入力してください", "단축키 이름을 1~64자로 입력하세요", "Namen mit 1–64 Zeichen eingeben", "Saisissez un nom de 1 à 64 caractères", "Introduce un nombre de 1 a 64 caracteres", "Insira um nome de 1 a 64 caracteres"],
"请选择有效的按键": ["请选择有效的按键", "Select a valid key", "Выберите допустимую клавишу", "請選擇有效的按鍵", "有効なキーを選択してください", "유효한 키를 선택하세요", "Eine gültige Taste auswählen", "Sélectionnez une touche valide", "Selecciona una tecla válida", "Selecione uma tecla válida"],
"运行窗口": ["运行窗口", "Run dialog", "Окно запуска", "執行視窗", "実行ダイアログ", "실행 대화상자", "Ausführen-Dialog", "Fenêtre Exécuter", "Diálogo Ejecutar", "Janela Executar"],
"还没有自定义快捷键": ["还没有自定义快捷键", "No custom shortcuts yet", "Своих сочетаний пока нет", "尚無自訂快速鍵", "カスタムショートカットはまだありません", "아직 사용자 지정 단축키가 없습니다", "Noch keine eigenen Tastenkürzel", "Aucun raccourci personnalisé", "Aún no hay atajos personalizados", "Ainda não há atalhos personalizados"],
"适用远端": ["适用远端", "Remote platform", "Удалённая система", "適用遠端", "対象リモートOS", "대상 원격 플랫폼", "Remote-Plattform", "Système distant", "Sistema remoto", "Sistema remoto"],
"选择桌面方案": ["选择桌面方案", "Choose desktop environment", "Выберите окружение рабочего стола", "選擇桌面環境", "デスクトップ環境を選択", "데스크톱 환경 선택", "Desktop-Umgebung auswählen", "Choisir l’environnement de bureau", "Elegir entorno de escritorio", "Escolher ambiente de trabalho"],
"选择桌面方案以加载系统预设": ["选择桌面方案以加载系统预设", "Choose a desktop environment to load system presets", "Выберите окружение, чтобы загрузить системные наборы", "選擇桌面環境以載入系統預設", "デスクトップ環境を選択してプリセットを読み込みます", "데스크톱 환경을 선택하여 시스템 프리셋을 불러오세요", "Desktop-Umgebung wählen, um Systemvorgaben zu laden", "Choisissez un environnement pour charger les préréglages système", "Elige un entorno para cargar los ajustes del sistema", "Escolha um ambiente para carregar as predefinições do sistema"],
"键盘上方常用栏": ["键盘上方常用栏", "Favorites above keyboard", "Избранное над клавиатурой", "鍵盤上方常用列", "キーボード上のお気に入り", "키보드 위 즐겨찾기 모음", "Favoriten über der Tastatur", "Favoris au-dessus du clavier", "Favoritos sobre el teclado", "Favoritos acima do teclado"],
};
@@ -12,6 +12,7 @@ import { AnnouncementInbox, AnnouncementItem, AnnouncementRequest, AnnouncementR
import { VideoSettings, VideoValues, NetworkStatistics, NetworkReport, RecentPresence, PresenceMessage, asciiKey } from './ControllerModels';
interface Command {
modifiers?: number[];
trustKey?: string;
displayResolutions?: ResolutionPreference[];
displayId?: number; displayKey?: string; width?: number; height?: number;
@@ -308,8 +309,8 @@ export class MobileController {
physicalKey(code: number, down: boolean): void { if (this.state.modal === 'privacyUnlock') return; this.send({ op: 'key', code: code, down: down }); }
key(code: number, down: boolean): void { if (this.state.modal === 'privacyUnlock') return; this.send({ op: 'virtualKey', code: code, down: down }); }
chord(code: number, modifiers: number[] = []): void {
modifiers.forEach((key: number) => this.key(key, true)); this.key(code, true); this.key(code, false);
modifiers.slice().reverse().forEach((key: number) => this.key(key, false));
if (!this.state.ready || !this.state.consent || this.state.modal === 'privacyUnlock') return;
this.send({ op: 'chord', code, modifiers: [17, 16, 18, 91].filter((key: number) => modifiers.includes(key)), connection: this.connection });
}
shortcut(code: number): void { this.chord(code, [this.state.platform === 'macos' ? 91 : 17]); }
text(text: string): void {
@@ -1,3 +1,5 @@
import { RemoteShortcut, RemoteShortcuts } from './RemoteShortcuts';
import { RemoteShortcutPanel, RemoteShortcutManager } from './RemoteShortcutPanel';
import { tr } from './Localization';
import { ResolutionMode, resolutionLabel } from './RemoteDisplayResolutions';
import { MobileController, MobileState } from './MobileController';
@@ -26,6 +28,12 @@ export struct MobileRemoteView {
private orbPositioned: boolean = false;
@State private keyboard: boolean = false;
@State private computerKeyboard: boolean = false;
@State private shortcutPanel: boolean = false;
@State private shortcutManager: boolean = false;
@State private pinnedShortcuts: RemoteShortcut[] = [];
@State private shortcutRevision: number = 0;
private shortcutStore?: RemoteShortcuts;
private shortcutPlatform: string = '';
@State private shift: boolean = false;
@State private modifiers: number[] = [];
@State private keyboardX: number = 8;
@@ -56,9 +64,23 @@ export struct MobileRemoteView {
private wheelLast: number = 0;
private keyboardOriginX: number = 0;
private keyboardOriginY: number = 0;
aboutToAppear(): void { if (this.controller) this.controller.releaseInput = () => this.release(); this.stateChanged(); }
aboutToAppear(): void {
if (this.controller) { this.controller.releaseInput = () => this.release(); this.shortcutStore = new RemoteShortcuts(this.controller.store); }
this.refreshShortcuts(); this.stateChanged();
}
private refreshShortcuts(): void { this.pinnedShortcuts = this.shortcutStore?.pinned(this.state.platform) ?? []; this.shortcutRevision++; }
private sendShortcut(item: RemoteShortcut): void {
if (!this.state.ready || item.platform !== RemoteShortcuts.platform(this.state.platform)) return;
this.modifiers = []; this.shift = false;
if (item.service) this.controller?.sas(); else this.controller?.chord(item.key, item.modifiers);
}
private manageShortcuts(): void { this.controller?.release(); this.menu = ''; this.shortcutManager = true; }
aboutToDisappear(): void { this.release(); if (this.controller) this.controller.releaseInput = () => {}; this.controller?.surface('0'); }
private stateChanged(): void {
if (!this.state.ready || this.state.modal === 'privacyUnlock') this.shortcutManager = false;
if (this.shortcutPlatform !== this.state.platform) {
this.shortcutPlatform = this.state.platform; this.shortcutManager = false; this.refreshShortcuts();
}
if (this.lastReset !== this.state.viewportReset || this.lastRelative !== this.state.relative) {
this.release();
if (this.lastReset !== this.state.viewportReset) { this.viewport.reset(); this.applyViewport(); }
@@ -165,6 +187,8 @@ export struct MobileRemoteView {
case 'space': return tr("空格");
case 'computer': return tr("电脑");
case 'clipboard': return tr("剪贴板");
case 'Win': return RemoteShortcuts.modifierName(91, this.state.platform);
case 'Alt': return RemoteShortcuts.modifierName(18, this.state.platform);
default: return this.shift && key.length === 1 ? key.toUpperCase() : key;
}
}
@@ -182,12 +206,13 @@ export struct MobileRemoteView {
if (value === 'clipboard') { this.state.modal = 'clipboard'; return; }
const modifier = value === 'Ctrl' ? 17 : value === 'Alt' ? 18 : value === 'Win' ? 91 : 0;
if (modifier) { this.modifiers = this.modifiers.includes(modifier) ? this.modifiers.filter((m: number) => m !== modifier) : [...this.modifiers, modifier]; return; }
if (value === 'copy' || value === 'paste' || value === 'select_all') { this.controller?.shortcut(value === 'copy' ? 67 : value === 'paste' ? 86 : 65); return; }
if (value === 'copy' || value === 'paste' || value === 'select_all') { this.controller?.shortcut(value === 'copy' ? 67 : value === 'paste' ? 86 : 65); this.modifiers = []; this.shift = false; return; }
const names = ['space','⌫','Enter','Esc','Tab','↑','↓','←','→']; const codes = [32,8,13,27,9,38,40,37,39];
let code = names.includes(value) ? codes[names.indexOf(value)] : /^F[1-6]$/.test(value) ? 111 + Number(value.substring(1)) : 0;
if (!code && this.modifiers.length) { const key = asciiKey(this.shift ? value.toUpperCase() : value); code = key & 255; if (key & 256) this.modifiers = [...this.modifiers, 16]; }
if (code) { this.controller?.chord(code, this.modifiers); this.modifiers = []; }
else this.controller?.text(this.shift ? value.toUpperCase() : value);
this.shift = false;
}
private keyboardDrag(event: GestureEvent): void {
this.keyboardX = clamp(this.keyboardOriginX + event.offsetX, 8, this.viewWidth - this.keyboardWidth() - 8);
@@ -314,12 +339,38 @@ export struct MobileRemoteView {
@Builder
Keyboard() {
Column({ space: 3 }) {
Row() {
Stack() { Row() {}.width(32).height(4).backgroundColor(MobileTheme.secondary).borderRadius(2) }.layoutWeight(1).height(28).accessibilityText(tr("拖动键盘"))
.gesture(PanGesture().onActionStart(() => { this.keyboardOriginX = this.keyboardX; this.keyboardOriginY = this.keyboardY; })
.onActionUpdate((e: GestureEvent) => this.keyboardDrag(e)))
MobileIconButton({ icon: 'close', label: tr("收起键盘"), iconExtent: 28, iconSize: 18, action: () => { this.keyboard = false; this.modifiers = []; } })
}.width('100%')
Stack() { Row() {}.width(32).height(4).backgroundColor(MobileTheme.secondary).borderRadius(2) }
.width('100%').height(14).accessibilityText(tr('拖动键盘'))
.gesture(PanGesture().onActionStart(() => { this.keyboardOriginX = this.keyboardX; this.keyboardOriginY = this.keyboardY; })
.onActionUpdate((e: GestureEvent) => this.keyboardDrag(e)))
Row({ space: 2 }) {
Button(this.shortcutPanel ? 'ABC' : this.computerKeyboard ? 'ABC' : '⌘', { type: ButtonType.Normal })
.width(44).height(44).fontSize(15).padding(0).fontColor(MobileTheme.ink).backgroundColor(Color.Transparent)
.accessibilityText(tr(this.computerKeyboard || this.shortcutPanel ? '键盘' : '电脑键盘'))
.onClick(() => { if (this.shortcutPanel) { this.shortcutPanel = false; this.computerKeyboard = false; } else this.computerKeyboard = !this.computerKeyboard; this.modifiers = []; })
if (this.shortcutPanel) {
Text(tr('快捷键')).layoutWeight(1).fontSize(14).textAlign(TextAlign.Center)
} else {
Scroll() {
Row({ space: 4 }) {
ForEach(this.pinnedShortcuts, (item: RemoteShortcut) => {
Button(item.title(), { type: ButtonType.Normal }).height(44).constraintSize({ minWidth: 44 }).fontSize(13)
.padding({ left: 10, right: 10 }).fontColor(MobileTheme.ink).backgroundColor(Color.Transparent)
.accessibilityText(`${item.title()}, ${item.chord()}`).onClick(() => this.sendShortcut(item))
}, (item: RemoteShortcut) => item.id)
}
}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).layoutWeight(1)
}
Button('•••', { type: ButtonType.Normal }).width(44).height(44).padding(0).fontColor(MobileTheme.ink).backgroundColor(Color.Transparent)
.accessibilityText(tr('更多快捷键')).onClick(() => { this.shortcutPanel = !this.shortcutPanel; this.modifiers = []; })
MobileIconButton({ icon: 'close', label: tr('收起键盘'), iconExtent: 44, iconSize: 20, buttonFill: '#D1D6DE', action: () => { this.keyboard = false; this.modifiers = []; } })
}.width('100%').height(44)
if (this.shortcutPanel) {
Column() {
RemoteShortcutPanel({ store: this.shortcutStore, platform: RemoteShortcuts.platform(this.state.platform), revision: this.shortcutRevision,
send: (item: RemoteShortcut) => this.sendShortcut(item), manage: () => this.manageShortcuts(), changed: () => this.refreshShortcuts() })
}.width('100%').height(Math.min(230, Math.max(100, this.viewHeight - 88)))
} else {
Scroll() {
Column({ space: 3 }) {
ForEach(this.keyboardRows(), (row: KeyRow) => {
@@ -334,7 +385,8 @@ export struct MobileRemoteView {
}.width('100%')
})
}.width('100%')
}.width('100%').constraintSize({ maxHeight: Math.max(31, this.viewHeight - 60) }).scrollBar(BarState.Off)
}.width('100%').constraintSize({ maxHeight: Math.max(31, this.viewHeight - 88) }).scrollBar(BarState.Off)
}
}.padding(5).width(this.keyboardWidth()).backgroundColor('#D1D6DE').borderRadius(12)
.shadow({ radius: 20, color: '#33000000', offsetY: 5 }).onAreaChange((_old: Area, next: Area) => this.keyboardHeight = Number(next.height))
.position({ x: clamp(this.keyboardX, 8, this.viewWidth - this.keyboardWidth() - 8), y: clamp(this.keyboardY, 8, Math.max(8, this.viewHeight - this.keyboardHeight - 8)) })
@@ -372,10 +424,10 @@ export struct MobileRemoteView {
}.padding(20).constraintSize({ minWidth: 200 }).backgroundColor('#B0202024').borderRadius(14)
.position({ x: (this.viewWidth - 200) / 2, y: (this.viewHeight - 104) / 2 }).hitTestBehavior(HitTestMode.None)
}
Column() {}.width('100%').height('100%').focusable(true).defaultFocus(true)
Column() {}.width('100%').height('100%').focusable(!this.shortcutManager).enabled(!this.shortcutManager).defaultFocus(true)
.onTouch((e: TouchEvent) => this.touch(e)).onMouse((e: MouseEvent) => this.physicalMouse(e))
.onAxisEvent((event: AxisEvent) => { const delta = event.getVerticalAxisValue(); if (delta) this.mouse.scroll(-delta); })
.onKeyEvent((e: KeyEvent) => { if (e.type === KeyType.Down || e.type === KeyType.Up) { this.controller?.physicalKey(e.keyCode, e.type === KeyType.Down); return true; } return false; })
.onKeyEvent((e: KeyEvent) => { if (this.shortcutManager) return false; if (e.type === KeyType.Down || e.type === KeyType.Up) { this.controller?.physicalKey(e.keyCode, e.type === KeyType.Down); return true; } return false; })
.onBlur(() => this.controller?.release())
.accessibilityText(tr("远程桌面:轻触点击、长按右键、双指捏合缩放、放大后双指拖动画面,原始比例下双指滚动"))
MobileIcon({ name: 'cursor', iconExtent: 24, color: '#000000' })
@@ -388,6 +440,10 @@ export struct MobileRemoteView {
}.width(54).height(54).backgroundColor('#F5F5F7').borderRadius(27).shadow({ radius: 14, color: '#33000000', offsetY: 4 })
.position({ x: this.orbX, y: this.orbY }).accessibilityText(tr("远程控制菜单,拖动可移动")).accessibilityLevel('yes')
.onTouch((e: TouchEvent) => this.orbTouch(e))
if (this.shortcutManager) {
RemoteShortcutManager({ store: this.shortcutStore, platform: RemoteShortcuts.platform(this.state.platform),
changed: () => this.refreshShortcuts(), close: () => { this.shortcutManager = false; this.refreshShortcuts(); } })
}
}.width('100%').height('100%').backgroundColor(Color.Black).clip(true)
.onAreaChange((_old: Area, next: Area) => {
this.release(); this.viewWidth = Number(next.width); this.viewHeight = Number(next.height); this.fit();
@@ -0,0 +1,163 @@
import { tr } from './Localization';
import { MobileTheme, MobileSegment } from './MobileWidgets';
import { RemoteShortcut, RemoteShortcuts } from './RemoteShortcuts';
@Component
export struct RemoteShortcutPanel {
store?: RemoteShortcuts;
@Prop @Watch('reload') platform: string = 'unknown';
@Prop @Watch('reload') revision: number = 0;
send: (item: RemoteShortcut) => void = () => {};
manage: () => void = () => {};
changed: () => void = () => {};
@State private group: number = 0;
@State private page: number = 0;
@State private desktop: string = '';
@State private items: RemoteShortcut[] = [];
aboutToAppear(): void { this.reload(); }
private reload(): void { this.desktop = this.store?.desktop() ?? ''; this.items = this.store?.items(this.platform) ?? []; this.page = 0; }
private filtered(): RemoteShortcut[] { return this.items.filter((item: RemoteShortcut) => item.group === ['common', 'keys', 'system', 'mine'][this.group]); }
build() {
Column({ space: 6 }) {
MobileSegment({ labels: [tr('常用'), tr('按键'), tr('系统'), tr('我的')], selection: this.group,
changed: (index: number) => { this.group = index; this.page = 0; } })
if (this.group === 2 && this.platform === 'linux') {
Select([{ value: tr('选择桌面方案') }, { value: 'GNOME' }, { value: 'KDE Plasma' }])
.selected(this.desktop === 'gnome' ? 1 : this.desktop === 'kde' ? 2 : 0)
.value(this.desktop === 'gnome' ? 'GNOME' : this.desktop === 'kde' ? 'KDE Plasma' : tr('选择桌面方案'))
.font({ size: 12 }).height(32).onSelect((index: number) => { this.store?.setDesktop(['', 'gnome', 'kde'][index]); this.reload(); this.changed(); })
}
Scroll() {
Column({ space: 4 }) {
if (!this.filtered().length) {
Text(tr(this.platform === 'unknown' ? '等待远端平台信息' : this.group === 2 && this.platform === 'linux' ? '选择桌面方案以加载系统预设' : '还没有自定义快捷键'))
.fontSize(12).fontColor(MobileTheme.secondary).padding(12)
}
GridRow({ columns: 3, gutter: 4 }) {
ForEach(this.filtered().slice(this.page * 6, this.page * 6 + 6), (item: RemoteShortcut) => {
GridCol() {
Column({ space: 3 }) {
Text(item.title()).fontSize(12).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.chord()).fontSize(10).fontColor(MobileTheme.secondary).maxLines(1)
}.width('100%').height(48).justifyContent(FlexAlign.Center).borderRadius(7).backgroundColor(Color.White)
.accessibilityLevel('yes').accessibilityText(`${item.title()}, ${item.chord()}`).onClick(() => this.send(item))
}
}, (item: RemoteShortcut) => item.id)
}
}.width('100%')
}.width('100%').layoutWeight(1)
Row() {
Button('‹').width(40).height(36).enabled(this.page > 0).accessibilityText(tr('上一页')).onClick(() => this.page--)
Text(`${this.page + 1}/${Math.max(1, Math.ceil(this.filtered().length / 6))}`).fontSize(11).width(38).textAlign(TextAlign.Center)
Button('›').width(40).height(36).enabled((this.page + 1) * 6 < this.filtered().length).accessibilityText(tr('下一页')).onClick(() => this.page++)
Blank()
Button(tr('管理与自定义')).fontSize(12).height(36).enabled(this.platform !== 'unknown').onClick(this.manage)
}.width('100%')
}.width('100%').height('100%').padding(3)
}
}
@Component
export struct RemoteShortcutManager {
store?: RemoteShortcuts;
platform: string = 'unknown';
close: () => void = () => {};
changed: () => void = () => {};
@State private items: RemoteShortcut[] = [];
@State private pinned: RemoteShortcut[] = [];
@State private editing: boolean = false;
@State private itemId: string = '';
@State private name: string = '';
@State private target: string = 'windows';
@State private mainKey: number = 67;
@State private modifiers: number[] = [];
@State private pin: boolean = true;
@State private error: string = '';
aboutToAppear(): void { this.reload(); }
private reload(): void { this.items = this.store?.items(this.platform) ?? []; this.pinned = this.store?.pinned(this.platform) ?? []; this.changed(); }
private edit(item?: RemoteShortcut): void {
this.itemId = item?.id ?? ''; this.name = item?.name ?? ''; this.target = item?.platform ?? this.platform;
this.mainKey = item?.key ?? 67; this.modifiers = item?.modifiers.slice() ?? [this.target === 'macos' ? 91 : 17];
this.pin = item ? (this.store?.pins(item.platform).includes(item.id) ?? false) : (this.store?.pins(this.target).length ?? 0) < 8;
this.error = ''; this.editing = true;
}
private setPinned(item: RemoteShortcut, add: boolean): void {
const values = this.store?.pins(this.platform) ?? [];
if (add && values.length >= 8) { this.error = tr('最多固定 8 个快捷键'); return; }
this.store?.setPins(this.platform, add ? values.concat([item.id]) : values.filter((id: string) => id !== item.id)); this.error = ''; this.reload();
}
private moveUp(item: RemoteShortcut): void {
const values = this.store?.pins(this.platform) ?? [], index = values.indexOf(item.id);
if (index > 0) { values.splice(index, 1); values.splice(index - 1, 0, item.id); this.store?.setPins(this.platform, values); this.reload(); }
}
private save(): void {
this.error = this.store?.save(this.itemId, this.name, this.target, this.mainKey, this.modifiers, this.pin) ?? '';
if (!this.error) { this.editing = false; this.reload(); }
}
@Builder
Item(item: RemoteShortcut, pinned: boolean) {
Column({ space: 4 }) {
Row() {
Text(item.title()).fontSize(15).fontColor(MobileTheme.ink).layoutWeight(1).maxLines(2)
Text(item.chord()).fontSize(12).fontColor(MobileTheme.secondary).maxLines(2)
}.width('100%')
Row({ space: 6 }) {
if (pinned) {
Button(tr('向上移动')).fontSize(12).height(36).enabled(this.pinned[0]?.id !== item.id).onClick(() => this.moveUp(item))
Button(tr('移除')).fontSize(12).height(36).onClick(() => this.setPinned(item, false))
} else {
Button(tr('添加到常用栏')).fontSize(12).height(36).enabled(!this.pinned.some((value: RemoteShortcut) => value.id === item.id)).onClick(() => this.setPinned(item, true))
if (item.custom) {
Button(tr('编辑')).fontSize(12).height(36).onClick(() => this.edit(item))
Button(tr('删除')).fontSize(12).height(36).backgroundColor(MobileTheme.red).onClick(() => { this.store?.remove(item.id); this.reload(); })
}
}
}.width('100%')
}.padding(12).width('100%').borderRadius(10).backgroundColor(Color.White)
}
build() {
Column({ space: 12 }) {
Row() {
Button(tr(this.editing ? '取消' : '完成')).fontSize(14).height(44).onClick(() => { if (this.editing) this.editing = false; else this.close(); })
Text(tr(this.editing ? this.itemId ? '编辑快捷键' : '新建快捷键' : '管理快捷键')).fontSize(17).fontWeight(600).layoutWeight(1).textAlign(TextAlign.Center)
Button(tr(this.editing ? '保存' : '新建')).fontSize(14).height(44).onClick(() => { if (this.editing) this.save(); else this.edit(); })
}.width('100%')
Text(tr('本地编辑,输入不会发送到远端')).fontSize(12).fontColor(MobileTheme.secondary)
if (this.error) { Text(this.error).fontSize(13).fontColor(MobileTheme.red) }
Scroll() {
Column({ space: 12 }) {
if (this.editing) {
TextInput({ placeholder: tr('名称'), text: this.name }).maxLength(64).onChange((value: string) => this.name = value)
Text(tr('适用远端')).width('100%').fontSize(13)
Select(RemoteShortcuts.platforms.map((os: string): SelectOption => ({ value: RemoteShortcuts.platformName(os) })))
.selected(RemoteShortcuts.platforms.indexOf(this.target)).value(RemoteShortcuts.platformName(this.target))
.width('100%').onSelect((index: number) => this.target = RemoteShortcuts.platforms[index])
Text(tr('主按键')).width('100%').fontSize(13)
Select(RemoteShortcuts.keys().map((key: number): SelectOption => ({ value: RemoteShortcuts.keyName(key, this.target) })))
.selected(RemoteShortcuts.keys().indexOf(this.mainKey)).value(RemoteShortcuts.keyName(this.mainKey, this.target))
.width('100%').onSelect((index: number) => this.mainKey = RemoteShortcuts.keys()[index])
Text(tr('修饰键')).width('100%').fontSize(13)
Row({ space: 6 }) {
ForEach(RemoteShortcuts.modifiers, (key: number) => {
Button(RemoteShortcuts.modifierName(key, this.target)).layoutWeight(1).height(44)
.backgroundColor(this.modifiers.includes(key) ? MobileTheme.blue : MobileTheme.secondary)
.accessibilitySelected(this.modifiers.includes(key)).onClick(() => this.modifiers = this.modifiers.includes(key) ? this.modifiers.filter((value: number) => value !== key) : RemoteShortcuts.normalize(this.modifiers.concat([key])))
}, (key: number) => `${key}-${this.target}`)
}.width('100%')
Text(`${tr('发送组合')}: ${new RemoteShortcut('', '', this.target, 'mine', this.mainKey, this.modifiers).chord()}`).fontSize(16).padding(12)
Row() {
Text(tr('固定到键盘上方')).layoutWeight(1).fontSize(14)
Toggle({ type: ToggleType.Switch, isOn: this.pin }).onChange((value: boolean) => this.pin = value)
}.width('100%')
} else {
Text(`${RemoteShortcuts.platformName(this.platform)} · ${tr('键盘上方常用栏')} (${this.pinned.length}/8)`).fontSize(14).width('100%')
ForEach(this.pinned, (item: RemoteShortcut) => { this.Item(item, true) }, (item: RemoteShortcut) => item.id)
Text(tr('快捷键')).fontSize(14).width('100%')
ForEach(this.items, (item: RemoteShortcut) => { this.Item(item, false) }, (item: RemoteShortcut) => item.id)
}
}.width('100%').padding({ bottom: 24 })
}.layoutWeight(1).width('100%')
}.padding(16).width('100%').height('100%').backgroundColor(MobileTheme.background)
.onKeyEvent((event: KeyEvent) => { if (event.keyCode === 2 && event.type === KeyType.Down) { if (this.editing) this.editing = false; else this.close(); return true; } return false; })
}
}
@@ -0,0 +1,110 @@
import { MobileStore } from './MobileStore';
import { tr } from './Localization';
export class RemoteShortcut {
id: string; name: string; platform: string; group: string; key: number; modifiers: number[];
service: boolean; custom: boolean;
constructor(id: string, name: string, platform: string, group: string, key: number, modifiers: number[] = [], service: boolean = false, custom: boolean = false) {
this.id = id; this.name = name; this.platform = platform; this.group = group; this.key = key;
this.modifiers = modifiers.slice(); this.service = service; this.custom = custom;
}
title(): string { return this.custom ? this.name : tr(this.name); }
chord(): string { return this.modifiers.map((key: number) => RemoteShortcuts.modifierName(key, this.platform)).concat([RemoteShortcuts.keyName(this.key, this.platform)]).join(' + '); }
}
export class RemoteShortcuts {
static readonly platforms: string[] = ['windows', 'macos', 'linux'];
static readonly modifiers: number[] = [17, 16, 18, 91];
private store: MobileStore;
private prefix: string = 'remote_shortcuts_v1:';
constructor(store: MobileStore) { this.store = store; }
static platform(value: string): string { return RemoteShortcuts.platforms.includes(value) ? value : 'unknown'; }
static platformName(value: string): string { return value === 'windows' ? 'Windows' : value === 'macos' ? 'macOS' : value === 'linux' ? 'Linux' : tr('未知'); }
static modifierName(key: number, platform: string): string {
return key === 17 ? 'Ctrl' : key === 16 ? 'Shift' : key === 18 ? (platform === 'macos' ? 'Option' : 'Alt') : key === 91 ? (platform === 'macos' ? 'Cmd' : platform === 'linux' ? 'Super' : 'Win') : '';
}
static keyName(key: number, platform: string): string {
if (key >= 65 && key <= 90 || key >= 48 && key <= 57) return String.fromCharCode(key);
if (key >= 112 && key <= 123) return `F${key - 111}`;
switch (key) {
case 8: return platform === 'macos' ? 'Delete ⌫' : 'Backspace'; case 9: return 'Tab'; case 13: return 'Enter';
case 27: return 'Esc'; case 32: return 'Space'; case 33: return 'PageUp'; case 34: return 'PageDown';
case 35: return 'End'; case 36: return 'Home'; case 37: return '←'; case 38: return '↑'; case 39: return '→';
case 40: return '↓'; case 45: return 'Insert'; case 46: return platform === 'macos' ? 'Delete ⌦' : 'Delete'; default: return '';
}
}
static keys(): number[] {
const keys: number[] = []; for (let i = 65; i <= 90; i++) keys.push(i); for (let i = 48; i <= 57; i++) keys.push(i);
for (let i = 112; i <= 123; i++) keys.push(i);
return keys.concat([27, 9, 13, 32, 8, 46, 45, 36, 35, 33, 34, 37, 38, 39, 40]);
}
static normalize(values: number[]): number[] { return RemoteShortcuts.modifiers.filter((key: number) => values.includes(key)); }
desktop(): string { const value = this.store.text(this.prefix + 'linux_desktop'); return ['gnome', 'kde'].includes(value) ? value : ''; }
setDesktop(value: string): void { this.store.put(this.prefix + 'linux_desktop', ['gnome', 'kde'].includes(value) ? value : ''); }
private preset(id: string, name: string, platform: string, group: string, key: number, modifiers: number[] = []): RemoteShortcut { return new RemoteShortcut(id, name, platform, group, key, modifiers); }
items(remote: string): RemoteShortcut[] {
const os = RemoteShortcuts.platform(remote); let result: RemoteShortcut[] = [];
if (os !== 'unknown') {
const ids = ['copy', 'paste', 'cut', 'all', 'undo', 'save'], names = ['复制', '粘贴', '剪切', '全选', '撤销', '保存'], keys = [67, 86, 88, 65, 90, 83];
for (let i = 0; i < ids.length; i++) result.push(this.preset(ids[i], names[i], os, 'common', keys[i], [os === 'macos' ? 91 : 17]));
if (os === 'linux') result = result.concat([this.preset('term-copy', '终端复制', os, 'common', 67, [17, 16]), this.preset('term-paste', '终端粘贴', os, 'common', 86, [17, 16])]);
}
const keys = [27, 9, 13, 8, 46, 36, 35, 33, 34, 37, 38, 39, 40]; for (let i = 112; i <= 123; i++) keys.push(i);
keys.forEach((key: number) => result.push(this.preset(key === 27 ? 'esc' : key === 9 ? 'tab' : `key-${key}`, RemoteShortcuts.keyName(key, os), os, 'keys', key)));
if (os === 'windows') {
result = result.concat([this.preset('switch', '切换窗口', os, 'system', 9, [18]), this.preset('desktop', '显示桌面', os, 'system', 68, [91]), this.preset('task', '任务管理器', os, 'system', 27, [17, 16]), new RemoteShortcut('sas', '安全选项', os, 'system', 46, [17, 18], true)]);
} else if (os === 'macos') {
result = result.concat([this.preset('switch', '切换应用', os, 'system', 9, [91]), this.preset('search', '聚焦搜索', os, 'system', 32, [91]), this.preset('forcequit', '强制退出面板', os, 'system', 27, [18, 91])]);
} else if (os === 'linux' && this.desktop()) {
result.push(this.preset('switch', this.desktop() === 'gnome' ? '切换应用' : '切换窗口', os, 'system', 9, [this.desktop() === 'gnome' ? 91 : 18]));
result.push(this.preset('run', '运行窗口', os, 'system', 113, [18]));
result.push(this.desktop() === 'gnome' ? this.preset('apps', '应用列表', os, 'system', 65, [91]) : this.preset('window-menu', '窗口菜单', os, 'system', 114, [18]));
}
return result.concat(this.custom().filter((item: RemoteShortcut) => item.platform === os));
}
custom(): RemoteShortcut[] {
try {
const raw = JSON.parse(this.store.text(this.prefix + 'custom', '[]')) as RemoteShortcut[];
if (!Array.isArray(raw)) return [];
const seen: string[] = [], result: RemoteShortcut[] = [];
raw.forEach((item: RemoteShortcut) => {
if (!item || typeof item.id !== 'string' || !item.id.startsWith('custom-') || seen.includes(item.id) || typeof item.name !== 'string' || !item.name.trim() || item.name.length > 64 || !RemoteShortcuts.platforms.includes(item.platform) || !RemoteShortcuts.keys().includes(item.key) || !Array.isArray(item.modifiers) || item.modifiers.length > 4 || !item.modifiers.every((key: number) => RemoteShortcuts.modifiers.includes(key))) return;
if (item.platform === 'windows' && item.key === 46 && item.modifiers.includes(17) && item.modifiers.includes(18)) return;
seen.push(item.id); result.push(new RemoteShortcut(item.id, item.name, item.platform, 'mine', item.key, RemoteShortcuts.normalize(item.modifiers), false, true));
});
return result.slice(0, 16);
} catch { return []; }
}
pins(platform: string): string[] {
const raw = this.store.text(this.prefix + 'pins_' + RemoteShortcuts.platform(platform), '');
const available = this.items(platform).map((item: RemoteShortcut) => item.id);
let values: string[] = ['esc', 'tab', 'copy', 'paste'];
try { const parsed = JSON.parse(raw) as string[]; if (Array.isArray(parsed)) values = parsed; } catch {}
return values.filter((id: string, index: number) => typeof id === 'string' && available.includes(id) && values.indexOf(id) === index).slice(0, 8);
}
setPins(platform: string, values: string[]): void { this.store.put(this.prefix + 'pins_' + RemoteShortcuts.platform(platform), JSON.stringify(values.slice(0, 8))); }
pinned(platform: string): RemoteShortcut[] {
const items = this.items(platform), result: RemoteShortcut[] = [];
this.pins(platform).forEach((id: string) => { const item = items.find((value: RemoteShortcut) => value.id === id); if (item) result.push(item); });
return result;
}
remove(id: string): void {
this.store.put(this.prefix + 'custom', JSON.stringify(this.custom().filter((item: RemoteShortcut) => item.id !== id)));
RemoteShortcuts.platforms.forEach((platform: string) => this.setPins(platform, this.pins(platform).filter((value: string) => value !== id)));
}
save(id: string, name: string, platform: string, key: number, modifiers: number[], pin: boolean): string {
name = name.trim(); if (!name || name.length > 64) return tr('请输入有效的快捷键名称');
if (!RemoteShortcuts.platforms.includes(platform) || !RemoteShortcuts.keys().includes(key)) return tr('请选择有效的按键');
const normalized = RemoteShortcuts.normalize(modifiers);
if (platform === 'windows' && key === 46 && normalized.includes(17) && normalized.includes(18)) return tr('请使用系统预设中的安全选项');
const old = this.custom();
if (old.some((item: RemoteShortcut) => item.id !== id && item.platform === platform && item.key === key && item.modifiers.join(',') === normalized.join(','))) return tr('已有相同组合的快捷键');
if (!old.some((item: RemoteShortcut) => item.id === id) && old.length >= 16) return tr('最多保存 16 个自定义快捷键');
if (pin && !this.pins(platform).includes(id) && this.pins(platform).length >= 8) return tr('最多固定 8 个快捷键');
const itemId = id || `custom-${Date.now()}-${Math.floor(Math.random() * 1000000000)}`;
const values = old.filter((item: RemoteShortcut) => item.id !== itemId); values.push(new RemoteShortcut(itemId, name, platform, 'mine', key, normalized, false, true));
this.store.put(this.prefix + 'custom', JSON.stringify(values));
RemoteShortcuts.platforms.forEach((target: string) => { const list = this.pins(target), index = list.indexOf(itemId), selected = list.filter((value: string) => value !== itemId); if (target === platform && pin) selected.splice(index >= 0 ? Math.min(index, selected.length) : selected.length, 0, itemId); this.setPins(target, selected); });
return '';
}
}
+15
View File
@@ -327,6 +327,21 @@ class MobileSession {
if (key<=0||key>255) return;
const bool down=v.at("down").get<bool>(); if (down) keys_.insert(key); else keys_.erase(key);
RemoteAction a{}; a.type=keyboard; a.k={size_t(key),0,false,down?key_down:key_up}; Send(a,kKeyboardStream);
} else if (op=="chord") {
if (v.at("connection").get<uint64_t>() != connection_) return;
const int key=v.at("code").get<int>();
const auto modifiers=v.at("modifiers").get<std::vector<int>>();
if (key<=0||key>255||modifiers.size()>4) return;
std::vector<int> ordered;
for (const int modifier : {17,16,18,91})
if (std::find(modifiers.begin(),modifiers.end(),modifier)!=modifiers.end()) ordered.push_back(modifier);
const auto stroke=[this](int code,bool down) {
if (down) keys_.insert(code); else keys_.erase(code);
RemoteAction a{}; a.type=keyboard; a.k={size_t(code),0,false,down?key_down:key_up}; Send(a,kKeyboardStream);
};
for (const int modifier : ordered) stroke(modifier,true);
stroke(key,true); stroke(key,false);
for (auto it=ordered.rbegin();it!=ordered.rend();++it) stroke(*it,false);
} else if (op=="display") {
const int index=v.at("index").get<int>(); if(index<0||index>=32)return;
Release(); display_=index; ++video_generation_; renderer_.Clear();
@@ -34,6 +34,8 @@
10000000000000000000001D /* RemoteMouseInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000001D /* RemoteMouseInput.swift */; };
10000000000000000000001E /* VirtualMouseBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000001E /* VirtualMouseBar.swift */; };
100000000000000000000030 /* RemoteSystemKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000030 /* RemoteSystemKeyboard.swift */; };
100000000000000000000031 /* RemoteShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000031 /* RemoteShortcuts.swift */; };
100000000000000000000032 /* RemoteShortcutPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000032 /* RemoteShortcutPanel.swift */; };
3C550ED141D4AEA30950BB7A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5BD6CC3A43B86A2E2267994F /* Localizable.strings */; };
4870B14C46F4C4B8F10FEF08 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = A0A3C0DA09EE562AE21DF986 /* InfoPlist.strings */; };
C10F10000000000000000001 /* RemotePrivacyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10F20000000000000000001 /* RemotePrivacyScreen.swift */; };
@@ -92,6 +94,8 @@
20000000000000000000001D /* RemoteMouseInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMouseInput.swift; sourceTree = "<group>"; };
20000000000000000000001E /* VirtualMouseBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualMouseBar.swift; sourceTree = "<group>"; };
200000000000000000000030 /* RemoteSystemKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSystemKeyboard.swift; sourceTree = "<group>"; };
200000000000000000000031 /* RemoteShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteShortcuts.swift; sourceTree = "<group>"; };
200000000000000000000032 /* RemoteShortcutPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteShortcutPanel.swift; sourceTree = "<group>"; };
23D5AD87A9BF0CBF3A6AADDF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Resources/en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
2E83770B40B2D299796EB0CD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "Resources/zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
2EA0435DFAEE685F04391B3B /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = Resources/es.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -187,6 +191,8 @@
children = (
20000000000000000000000A /* RemoteTouchInputView.swift */,
200000000000000000000030 /* RemoteSystemKeyboard.swift */,
200000000000000000000031 /* RemoteShortcuts.swift */,
200000000000000000000032 /* RemoteShortcutPanel.swift */,
20000000000000000000001D /* RemoteMouseInput.swift */,
20000000000000000000001E /* VirtualMouseBar.swift */,
);
@@ -361,6 +367,8 @@
100000000000000000000006 /* NativeMetalVideoView.swift in Sources */,
100000000000000000000007 /* RemoteTouchInputView.swift in Sources */,
100000000000000000000030 /* RemoteSystemKeyboard.swift in Sources */,
100000000000000000000031 /* RemoteShortcuts.swift in Sources */,
100000000000000000000032 /* RemoteShortcutPanel.swift in Sources */,
10000000000000000000001D /* RemoteMouseInput.swift in Sources */,
10000000000000000000001E /* VirtualMouseBar.swift in Sources */,
100000000000000000000012 /* PrivacyPreferences.swift in Sources */,
@@ -195,6 +195,7 @@ typedef struct {
- (void)sendScrollX:(float)x y:(float)y deltaX:(NSInteger)deltaX
deltaY:(NSInteger)deltaY;
- (void)sendWindowsKeyCode:(NSUInteger)keyCode isDown:(BOOL)isDown;
- (void)sendKeyChord:(NSUInteger)keyCode modifiers:(NSArray<NSNumber *> *)modifiers;
- (void)sendText:(NSString *)text;
- (void)sendCommittedText:(NSString *)text;
- (void)switchToDisplay:(NSInteger)displayIndex;
@@ -974,6 +974,30 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
stream:kKeyboardStream];
}
- (void)sendKeyChord:(NSUInteger)keyCode modifiers:(NSArray<NSNumber *> *)modifiers {
if (!keyCode || keyCode > 255 || modifiers.count > 4) return;
std::vector<NSUInteger> keys;
for (NSUInteger code : {17u, 16u, 18u, 91u}) {
if ([modifiers containsObject:@(code)]) keys.push_back(code);
}
std::vector<std::string> messages;
for (NSUInteger code : keys) messages.push_back(KeyJSON(code, true));
messages.push_back(KeyJSON(keyCode, true));
messages.push_back(KeyJSON(keyCode, false));
for (auto it = keys.rbegin(); it != keys.rend(); ++it)
messages.push_back(KeyJSON(*it, false));
const uint64_t generation = _activeControllerGeneration.load();
// A whole chord shares one RTC task and can never spill into a new session.
dispatch_async(_rtcQueue, ^{
if (![self isControllerGenerationActive:generation] ||
!self->_state->controller_peer) return;
for (const auto& message : messages) {
MiniRtcSendReliableDataFrame(self->_state->controller_peer,
message.data(), message.size(), kKeyboardStream);
}
});
}
- (void)sendText:(NSString *)text {
NSData *utf8 = [text dataUsingEncoding:NSUTF8StringEncoding];
if (!utf8.length) return;
@@ -0,0 +1,225 @@
import SwiftUI
struct RemoteShortcutPanel: View {
@ObservedObject var store: RemoteShortcutStore
@ObservedObject var session: RemoteSessionModel
let systemKeyboard: () -> Void
let dismiss: () -> Void
let manage: () -> Void
@State private var category = "common"
@State private var page = 0
private let groups = ["common", "keys", "system", "mine"]
private let labels = ["常用", "按键", "系统", "我的"]
private var platform: String { session.remotePlatform.rawValue }
private var items: [RemoteShortcut] { store.items(platform: platform).filter { $0.group == category } }
var body: some View {
VStack(spacing: 0) {
RemoteKeyboardToolbar(
switchKeyboard: systemKeyboard,
more: systemKeyboard,
moreAccessibilityLabel: NSLocalizedString("系统键盘", comment: ""),
dismiss: dismiss
) {
Text(NSLocalizedString("快捷键", comment: ""))
.font(.subheadline).foregroundStyle(.secondary)
}
VStack(spacing: 4) {
Picker(NSLocalizedString("快捷键", comment: ""), selection: $category) {
ForEach(Array(groups.enumerated()), id: \.element) { index, group in
Text(NSLocalizedString(labels[index], comment: "")).tag(group)
}
}.pickerStyle(.segmented)
if category == "system" && platform == "linux" {
Picker(NSLocalizedString("选择桌面方案", comment: ""), selection: Binding(get: { store.desktop }, set: { store.desktop = $0 })) {
Text(NSLocalizedString("选择桌面方案", comment: "")).tag("")
Text("GNOME").tag("gnome")
Text("KDE Plasma").tag("kde")
}.frame(maxWidth: .infinity, alignment: .leading)
}
GeometryReader { geometry in
ScrollView {
if items.isEmpty {
Text(NSLocalizedString(!RemoteShortcut.platforms.contains(platform) ? "等待远端平台信息" : category == "system" ? "选择桌面方案以加载系统预设" : "还没有自定义快捷键", comment: ""))
.font(.footnote).foregroundStyle(.secondary).frame(maxWidth: .infinity).padding(.vertical, 16)
} else {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 6), count: 3), spacing: 6) {
ForEach(Array(items.dropFirst(page * 6).prefix(6))) { item in
Button {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
session.sendShortcut(item)
} label: {
VStack(spacing: 3) {
Text(item.title).font(.subheadline)
Text(item.chord).font(.caption2).foregroundStyle(.secondary)
}
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity, minHeight: max(44, (geometry.size.height - (session.keyboardInputStatus.isEmpty ? 6 : 34)) / 2))
.background(Color(uiColor: .secondarySystemBackground), in: RoundedRectangle(cornerRadius: 7))
}.disabled(!session.isConnected)
.accessibilityLabel(item.title + ", " + item.chord)
}
}
}
if !session.keyboardInputStatus.isEmpty {
Text(session.keyboardInputStatus).font(.caption).foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
HStack(spacing: 6) {
if items.count > 6 {
Button { page = max(0, page - 1) } label: { Image(systemName: "chevron.left").frame(width: 44, height: 44) }
.disabled(page == 0).accessibilityLabel(NSLocalizedString("上一页", comment: ""))
Text("\(page + 1) / \((items.count + 5) / 6)").font(.caption)
Button { page += 1 } label: { Image(systemName: "chevron.right").frame(width: 44, height: 44) }
.disabled((page + 1) * 6 >= items.count).accessibilityLabel(NSLocalizedString("下一页", comment: ""))
} else {
Text(RemoteShortcut.platformName(platform)).font(.caption).foregroundStyle(.secondary)
}
Spacer(minLength: 0)
Button(NSLocalizedString("管理与自定义", comment: ""), action: manage)
.font(.subheadline).frame(minHeight: 44)
.disabled(!RemoteShortcut.platforms.contains(platform))
}.frame(height: 44)
}
.padding(.horizontal, 6)
.padding(.top, 4)
}
.buttonStyle(.plain)
.background(Color(uiColor: RemoteKeyboardInputBar.keyboardBackground))
.onValueChange(of: category) { _ in page = 0 }
.onValueChange(of: platform) { _ in page = 0 }
.onValueChange(of: store.revision) { _ in page = min(page, max(0, (items.count - 1) / 6)) }
}
}
struct RemoteShortcutManager: View {
@ObservedObject var store: RemoteShortcutStore
let platform: String
@Environment(\.dismiss) private var dismiss
@State private var editor: RemoteShortcut?
@State private var error = ""
var body: some View {
NavigationStack {
List {
Section {
Text(NSLocalizedString("本地编辑,输入不会发送到远端", comment: "")).font(.footnote).foregroundStyle(.secondary)
}
Section(NSLocalizedString("键盘上方常用栏", comment: "")) {
ForEach(store.pinned(platform: platform)) { item in
HStack {
label(item)
Spacer(minLength: 2)
Button {
var values = store.pins(platform: platform)
if let index = values.firstIndex(of: item.id), index > 0 { values.swapAt(index - 1, index); store.setPins(values, platform: platform) }
} label: { Image(systemName: "arrow.up").frame(width: 44, height: 44) }
.disabled(store.pins(platform: platform).first == item.id)
.accessibilityLabel(NSLocalizedString("向上移动", comment: ""))
Button(NSLocalizedString("移除", comment: "")) { store.setPins(store.pins(platform: platform).filter { $0 != item.id }, platform: platform) }
.frame(minHeight: 44)
}.buttonStyle(.borderless)
}
}
Section(NSLocalizedString("添加到常用栏", comment: "")) {
ForEach(store.items(platform: platform)) { item in
HStack {
label(item)
Spacer(minLength: 2)
if !store.pins(platform: platform).contains(item.id) {
Button {
var values = store.pins(platform: platform)
guard values.count < 8 else { error = NSLocalizedString("最多固定 8 个快捷键", comment: ""); return }
values.append(item.id); store.setPins(values, platform: platform)
} label: { Image(systemName: "plus").frame(width: 44, height: 44) }
.accessibilityLabel(NSLocalizedString("添加到常用栏", comment: ""))
}
if item.custom {
Button(NSLocalizedString("编辑", comment: "")) { editor = item }.frame(minHeight: 44)
Button(role: .destructive) { store.remove(item.id) } label: { Image(systemName: "trash").frame(width: 44, height: 44) }
.accessibilityLabel(NSLocalizedString("删除", comment: ""))
}
}.buttonStyle(.borderless)
}
}
if !error.isEmpty { Text(error).foregroundStyle(.red) }
}
.navigationTitle(NSLocalizedString("管理快捷键", comment: ""))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) { Button(NSLocalizedString("完成", comment: "")) { dismiss() } }
ToolbarItem(placement: .primaryAction) {
Button(NSLocalizedString("新建", comment: "")) {
editor = RemoteShortcut(id: UUID().uuidString, name: "", platform: platform, group: "mine", key: 83, modifiers: [platform == "macos" ? 91 : 17])
}
}
}
.sheet(item: $editor) { item in RemoteShortcutEditor(store: store, item: item) }
}
}
private func label(_ item: RemoteShortcut) -> some View {
VStack(alignment: .leading, spacing: 3) { Text(item.title); Text(item.chord).font(.caption).foregroundStyle(.secondary) }
}
}
private struct RemoteShortcutEditor: View {
@ObservedObject var store: RemoteShortcutStore
let item: RemoteShortcut
@Environment(\.dismiss) private var dismiss
@State private var name: String
@State private var platform: String
@State private var key: UInt
@State private var modifiers: Set<UInt>
@State private var pin: Bool
@State private var error = ""
init(store: RemoteShortcutStore, item: RemoteShortcut) {
self.store = store; self.item = item
_name = State(initialValue: item.name); _platform = State(initialValue: item.platform)
_key = State(initialValue: item.key); _modifiers = State(initialValue: Set(item.modifiers))
_pin = State(initialValue: item.custom ? store.pins(platform: item.platform).contains(item.id) : store.pins(platform: item.platform).count < 8)
}
private var preview: RemoteShortcut { RemoteShortcut(id: "", name: name, platform: platform, group: "mine", key: key, modifiers: RemoteShortcut.modifierKeys.filter(modifiers.contains)) }
var body: some View {
NavigationStack {
Form {
Section { Text(NSLocalizedString("本地编辑,输入不会发送到远端", comment: "")).font(.footnote).foregroundStyle(.secondary) }
TextField(NSLocalizedString("名称", comment: ""), text: $name)
Picker(NSLocalizedString("适用远端", comment: ""), selection: $platform) {
ForEach(RemoteShortcut.platforms, id: \.self) { Text(RemoteShortcut.platformName($0)).tag($0) }
}
Picker(NSLocalizedString("主按键", comment: ""), selection: $key) {
ForEach(RemoteShortcut.keys, id: \.self) { Text(RemoteShortcut.keyName($0, platform: platform)).tag($0) }
}
Section(NSLocalizedString("修饰键", comment: "")) {
HStack(spacing: 6) {
ForEach(RemoteShortcut.modifierKeys, id: \.self) { code in
Button {
if modifiers.contains(code) { modifiers.remove(code) } else { modifiers.insert(code) }
} label: {
Text(RemoteShortcut.modifierName(code, platform: platform)).font(.subheadline)
.frame(maxWidth: .infinity, minHeight: 44)
.background(modifiers.contains(code) ? Color.accentColor.opacity(0.18) : Color(uiColor: .tertiarySystemFill), in: RoundedRectangle(cornerRadius: 7))
}.buttonStyle(.borderless)
.accessibilityAddTraits(modifiers.contains(code) ? .isSelected : [])
}
}
}
Section(NSLocalizedString("发送组合", comment: "")) { Text(preview.chord) }
Toggle(NSLocalizedString("固定到键盘上方", comment: ""), isOn: $pin)
if !error.isEmpty { Text(error).foregroundStyle(.red) }
}
.navigationTitle(NSLocalizedString(item.custom ? "编辑快捷键" : "新建快捷键", comment: ""))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) { Button(NSLocalizedString("取消", comment: "")) { dismiss() } }
ToolbarItem(placement: .confirmationAction) {
Button(NSLocalizedString("保存", comment: "")) {
if let message = store.save(id: item.custom ? item.id : nil, name: name, platform: platform, key: key, modifiers: Array(modifiers), pin: pin) { error = message } else { dismiss() }
}
}
}
}
}
}
@@ -0,0 +1,156 @@
import Foundation
import Combine
struct RemoteShortcut: Codable, Identifiable, Equatable {
let id: String
let name: String
let platform: String
let group: String
let key: UInt
let modifiers: [UInt]
var service = false
var custom = false
var title: String { custom ? name : NSLocalizedString(name, comment: "") }
var chord: String {
(modifiers.map { Self.modifierName($0, platform: platform) } +
[Self.keyName(key, platform: platform)]).joined(separator: " + ")
}
static let platforms = ["windows", "macos", "linux"]
static let modifierKeys: [UInt] = [17, 16, 18, 91]
static let keys: [UInt] = Array(65...90) + Array(48...57) + Array(112...123) +
[27, 9, 13, 32, 8, 46, 45, 36, 35, 33, 34, 37, 38, 39, 40]
static func modifierName(_ key: UInt, platform: String) -> String {
switch key {
case 17: return "Ctrl"
case 16: return "Shift"
case 18: return platform == "macos" ? "Option" : "Alt"
case 91: return platform == "macos" ? "Cmd" : platform == "linux" ? "Super" : "Win"
default: return ""
}
}
static func keyName(_ key: UInt, platform: String) -> String {
if (65...90).contains(key) || (48...57).contains(key) { return String(UnicodeScalar(UInt32(key))!) }
if (112...123).contains(key) { return "F\(key - 111)" }
switch key {
case 8: return platform == "macos" ? "Delete ⌫" : "Backspace"
case 9: return "Tab"
case 13: return "Enter"
case 27: return "Esc"
case 32: return "Space"
case 33: return "PageUp"
case 34: return "PageDown"
case 35: return "End"
case 36: return "Home"
case 37: return "←"
case 38: return "↑"
case 39: return "→"
case 40: return "↓"
case 45: return "Insert"
case 46: return platform == "macos" ? "Delete ⌦" : "Delete"
default: return ""
}
}
static func platformName(_ platform: String) -> String {
switch platform {
case "windows": return "Windows"
case "macos": return "macOS"
case "linux": return "Linux"
default: return NSLocalizedString("未知", comment: "")
}
}
var valid: Bool {
Self.platforms.contains(platform) && id.hasPrefix("custom-") &&
!name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && name.count <= 64 &&
Self.keys.contains(key) && modifiers.count <= 4 &&
modifiers.allSatisfy(Self.modifierKeys.contains) &&
!(platform == "windows" && key == 46 && modifiers.contains(17) && modifiers.contains(18))
}
}
final class RemoteShortcutStore: ObservableObject {
@Published private(set) var revision = 0
private let defaults: UserDefaults
private let prefix = "remote_shortcuts_v1."
init(defaults: UserDefaults = .standard) { self.defaults = defaults }
var desktop: String {
get { let value = defaults.string(forKey: prefix + "linux_desktop") ?? ""; return ["gnome", "kde"].contains(value) ? value : "" }
set { defaults.set(["gnome", "kde"].contains(newValue) ? newValue : "", forKey: prefix + "linux_desktop"); revision &+= 1 }
}
var custom: [RemoteShortcut] {
guard let data = defaults.data(forKey: prefix + "custom"),
let decoded = try? JSONDecoder().decode([RemoteShortcut].self, from: data) else { return [] }
var seen = Set<String>()
return Array(decoded.filter { $0.valid && $0.custom && !$0.service && seen.insert($0.id).inserted }.prefix(16))
}
func items(platform: String) -> [RemoteShortcut] {
func item(_ id: String, _ name: String, _ group: String, _ key: UInt, _ modifiers: UInt...) -> RemoteShortcut {
RemoteShortcut(id: id, name: name, platform: platform, group: group, key: key, modifiers: modifiers)
}
var result: [RemoteShortcut] = []
if RemoteShortcut.platforms.contains(platform) {
let primary: UInt = platform == "macos" ? 91 : 17
let names = ["复制", "粘贴", "剪切", "全选", "撤销", "保存"]
let ids = ["copy", "paste", "cut", "all", "undo", "save"]
let keys: [UInt] = [67, 86, 88, 65, 90, 83]
for index in ids.indices { result.append(item(ids[index], names[index], "common", keys[index], primary)) }
if platform == "linux" {
result += [item("term-copy", "终端复制", "common", 67, 17, 16), item("term-paste", "终端粘贴", "common", 86, 17, 16)]
}
}
let keys: [UInt] = [27, 9, 13, 8, 46, 36, 35, 33, 34, 37, 38, 39, 40] + Array(112...123)
for key in keys {
result.append(item(key == 27 ? "esc" : key == 9 ? "tab" : "key-\(key)", RemoteShortcut.keyName(key, platform: platform), "keys", key))
}
if platform == "windows" {
result += [item("switch", "切换窗口", "system", 9, 18), item("desktop", "显示桌面", "system", 68, 91), item("task", "任务管理器", "system", 27, 17, 16)]
result.append(RemoteShortcut(id: "sas", name: "安全选项", platform: platform, group: "system", key: 46, modifiers: [17, 18], service: true))
} else if platform == "macos" {
result += [item("switch", "切换应用", "system", 9, 91), item("search", "聚焦搜索", "system", 32, 91), item("forcequit", "强制退出面板", "system", 27, 18, 91)]
} else if platform == "linux" && !desktop.isEmpty {
result += [item("switch", desktop == "gnome" ? "切换应用" : "切换窗口", "system", 9, desktop == "gnome" ? 91 : 18), item("run", "运行窗口", "system", 113, 18)]
result.append(desktop == "gnome" ? item("apps", "应用列表", "system", 65, 91) : item("window-menu", "窗口菜单", "system", 114, 18))
}
return result + custom.filter { $0.platform == platform }
}
func pins(platform: String) -> [String] {
let values = defaults.stringArray(forKey: prefix + "pins_" + platform) ?? ["esc", "tab", "copy", "paste"]
let available = Set(items(platform: platform).map(\.id))
var seen = Set<String>()
return Array(values.filter { available.contains($0) && seen.insert($0).inserted }.prefix(8))
}
func pinned(platform: String) -> [RemoteShortcut] {
let all = items(platform: platform)
return pins(platform: platform).compactMap { id in all.first { $0.id == id } }
}
func setPins(_ values: [String], platform: String) {
defaults.set(Array(values.prefix(8)), forKey: prefix + "pins_" + platform); revision &+= 1
}
func remove(_ id: String) {
write(custom.filter { $0.id != id })
for platform in RemoteShortcut.platforms { setPins(pins(platform: platform).filter { $0 != id }, platform: platform) }
}
func save(id: String?, name: String, platform: String, key: UInt, modifiers: [UInt], pin: Bool) -> String? {
let name = name.trimmingCharacters(in: .whitespacesAndNewlines)
let normalized = RemoteShortcut.modifierKeys.filter(modifiers.contains)
let item = RemoteShortcut(id: id ?? "custom-" + UUID().uuidString, name: name, platform: platform, group: "mine", key: key, modifiers: normalized, custom: true)
if platform == "windows" && key == 46 && normalized.contains(17) && normalized.contains(18) { return NSLocalizedString("请使用系统预设中的安全选项", comment: "") }
guard item.valid else { return NSLocalizedString("请输入有效的快捷键名称", comment: "") }
var values = custom
if values.contains(where: { $0.id != id && $0.platform == platform && $0.key == key && Set($0.modifiers) == Set(normalized) }) { return NSLocalizedString("已有相同组合的快捷键", comment: "") }
if !values.contains(where: { $0.id == id }) && values.count >= 16 { return NSLocalizedString("最多保存 16 个自定义快捷键", comment: "") }
if pin && !pins(platform: platform).contains(item.id) && pins(platform: platform).count >= 8 { return NSLocalizedString("最多固定 8 个快捷键", comment: "") }
values.removeAll { $0.id == id }; values.append(item); write(values)
for target in RemoteShortcut.platforms {
var selected = pins(platform: target); let previous = selected.firstIndex(of: item.id)
selected.removeAll { $0 == item.id }
if target == platform && pin { selected.insert(item.id, at: min(previous ?? selected.count, selected.count)) }
setPins(selected, platform: target)
}
return nil
}
private func write(_ values: [RemoteShortcut]) {
guard let data = try? JSONEncoder().encode(values) else { return }
defaults.set(data, forKey: prefix + "custom"); revision &+= 1
}
}
@@ -10,6 +10,9 @@ struct RemoteSystemKeyboard: View {
var geometry: RemoteKeyboardGeometry? = nil
var animatePresentation = true
var didPresent: () -> Void = {}
var shortcuts: [RemoteShortcut] = []
var sendShortcut: (RemoteShortcut) -> Void = { _ in }
var showShortcuts: () -> Void = {}
let switchKeyboard: () -> Void
let dismiss: () -> Void
@@ -23,6 +26,11 @@ struct RemoteSystemKeyboard: View {
didPresent: didPresent,
key: session.sendKeyStroke,
status: session.keyboardInputStatus,
shortcuts: shortcuts,
sendShortcut: sendShortcut,
showShortcuts: showShortcuts,
inputBlocked: { session.showKeyboardCompositionHint() },
inputReady: session.clearKeyboardInputStatus,
switchKeyboard: switchKeyboard,
dismiss: dismiss
)
@@ -43,6 +51,11 @@ private struct RemoteKeyboardHost: UIViewControllerRepresentable {
let didPresent: () -> Void
let key: (UInt) -> Void
let status: String
let shortcuts: [RemoteShortcut]
let sendShortcut: (RemoteShortcut) -> Void
let showShortcuts: () -> Void
let inputBlocked: () -> Void
let inputReady: () -> Void
let switchKeyboard: () -> Void
let dismiss: () -> Void
@@ -63,13 +76,32 @@ private struct RemoteKeyboardHost: UIViewControllerRepresentable {
controller.animatePresentation = animatePresentation
controller.didPresent = didPresent
controller.editor.key = key
let prepareShortcut: () -> Bool = { [weak controller] in
guard let controller else { return false }
if controller.editor.markedTextRange != nil { inputBlocked(); return false }
guard controller.editor.prepareShortcut() else { return false }
inputReady()
return true
}
controller.inputBar.switchKeyboard = { [weak controller] in
controller?.rememberKeyboardHeight()
guard let controller, prepareShortcut() else { return }
controller.rememberKeyboardHeight()
// Let SwiftUI install the replacement panel before teardown hides
// the native keyboard, avoiding an empty frame between the two.
controller?.animateDismissal = false
controller.animateDismissal = false
switchKeyboard()
}
controller.inputBar.showShortcuts = { [weak controller] in
guard let controller, prepareShortcut() else { return }
controller.rememberKeyboardHeight()
controller.animateDismissal = false
showShortcuts()
}
controller.inputBar.sendShortcut = { item in
guard prepareShortcut() else { return }
sendShortcut(item)
}
controller.inputBar.setShortcuts(shortcuts)
controller.inputBar.dismiss = { [weak controller] in
controller?.stop()
dismiss()
@@ -204,6 +236,8 @@ final class RemoteKeyboardController: UIViewController, ObservableObject {
finishKeyboardPresentation()
editor.stop()
inputBar.switchKeyboard = nil
inputBar.showShortcuts = nil
inputBar.sendShortcut = nil
inputBar.dismiss = nil
let dismiss = {
self.editor.resignFirstResponder()
@@ -258,6 +292,41 @@ private final class RemoteKeyboardHostView: UIView {
}
}
/// Both replacement panels use the same edge controls and layout. Only the
/// center content changes, so switching panels cannot move the return label.
struct RemoteKeyboardToolbar<Center: View>: View {
let switchKeyboard: () -> Void
let more: () -> Void
var moreAccessibilityLabel = NSLocalizedString("更多快捷键", comment: "")
let dismiss: () -> Void
var dismissAccessibilityLabel = NSLocalizedString("收起键盘", comment: "")
@ViewBuilder var center: () -> Center
var body: some View {
HStack(spacing: 4) {
Button(NSLocalizedString("系统键盘", comment: ""), action: switchKeyboard)
.font(.system(size: 15, weight: .medium))
.frame(minWidth: 80, minHeight: 44)
.fixedSize(horizontal: true, vertical: false)
.accessibilityLabel(NSLocalizedString("切换到系统键盘", comment: ""))
center()
.frame(maxWidth: .infinity, maxHeight: .infinity)
Button(action: more) {
Image(systemName: "ellipsis")
.font(.system(size: 17))
.frame(width: 44, height: 44)
}
.accessibilityLabel(moreAccessibilityLabel)
RemoteKeyboardDismissButton(accessibilityLabel: dismissAccessibilityLabel, action: dismiss)
.frame(width: 44, height: 44)
}
.padding(.horizontal, 4)
.frame(height: 44)
.foregroundStyle(Color(uiColor: .label))
.buttonStyle(.plain)
}
}
/// Use the same UIKit symbol layout in both the native and SwiftUI toolbars.
/// Matching font sizes alone still gives their symbols different baselines.
struct RemoteKeyboardDismissButton: UIViewRepresentable {
@@ -306,7 +375,11 @@ final class RemoteKeyboardInputBar: UIView {
traits.userInterfaceStyle == .dark ? .secondarySystemBackground : .systemGray4
}
var switchKeyboard: (() -> Void)?
var showShortcuts: (() -> Void)?
var sendShortcut: ((RemoteShortcut) -> Void)?
var dismiss: (() -> Void)?
private let shortcutRow = UIStackView()
private var shortcuts: [RemoteShortcut] = []
private let statusLabel = UILabel()
private let row = UIStackView()
private var status = ""
@@ -331,10 +404,29 @@ final class RemoteKeyboardInputBar: UIView {
dismissButton.accessibilityLabel = NSLocalizedString("收起系统键盘", comment: "")
dismissButton.addTarget(self, action: #selector(hideKeyboard), for: .touchUpInside)
let moreButton = UIButton(type: .system)
moreButton.setImage(UIImage(systemName: "ellipsis"), for: .normal)
moreButton.accessibilityLabel = NSLocalizedString("更多快捷键", comment: "")
moreButton.addTarget(self, action: #selector(openShortcuts), for: .touchUpInside)
let scroll = UIScrollView()
scroll.showsHorizontalScrollIndicator = false
shortcutRow.axis = .horizontal
shortcutRow.spacing = 2
shortcutRow.translatesAutoresizingMaskIntoConstraints = false
scroll.addSubview(shortcutRow)
NSLayoutConstraint.activate([
shortcutRow.leadingAnchor.constraint(equalTo: scroll.contentLayoutGuide.leadingAnchor),
shortcutRow.trailingAnchor.constraint(equalTo: scroll.contentLayoutGuide.trailingAnchor),
shortcutRow.topAnchor.constraint(equalTo: scroll.contentLayoutGuide.topAnchor),
shortcutRow.bottomAnchor.constraint(equalTo: scroll.contentLayoutGuide.bottomAnchor),
shortcutRow.heightAnchor.constraint(equalTo: scroll.frameLayoutGuide.heightAnchor),
moreButton.widthAnchor.constraint(equalToConstant: 44)
])
row.axis = .horizontal
row.alignment = .fill
row.spacing = 4
for item in [computerButton, UIView(), dismissButton] { row.addArrangedSubview(item) }
for item in [computerButton, scroll, moreButton, dismissButton] { row.addArrangedSubview(item) }
row.translatesAutoresizingMaskIntoConstraints = false
addSubview(row)
NSLayoutConstraint.activate([
@@ -368,6 +460,24 @@ final class RemoteKeyboardInputBar: UIView {
setNeedsLayout()
}
func setShortcuts(_ items: [RemoteShortcut]) {
guard items != shortcuts else { return }
shortcuts = items
for child in shortcutRow.arrangedSubviews { shortcutRow.removeArrangedSubview(child); child.removeFromSuperview() }
for item in items {
let button = UIButton(type: .system)
button.setTitle(item.title, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 13)
button.accessibilityLabel = item.title + ", " + item.chord
button.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).isActive = true
button.addAction(UIAction { [weak self] _ in
UIImpactFeedbackGenerator(style: .light).impactOccurred()
self?.sendShortcut?(item)
}, for: .touchUpInside)
shortcutRow.addArrangedSubview(button)
}
}
override func layoutSubviews() {
super.layoutSubviews()
let inset = max(8, safeAreaInsets.left)
@@ -383,6 +493,7 @@ final class RemoteKeyboardInputBar: UIView {
}
@objc private func switchToComputer() { switchKeyboard?() }
@objc private func openShortcuts() { showShortcuts?() }
@objc private func hideKeyboard() { dismiss?() }
}
@@ -523,6 +634,13 @@ final class RemoteTextInputView: UITextField, UITextFieldDelegate {
resetContext()
}
func prepareShortcut() -> Bool {
guard markedTextRange == nil, synchronizeText() else { return false }
// Future IME corrections must not delete text at a caret moved by a shortcut.
resetContext()
return true
}
func stop() {
acceptingInput = false
applyEdit = nil
@@ -1224,11 +1224,34 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
}
func sendKeyStroke(_ keyCode: UInt) {
bridge.sendWindowsKeyCode(keyCode, isDown: true)
bridge.sendWindowsKeyCode(keyCode, isDown: false)
sendKeyChord(keyCode, modifiers: [])
}
func sendKeyChord(_ keyCode: UInt, modifiers: [UInt]) {
guard hasNetworkConsent, isConnected, !privacyScreen.unlockNotice else { return }
bridge.sendKeyChord(keyCode, modifiers: RemoteShortcut.modifierKeys.filter(modifiers.contains).map { NSNumber(value: $0) })
}
func showKeyboardCompositionHint() {
keyboardInputStatus = NSLocalizedString("请先选词或取消组词", comment: "")
}
func clearKeyboardInputStatus() { keyboardInputStatus = "" }
func sendShortcut(_ item: RemoteShortcut) {
guard hasNetworkConsent, isConnected, !privacyScreen.unlockNotice,
item.platform == remotePlatform.rawValue else { return }
keyboardInputStatus = ""
if item.service {
sendSecureAttention()
keyboardInputStatus = controlStatus
} else {
sendKeyChord(item.key, modifiers: item.modifiers)
}
}
func sendKeyState(_ keyCode: UInt, isDown: Bool) {
guard hasNetworkConsent, isConnected else { return }
bridge.sendWindowsKeyCode(keyCode, isDown: isDown)
}
@@ -1504,6 +1527,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
func rtcBridge(_ bridge: CrossDeskRTCBridge, didSendSecureAttention success: Bool) {
guard hasNetworkConsent, isConnected else { return }
controlStatus = success ? NSLocalizedString("已发送 Ctrl+Alt+Del", comment: "") : NSLocalizedString("指令发送失败,请重试", comment: "")
keyboardInputStatus = controlStatus
}
func rtcBridge(_ bridge: CrossDeskRTCBridge,
@@ -31,6 +31,7 @@ private enum RemoteKeyboardMetrics {
private enum RemoteKeyboardMode: String {
case system
case computer
case shortcuts
}
private struct RemoteViewportState: Equatable {
@@ -45,6 +46,10 @@ struct RemoteSessionView: View {
@State private var virtualMouseVisible = false
@StateObject private var systemKeyboardContext = RemoteKeyboardInputContext()
@StateObject private var keyboardGeometry = RemoteKeyboardGeometry()
@StateObject private var shortcutStore = RemoteShortcutStore()
@State private var shortcutManagerVisible = false
@State private var shortcutManagerPlatform = "unknown"
@State private var previousKeyboardMode: RemoteKeyboardMode = .computer
@State private var keyboardInputVisible = false
@State private var keyboardMode: RemoteKeyboardMode = .system
@State private var animateSystemKeyboardPresentation = true
@@ -171,10 +176,13 @@ struct RemoteSessionView: View {
geometry: keyboardGeometry,
animatePresentation: animateSystemKeyboardPresentation,
didPresent: { waitingForSystemKeyboard = false },
shortcuts: shortcutStore.pinned(platform: session.remotePlatform.rawValue),
sendShortcut: session.sendShortcut,
showShortcuts: { previousKeyboardMode = .shortcuts; keyboardMode = .shortcuts },
switchKeyboard: { keyboardMode = .computer },
dismiss: { keyboardInputVisible = false })
}
if keyboardMode == .computer || waitingForSystemKeyboard {
if keyboardMode != .system || waitingForSystemKeyboard {
dockedKeyboard
}
}
@@ -204,6 +212,7 @@ struct RemoteSessionView: View {
if visible {
mouseInput.releaseAll()
keyboardInputVisible = false
shortcutManagerVisible = false
}
}
.onAppear {
@@ -262,6 +271,7 @@ struct RemoteSessionView: View {
if !connected {
mouseInput.releaseAll()
keyboardInputVisible = false
shortcutManagerVisible = false
}
}
.onValueChange(of: session.remoteCursorPositionRevision) { _ in
@@ -278,6 +288,14 @@ struct RemoteSessionView: View {
session.fileImportFailed()
}
}
.fullScreenCover(isPresented: $shortcutManagerVisible, onDismiss: {
if session.isConnected && scenePhase == .active && !session.privacyScreen.unlockNotice {
keyboardMode = .shortcuts
keyboardInputVisible = true
}
}) {
RemoteShortcutManager(store: shortcutStore, platform: shortcutManagerPlatform)
}
}
private var privacyUnlockOverlay: some View {
@@ -505,7 +523,7 @@ struct RemoteSessionView: View {
}
}
x = min(max(x, halfWidth + 8), max(halfWidth + 8, size.width - halfWidth - 8))
if keyboardInputVisible && keyboardMode == .computer {
if keyboardInputVisible && keyboardMode != .system {
let keyboardSize = keyboardPanelSize(in: size)
let center = keyboardCenter(in: size, panelSize: keyboardSize)
let keyboardRect = CGRect(x: center.x - keyboardSize.width / 2, y: center.y - keyboardSize.height / 2, width: keyboardSize.width, height: keyboardSize.height)
@@ -537,16 +555,33 @@ struct RemoteSessionView: View {
.ignoresSafeArea(.keyboard)
}
private func returnToSystemKeyboard() {
previousKeyboardMode = keyboardMode
animateSystemKeyboardPresentation = false
waitingForSystemKeyboard = true
keyboardMode = .system
}
private func remoteKeyboardPanel(bottomInset: CGFloat) -> some View {
RemoteComputerKeyboard(
session: session,
switchKeyboard: {
animateSystemKeyboardPresentation = false
waitingForSystemKeyboard = true
keyboardMode = .system
},
dismiss: { keyboardInputVisible = false }
)
Group {
if keyboardMode == .shortcuts || (waitingForSystemKeyboard && previousKeyboardMode == .shortcuts) {
RemoteShortcutPanel(store: shortcutStore, session: session,
systemKeyboard: returnToSystemKeyboard,
dismiss: { keyboardInputVisible = false },
manage: {
mouseInput.releaseAll()
keyboardInputVisible = false
statusMenuVisible = false
shortcutManagerPlatform = session.remotePlatform.rawValue
shortcutManagerVisible = true
})
} else {
RemoteComputerKeyboard(session: session,
switchKeyboard: returnToSystemKeyboard,
showShortcuts: { previousKeyboardMode = .shortcuts; keyboardMode = .shortcuts },
dismiss: { keyboardInputVisible = false })
}
}
.padding(.bottom, bottomInset)
.background(RemoteKeyboardPalette.background)
}
@@ -837,45 +872,44 @@ private struct RemoteComputerKeyboard: View {
@Environment(\.verticalSizeClass) private var verticalSizeClass
@StateObject private var feedback = RemoteKeyboardFeedback()
let switchKeyboard: () -> Void
let showShortcuts: () -> Void
let dismiss: () -> Void
@State private var activeModifiers: Set<UInt> = []
@State private var page: RemoteComputerKeyboardPage = .input
private static let commonRow: [RemoteComputerKey] = [
private var commonRow: [RemoteComputerKey] { [
.init("Ctrl", 0x11, isModifier: true),
.init("Win", 0x5B, isModifier: true),
.init("Alt", 0x12, isModifier: true),
.init(RemoteShortcut.modifierName(91, platform: session.remotePlatform.rawValue), 0x5B, isModifier: true),
.init(RemoteShortcut.modifierName(18, platform: session.remotePlatform.rawValue), 0x12, isModifier: true),
.init(NSLocalizedString("空格", comment: ""), 0x20, width: 2.6),
.init(NSLocalizedString("回车", comment: ""), 0x0D, width: 1.4, style: .special)
]
] }
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 4) {
Button(NSLocalizedString("系统键盘", comment: "")) {
RemoteKeyboardToolbar(
switchKeyboard: {
feedback.keyPressed()
releaseModifiers()
switchKeyboard()
}
.font(.system(size: 15, weight: .medium))
.fixedSize(horizontal: true, vertical: false)
.frame(minWidth: 80, minHeight: 44)
.accessibilityLabel(NSLocalizedString("切换到系统键盘", comment: ""))
},
more: {
releaseModifiers()
showShortcuts()
},
dismiss: {
feedback.keyPressed()
releaseModifiers()
dismiss()
},
dismissAccessibilityLabel: NSLocalizedString("收起电脑键盘", comment: "")
) {
if verticalSizeClass == .compact {
pageTabs
} else {
Spacer(minLength: 0)
}
RemoteKeyboardDismissButton(accessibilityLabel: NSLocalizedString("收起电脑键盘", comment: "")) {
feedback.keyPressed()
releaseModifiers()
dismiss()
}
.frame(width: RemoteKeyboardDismissButton.sideLength,
height: RemoteKeyboardDismissButton.sideLength)
}
.padding(.horizontal, 4)
.frame(height: 44)
if verticalSizeClass != .compact {
pageTabs
@@ -919,7 +953,7 @@ private struct RemoteComputerKeyboard: View {
}
RemoteComputerKeyboardRow(
keys: Self.commonRow,
keys: commonRow,
activeModifiers: activeModifiers,
action: handleKey
)
@@ -966,14 +1000,13 @@ private struct RemoteComputerKeyboard: View {
switch key.action {
case let .modifier(keyCode):
if activeModifiers.contains(keyCode) {
session.sendKeyState(keyCode, isDown: false)
activeModifiers.remove(keyCode)
} else {
session.sendKeyState(keyCode, isDown: true)
activeModifiers.insert(keyCode)
}
case let .stroke(keyCode):
session.sendKeyStroke(keyCode)
session.sendKeyChord(keyCode, modifiers: Array(activeModifiers))
activeModifiers.removeAll()
case .switchKeyboard:
releaseModifiers()
switchKeyboard()
@@ -984,9 +1017,6 @@ private struct RemoteComputerKeyboard: View {
}
private func releaseModifiers() {
for keyCode in activeModifiers {
session.sendKeyState(keyCode, isDown: false)
}
activeModifiers.removeAll()
}
}
@@ -295,3 +295,59 @@
"配对信息已复制。" = "Kopplungsdaten kopiert.";
"无法复制配对信息,请重试。" = "Kopplungsdaten konnten nicht kopiert werden. Bitte erneut versuchen.";
"安全存储不可用,请解锁设备后重试" = "Der sichere Speicher ist nicht verfügbar. Bitte entsperren Sie das Gerät und versuchen Sie es erneut";
"上一页" = "Vorherige Seite";
"下一页" = "Nächste Seite";
"主按键" = "Haupttaste";
"任务管理器" = "Task-Manager";
"保存" = "Speichern";
"修饰键" = "Modifikatortasten";
"全选" = "Alles auswählen";
"切换应用" = "Apps wechseln";
"切换窗口" = "Fenster wechseln";
"剪切" = "Ausschneiden";
"发送组合" = "Tastenkombination";
"名称" = "Name";
"向上移动" = "Nach oben";
"固定到键盘上方" = "Über der Tastatur anheften";
"复制" = "Kopieren";
"安全选项" = "Sicherheitsoptionen";
"已有相同组合的快捷键" = "Diese Kombination ist bereits vorhanden";
"常用" = "Favoriten";
"应用列表" = "App-Liste";
"强制退出面板" = "Dialog zum sofortigen Beenden";
"快捷键" = "Tastenkürzel";
"我的" = "Eigene";
"按键" = "Tasten";
"撤销" = "Rückgängig";
"收起键盘" = "Tastatur ausblenden";
"新建" = "Neu";
"新建快捷键" = "Neues Tastenkürzel";
"显示桌面" = "Desktop anzeigen";
"更多快捷键" = "Weitere Tastenkürzel";
"最多保存 16 个自定义快捷键" = "Bis zu 16 eigene Tastenkürzel speichern";
"最多固定 8 个快捷键" = "Bis zu 8 Tastenkürzel anheften";
"本地编辑,输入不会发送到远端" = "Lokale Bearbeitung. Eingaben bleiben auf diesem Gerät.";
"添加到常用栏" = "Zur Favoritenleiste hinzufügen";
"移除" = "Entfernen";
"窗口菜单" = "Fenstermenü";
"等待远端平台信息" = "Warten auf Informationen zur Remote-Plattform";
"管理与自定义" = "Verwalten und anpassen";
"管理快捷键" = "Tastenkürzel verwalten";
"粘贴" = "Einfügen";
"系统" = "System";
"终端复制" = "Im Terminal kopieren";
"终端粘贴" = "Im Terminal einfügen";
"编辑" = "Bearbeiten";
"编辑快捷键" = "Tastenkürzel bearbeiten";
"聚焦搜索" = "Spotlight-Suche";
"请使用系统预设中的安全选项" = "Sicherheitsoptionen in den Systemvorgaben verwenden";
"请先选词或取消组词" = "Zuerst die aktuelle Texteingabe bestätigen oder abbrechen";
"请输入有效的快捷键名称" = "Namen mit 1–64 Zeichen eingeben";
"请选择有效的按键" = "Eine gültige Taste auswählen";
"运行窗口" = "Ausführen-Dialog";
"返回" = "Zurück";
"还没有自定义快捷键" = "Noch keine eigenen Tastenkürzel";
"适用远端" = "Remote-Plattform";
"选择桌面方案" = "Desktop-Umgebung auswählen";
"选择桌面方案以加载系统预设" = "Desktop-Umgebung wählen, um Systemvorgaben zu laden";
"键盘上方常用栏" = "Favoriten über der Tastatur";
@@ -295,3 +295,59 @@
"配对信息已复制。" = "Pairing information copied.";
"无法复制配对信息,请重试。" = "Unable to copy pairing information. Please try again.";
"安全存储不可用,请解锁设备后重试" = "Secure storage is unavailable, please unlock the device and try again";
"上一页" = "Previous page";
"下一页" = "Next page";
"主按键" = "Main key";
"任务管理器" = "Task Manager";
"保存" = "Save";
"修饰键" = "Modifiers";
"全选" = "Select all";
"切换应用" = "Switch apps";
"切换窗口" = "Switch windows";
"剪切" = "Cut";
"发送组合" = "Key combination";
"名称" = "Name";
"向上移动" = "Move up";
"固定到键盘上方" = "Pin above keyboard";
"复制" = "Copy";
"安全选项" = "Security options";
"已有相同组合的快捷键" = "This combination already exists";
"常用" = "Favorites";
"应用列表" = "App list";
"强制退出面板" = "Force Quit dialog";
"快捷键" = "Shortcuts";
"我的" = "Mine";
"按键" = "Keys";
"撤销" = "Undo";
"收起键盘" = "Hide keyboard";
"新建" = "New";
"新建快捷键" = "New shortcut";
"显示桌面" = "Show desktop";
"更多快捷键" = "More shortcuts";
"最多保存 16 个自定义快捷键" = "Save up to 16 custom shortcuts";
"最多固定 8 个快捷键" = "Pin up to 8 shortcuts";
"本地编辑,输入不会发送到远端" = "Local editing. Input stays on this device.";
"添加到常用栏" = "Add to favorites bar";
"移除" = "Remove";
"窗口菜单" = "Window menu";
"等待远端平台信息" = "Waiting for remote platform information";
"管理与自定义" = "Manage & customize";
"管理快捷键" = "Manage shortcuts";
"粘贴" = "Paste";
"系统" = "System";
"终端复制" = "Terminal copy";
"终端粘贴" = "Terminal paste";
"编辑" = "Edit";
"编辑快捷键" = "Edit shortcut";
"聚焦搜索" = "Spotlight search";
"请使用系统预设中的安全选项" = "Use Security options in the system presets";
"请先选词或取消组词" = "Confirm or cancel the current composition first";
"请输入有效的快捷键名称" = "Enter a shortcut name (1–64 characters)";
"请选择有效的按键" = "Select a valid key";
"运行窗口" = "Run dialog";
"返回" = "Back";
"还没有自定义快捷键" = "No custom shortcuts yet";
"适用远端" = "Remote platform";
"选择桌面方案" = "Choose desktop environment";
"选择桌面方案以加载系统预设" = "Choose a desktop environment to load system presets";
"键盘上方常用栏" = "Favorites above keyboard";
@@ -295,3 +295,59 @@
"配对信息已复制。" = "Información de emparejamiento copiada.";
"无法复制配对信息,请重试。" = "No se pudo copiar la información de emparejamiento. Inténtelo de nuevo.";
"安全存储不可用,请解锁设备后重试" = "El almacenamiento seguro no está disponible. Desbloquee el dispositivo e inténtelo nuevamente.";
"上一页" = "Página anterior";
"下一页" = "Página siguiente";
"主按键" = "Tecla principal";
"任务管理器" = "Administrador de tareas";
"保存" = "Guardar";
"修饰键" = "Modificadores";
"全选" = "Seleccionar todo";
"切换应用" = "Cambiar de aplicación";
"切换窗口" = "Cambiar de ventana";
"剪切" = "Cortar";
"发送组合" = "Combinación de teclas";
"名称" = "Nombre";
"向上移动" = "Subir";
"固定到键盘上方" = "Fijar sobre el teclado";
"复制" = "Copiar";
"安全选项" = "Opciones de seguridad";
"已有相同组合的快捷键" = "Esta combinación ya existe";
"常用" = "Favoritos";
"应用列表" = "Lista de aplicaciones";
"强制退出面板" = "Diálogo Forzar salida";
"快捷键" = "Atajos";
"我的" = "Mis atajos";
"按键" = "Teclas";
"撤销" = "Deshacer";
"收起键盘" = "Ocultar teclado";
"新建" = "Nuevo";
"新建快捷键" = "Nuevo atajo";
"显示桌面" = "Mostrar escritorio";
"更多快捷键" = "Más atajos";
"最多保存 16 个自定义快捷键" = "Guarda hasta 16 atajos personalizados";
"最多固定 8 个快捷键" = "Fija hasta 8 atajos";
"本地编辑,输入不会发送到远端" = "Edición local. Lo escrito permanece en este dispositivo.";
"添加到常用栏" = "Añadir a la barra de favoritos";
"移除" = "Quitar";
"窗口菜单" = "Menú de ventana";
"等待远端平台信息" = "Esperando información del sistema remoto";
"管理与自定义" = "Gestionar y personalizar";
"管理快捷键" = "Gestionar atajos";
"粘贴" = "Pegar";
"系统" = "Sistema";
"终端复制" = "Copiar en terminal";
"终端粘贴" = "Pegar en terminal";
"编辑" = "Editar";
"编辑快捷键" = "Editar atajo";
"聚焦搜索" = "Búsqueda de Spotlight";
"请使用系统预设中的安全选项" = "Usa Opciones de seguridad en los ajustes del sistema";
"请先选词或取消组词" = "Confirma o cancela primero la composición actual";
"请输入有效的快捷键名称" = "Introduce un nombre de 1 a 64 caracteres";
"请选择有效的按键" = "Selecciona una tecla válida";
"运行窗口" = "Diálogo Ejecutar";
"返回" = "Volver";
"还没有自定义快捷键" = "Aún no hay atajos personalizados";
"适用远端" = "Sistema remoto";
"选择桌面方案" = "Elegir entorno de escritorio";
"选择桌面方案以加载系统预设" = "Elige un entorno para cargar los ajustes del sistema";
"键盘上方常用栏" = "Favoritos sobre el teclado";
@@ -295,3 +295,59 @@
"配对信息已复制。" = "Informations d’association copiées.";
"无法复制配对信息,请重试。" = "Impossible de copier les informations d’association. Réessayez.";
"安全存储不可用,请解锁设备后重试" = "Le stockage sécurisé n'est pas disponible, veuillez déverrouiller l'appareil et réessayer";
"上一页" = "Page précédente";
"下一页" = "Page suivante";
"主按键" = "Touche principale";
"任务管理器" = "Gestionnaire des tâches";
"保存" = "Enregistrer";
"修饰键" = "Touches de modification";
"全选" = "Sélectionner tout";
"切换应用" = "Changer d’application";
"切换窗口" = "Changer de fenêtre";
"剪切" = "Couper";
"发送组合" = "Combinaison à envoyer";
"名称" = "Nom";
"向上移动" = "Monter";
"固定到键盘上方" = "Épingler au-dessus du clavier";
"复制" = "Copier";
"安全选项" = "Options de sécurité";
"已有相同组合的快捷键" = "Cette combinaison existe déjà";
"常用" = "Favoris";
"应用列表" = "Liste des applications";
"强制退出面板" = "Fenêtre Forcer à quitter";
"快捷键" = "Raccourcis";
"我的" = "Mes raccourcis";
"按键" = "Touches";
"撤销" = "Annuler l’action";
"收起键盘" = "Masquer le clavier";
"新建" = "Nouveau";
"新建快捷键" = "Nouveau raccourci";
"显示桌面" = "Afficher le bureau";
"更多快捷键" = "Autres raccourcis";
"最多保存 16 个自定义快捷键" = "Enregistrez jusqu’à 16 raccourcis personnalisés";
"最多固定 8 个快捷键" = "Épinglez jusqu’à 8 raccourcis";
"本地编辑,输入不会发送到远端" = "Édition locale. La saisie reste sur cet appareil.";
"添加到常用栏" = "Ajouter à la barre des favoris";
"移除" = "Retirer";
"窗口菜单" = "Menu de la fenêtre";
"等待远端平台信息" = "En attente des informations du système distant";
"管理与自定义" = "Gérer et personnaliser";
"管理快捷键" = "Gérer les raccourcis";
"粘贴" = "Coller";
"系统" = "Système";
"终端复制" = "Copier dans le terminal";
"终端粘贴" = "Coller dans le terminal";
"编辑" = "Modifier";
"编辑快捷键" = "Modifier le raccourci";
"聚焦搜索" = "Recherche Spotlight";
"请使用系统预设中的安全选项" = "Utilisez les options de sécurité des préréglages système";
"请先选词或取消组词" = "Validez ou annulez d’abord la composition en cours";
"请输入有效的快捷键名称" = "Saisissez un nom de 1 à 64 caractères";
"请选择有效的按键" = "Sélectionnez une touche valide";
"运行窗口" = "Fenêtre Exécuter";
"返回" = "Retour";
"还没有自定义快捷键" = "Aucun raccourci personnalisé";
"适用远端" = "Système distant";
"选择桌面方案" = "Choisir l’environnement de bureau";
"选择桌面方案以加载系统预设" = "Choisissez un environnement pour charger les préréglages système";
"键盘上方常用栏" = "Favoris au-dessus du clavier";
@@ -295,3 +295,59 @@
"配对信息已复制。" = "ペアリング情報をコピーしました。";
"无法复制配对信息,请重试。" = "ペアリング情報をコピーできません。もう一度お試しください。";
"安全存储不可用,请解锁设备后重试" = "安全なストレージが利用できません。デバイスのロックを解除して、もう一度お試しください。";
"上一页" = "前のページ";
"下一页" = "次のページ";
"主按键" = "メインキー";
"任务管理器" = "タスクマネージャー";
"保存" = "保存";
"修饰键" = "修飾キー";
"全选" = "全て選択";
"切换应用" = "アプリを切り替え";
"切换窗口" = "ウインドウを切り替え";
"剪切" = "切り取り";
"发送组合" = "送信するキーの組み合わせ";
"名称" = "名前";
"向上移动" = "上へ移動";
"固定到键盘上方" = "キーボードの上に固定";
"复制" = "コピー";
"安全选项" = "セキュリティオプション";
"已有相同组合的快捷键" = "この組み合わせは登録済みです";
"常用" = "よく使う項目";
"应用列表" = "アプリ一覧";
"强制退出面板" = "強制終了ダイアログ";
"快捷键" = "ショートカット";
"我的" = "マイ項目";
"按键" = "キー";
"撤销" = "取り消す";
"收起键盘" = "キーボードを隠す";
"新建" = "新規";
"新建快捷键" = "ショートカットを作成";
"显示桌面" = "デスクトップを表示";
"更多快捷键" = "その他のショートカット";
"最多保存 16 个自定义快捷键" = "カスタムショートカットは16個まで保存できます";
"最多固定 8 个快捷键" = "固定できるショートカットは8個までです";
"本地编辑,输入不会发送到远端" = "ローカル編集。入力はリモートに送信されません。";
"添加到常用栏" = "お気に入りバーに追加";
"移除" = "取り除く";
"窗口菜单" = "ウインドウメニュー";
"等待远端平台信息" = "リモートのOS情報を待っています";
"管理与自定义" = "管理とカスタマイズ";
"管理快捷键" = "ショートカットを管理";
"粘贴" = "貼り付け";
"系统" = "システム";
"终端复制" = "端末でコピー";
"终端粘贴" = "端末に貼り付け";
"编辑" = "編集";
"编辑快捷键" = "ショートカットを編集";
"聚焦搜索" = "Spotlight検索";
"请使用系统预设中的安全选项" = "システムプリセットのセキュリティオプションを使用してください";
"请先选词或取消组词" = "先に変換を確定またはキャンセルしてください";
"请输入有效的快捷键名称" = "ショートカット名を1〜64文字で入力してください";
"请选择有效的按键" = "有効なキーを選択してください";
"运行窗口" = "実行ダイアログ";
"返回" = "戻る";
"还没有自定义快捷键" = "カスタムショートカットはまだありません";
"适用远端" = "対象リモートOS";
"选择桌面方案" = "デスクトップ環境を選択";
"选择桌面方案以加载系统预设" = "デスクトップ環境を選択してプリセットを読み込みます";
"键盘上方常用栏" = "キーボード上のお気に入り";
@@ -295,3 +295,59 @@
"配对信息已复制。" = "페어링 정보를 복사했습니다.";
"无法复制配对信息,请重试。" = "페어링 정보를 복사할 수 없습니다. 다시 시도하세요.";
"安全存储不可用,请解锁设备后重试" = "보안 저장소를 사용할 수 없습니다. 장치를 잠금 해제한 후 다시 시도해 주세요.";
"上一页" = "이전 페이지";
"下一页" = "다음 페이지";
"主按键" = "기본 키";
"任务管理器" = "작업 관리자";
"保存" = "저장";
"修饰键" = "보조 키";
"全选" = "모두 선택";
"切换应用" = "앱 전환";
"切换窗口" = "창 전환";
"剪切" = "잘라내기";
"发送组合" = "전송할 키 조합";
"名称" = "이름";
"向上移动" = "위로 이동";
"固定到键盘上方" = "키보드 위에 고정";
"复制" = "복사";
"安全选项" = "보안 옵션";
"已有相同组合的快捷键" = "이미 등록된 키 조합입니다";
"常用" = "자주 사용";
"应用列表" = "앱 목록";
"强制退出面板" = "강제 종료 대화상자";
"快捷键" = "단축키";
"我的" = "내 단축키";
"按键" = "키";
"撤销" = "실행 취소";
"收起键盘" = "키보드 숨기기";
"新建" = "새로 만들기";
"新建快捷键" = "새 단축키";
"显示桌面" = "바탕 화면 표시";
"更多快捷键" = "더 많은 단축키";
"最多保存 16 个自定义快捷键" = "사용자 지정 단축키는 최대 16개까지 저장할 수 있습니다";
"最多固定 8 个快捷键" = "단축키는 최대 8개까지 고정할 수 있습니다";
"本地编辑,输入不会发送到远端" = "로컬 편집 중입니다. 입력은 원격 기기로 전송되지 않습니다.";
"添加到常用栏" = "즐겨찾기 모음에 추가";
"移除" = "제거";
"窗口菜单" = "창 메뉴";
"等待远端平台信息" = "원격 플랫폼 정보를 기다리는 중";
"管理与自定义" = "관리 및 사용자 지정";
"管理快捷键" = "단축키 관리";
"粘贴" = "붙여넣기";
"系统" = "시스템";
"终端复制" = "터미널 복사";
"终端粘贴" = "터미널 붙여넣기";
"编辑" = "편집";
"编辑快捷键" = "단축키 편집";
"聚焦搜索" = "Spotlight 검색";
"请使用系统预设中的安全选项" = "시스템 프리셋의 보안 옵션을 사용하세요";
"请先选词或取消组词" = "먼저 조합 중인 문자를 확정하거나 취소하세요";
"请输入有效的快捷键名称" = "단축키 이름을 1~64자로 입력하세요";
"请选择有效的按键" = "유효한 키를 선택하세요";
"运行窗口" = "실행 대화상자";
"返回" = "뒤로";
"还没有自定义快捷键" = "아직 사용자 지정 단축키가 없습니다";
"适用远端" = "대상 원격 플랫폼";
"选择桌面方案" = "데스크톱 환경 선택";
"选择桌面方案以加载系统预设" = "데스크톱 환경을 선택하여 시스템 프리셋을 불러오세요";
"键盘上方常用栏" = "키보드 위 즐겨찾기 모음";
@@ -295,3 +295,59 @@
"配对信息已复制。" = "Informações de pareamento copiadas.";
"无法复制配对信息,请重试。" = "Não foi possível copiar as informações de pareamento. Tente novamente.";
"安全存储不可用,请解锁设备后重试" = "O armazenamento seguro não está disponível. Desbloqueie o dispositivo e tente novamente";
"上一页" = "Página anterior";
"下一页" = "Próxima página";
"主按键" = "Tecla principal";
"任务管理器" = "Gerenciador de Tarefas";
"保存" = "Salvar";
"修饰键" = "Modificadores";
"全选" = "Selecionar tudo";
"切换应用" = "Alternar aplicativos";
"切换窗口" = "Alternar janelas";
"剪切" = "Recortar";
"发送组合" = "Combinação de teclas";
"名称" = "Nome";
"向上移动" = "Mover para cima";
"固定到键盘上方" = "Fixar acima do teclado";
"复制" = "Copiar";
"安全选项" = "Opções de segurança";
"已有相同组合的快捷键" = "Esta combinação já existe";
"常用" = "Favoritos";
"应用列表" = "Lista de aplicativos";
"强制退出面板" = "Janela Forçar Encerrar";
"快捷键" = "Atalhos";
"我的" = "Meus atalhos";
"按键" = "Teclas";
"撤销" = "Desfazer";
"收起键盘" = "Ocultar teclado";
"新建" = "Novo";
"新建快捷键" = "Novo atalho";
"显示桌面" = "Mostrar área de trabalho";
"更多快捷键" = "Mais atalhos";
"最多保存 16 个自定义快捷键" = "Salve até 16 atalhos personalizados";
"最多固定 8 个快捷键" = "Fixe até 8 atalhos";
"本地编辑,输入不会发送到远端" = "Edição local. A entrada fica neste dispositivo.";
"添加到常用栏" = "Adicionar à barra de favoritos";
"移除" = "Remover";
"窗口菜单" = "Menu da janela";
"等待远端平台信息" = "Aguardando informações do sistema remoto";
"管理与自定义" = "Gerenciar e personalizar";
"管理快捷键" = "Gerenciar atalhos";
"粘贴" = "Colar";
"系统" = "Sistema";
"终端复制" = "Copiar no terminal";
"终端粘贴" = "Colar no terminal";
"编辑" = "Editar";
"编辑快捷键" = "Editar atalho";
"聚焦搜索" = "Busca do Spotlight";
"请使用系统预设中的安全选项" = "Use Opções de segurança nas predefinições do sistema";
"请先选词或取消组词" = "Confirme ou cancele primeiro a composição atual";
"请输入有效的快捷键名称" = "Insira um nome de 1 a 64 caracteres";
"请选择有效的按键" = "Selecione uma tecla válida";
"运行窗口" = "Janela Executar";
"返回" = "Voltar";
"还没有自定义快捷键" = "Ainda não há atalhos personalizados";
"适用远端" = "Sistema remoto";
"选择桌面方案" = "Escolher ambiente de trabalho";
"选择桌面方案以加载系统预设" = "Escolha um ambiente para carregar as predefinições do sistema";
"键盘上方常用栏" = "Favoritos acima do teclado";
@@ -295,3 +295,59 @@
"配对信息已复制。" = "Informações de pareamento copiadas.";
"无法复制配对信息,请重试。" = "Não foi possível copiar as informações de pareamento. Tente novamente.";
"安全存储不可用,请解锁设备后重试" = "O armazenamento seguro não está disponível. Desbloqueie o dispositivo e tente novamente";
"上一页" = "Página anterior";
"下一页" = "Próxima página";
"主按键" = "Tecla principal";
"任务管理器" = "Gerenciador de Tarefas";
"保存" = "Salvar";
"修饰键" = "Modificadores";
"全选" = "Selecionar tudo";
"切换应用" = "Alternar aplicativos";
"切换窗口" = "Alternar janelas";
"剪切" = "Recortar";
"发送组合" = "Combinação de teclas";
"名称" = "Nome";
"向上移动" = "Mover para cima";
"固定到键盘上方" = "Fixar acima do teclado";
"复制" = "Copiar";
"安全选项" = "Opções de segurança";
"已有相同组合的快捷键" = "Esta combinação já existe";
"常用" = "Favoritos";
"应用列表" = "Lista de aplicativos";
"强制退出面板" = "Janela Forçar Encerrar";
"快捷键" = "Atalhos";
"我的" = "Meus atalhos";
"按键" = "Teclas";
"撤销" = "Desfazer";
"收起键盘" = "Ocultar teclado";
"新建" = "Novo";
"新建快捷键" = "Novo atalho";
"显示桌面" = "Mostrar área de trabalho";
"更多快捷键" = "Mais atalhos";
"最多保存 16 个自定义快捷键" = "Salve até 16 atalhos personalizados";
"最多固定 8 个快捷键" = "Fixe até 8 atalhos";
"本地编辑,输入不会发送到远端" = "Edição local. A entrada fica neste dispositivo.";
"添加到常用栏" = "Adicionar à barra de favoritos";
"移除" = "Remover";
"窗口菜单" = "Menu da janela";
"等待远端平台信息" = "Aguardando informações do sistema remoto";
"管理与自定义" = "Gerenciar e personalizar";
"管理快捷键" = "Gerenciar atalhos";
"粘贴" = "Colar";
"系统" = "Sistema";
"终端复制" = "Copiar no terminal";
"终端粘贴" = "Colar no terminal";
"编辑" = "Editar";
"编辑快捷键" = "Editar atalho";
"聚焦搜索" = "Busca do Spotlight";
"请使用系统预设中的安全选项" = "Use Opções de segurança nas predefinições do sistema";
"请先选词或取消组词" = "Confirme ou cancele primeiro a composição atual";
"请输入有效的快捷键名称" = "Insira um nome de 1 a 64 caracteres";
"请选择有效的按键" = "Selecione uma tecla válida";
"运行窗口" = "Janela Executar";
"返回" = "Voltar";
"还没有自定义快捷键" = "Ainda não há atalhos personalizados";
"适用远端" = "Sistema remoto";
"选择桌面方案" = "Escolher ambiente de trabalho";
"选择桌面方案以加载系统预设" = "Escolha um ambiente para carregar as predefinições do sistema";
"键盘上方常用栏" = "Favoritos acima do teclado";
@@ -295,3 +295,59 @@
"配对信息已复制。" = "Данные сопряжения скопированы.";
"无法复制配对信息,请重试。" = "Не удалось скопировать данные сопряжения. Повторите попытку.";
"安全存储不可用,请解锁设备后重试" = "Безопасное хранилище недоступно. Разблокируйте устройство и повторите попытку.";
"上一页" = "Предыдущая страница";
"下一页" = "Следующая страница";
"主按键" = "Основная клавиша";
"任务管理器" = "Диспетчер задач";
"保存" = "Сохранить";
"修饰键" = "Клавиши-модификаторы";
"全选" = "Выбрать все";
"切换应用" = "Переключить приложение";
"切换窗口" = "Переключить окно";
"剪切" = "Вырезать";
"发送组合" = "Сочетание клавиш";
"名称" = "Название";
"向上移动" = "Переместить вверх";
"固定到键盘上方" = "Закрепить над клавиатурой";
"复制" = "Копировать";
"安全选项" = "Параметры безопасности";
"已有相同组合的快捷键" = "Такое сочетание уже существует";
"常用" = "Избранное";
"应用列表" = "Список приложений";
"强制退出面板" = "Окно принудительного завершения";
"快捷键" = "Сочетания клавиш";
"我的" = "Мои";
"按键" = "Клавиши";
"撤销" = "Отменить";
"收起键盘" = "Скрыть клавиатуру";
"新建" = "Создать";
"新建快捷键" = "Новое сочетание";
"显示桌面" = "Показать рабочий стол";
"更多快捷键" = "Другие сочетания";
"最多保存 16 个自定义快捷键" = "Можно сохранить до 16 своих сочетаний";
"最多固定 8 个快捷键" = "Можно закрепить до 8 сочетаний";
"本地编辑,输入不会发送到远端" = "Локальное редактирование. Ввод не отправляется на удалённое устройство.";
"添加到常用栏" = "Добавить на панель избранного";
"移除" = "Убрать";
"窗口菜单" = "Меню окна";
"等待远端平台信息" = "Ожидание сведений об удалённой системе";
"管理与自定义" = "Управление и настройка";
"管理快捷键" = "Управление сочетаниями";
"粘贴" = "Вставить";
"系统" = "Система";
"终端复制" = "Копировать в терминале";
"终端粘贴" = "Вставить в терминале";
"编辑" = "Изменить";
"编辑快捷键" = "Изменить сочетание";
"聚焦搜索" = "Поиск Spotlight";
"请使用系统预设中的安全选项" = "Используйте параметры безопасности из системных наборов";
"请先选词或取消组词" = "Сначала подтвердите или отмените текущий ввод";
"请输入有效的快捷键名称" = "Введите название от 1 до 64 символов";
"请选择有效的按键" = "Выберите допустимую клавишу";
"运行窗口" = "Окно запуска";
"返回" = "Назад";
"还没有自定义快捷键" = "Своих сочетаний пока нет";
"适用远端" = "Удалённая система";
"选择桌面方案" = "Выберите окружение рабочего стола";
"选择桌面方案以加载系统预设" = "Выберите окружение, чтобы загрузить системные наборы";
"键盘上方常用栏" = "Избранное над клавиатурой";
@@ -295,3 +295,59 @@
"配对信息已复制。" = "配对信息已复制。";
"无法复制配对信息,请重试。" = "无法复制配对信息,请重试。";
"安全存储不可用,请解锁设备后重试" = "安全存储不可用,请解锁设备后重试";
"上一页" = "上一页";
"下一页" = "下一页";
"主按键" = "主按键";
"任务管理器" = "任务管理器";
"保存" = "保存";
"修饰键" = "修饰键";
"全选" = "全选";
"切换应用" = "切换应用";
"切换窗口" = "切换窗口";
"剪切" = "剪切";
"发送组合" = "发送组合";
"名称" = "名称";
"向上移动" = "向上移动";
"固定到键盘上方" = "固定到键盘上方";
"复制" = "复制";
"安全选项" = "安全选项";
"已有相同组合的快捷键" = "已有相同组合的快捷键";
"常用" = "常用";
"应用列表" = "应用列表";
"强制退出面板" = "强制退出面板";
"快捷键" = "快捷键";
"我的" = "我的";
"按键" = "按键";
"撤销" = "撤销";
"收起键盘" = "收起键盘";
"新建" = "新建";
"新建快捷键" = "新建快捷键";
"显示桌面" = "显示桌面";
"更多快捷键" = "更多快捷键";
"最多保存 16 个自定义快捷键" = "最多保存 16 个自定义快捷键";
"最多固定 8 个快捷键" = "最多固定 8 个快捷键";
"本地编辑,输入不会发送到远端" = "本地编辑,输入不会发送到远端";
"添加到常用栏" = "添加到常用栏";
"移除" = "移除";
"窗口菜单" = "窗口菜单";
"等待远端平台信息" = "等待远端平台信息";
"管理与自定义" = "管理与自定义";
"管理快捷键" = "管理快捷键";
"粘贴" = "粘贴";
"系统" = "系统";
"终端复制" = "终端复制";
"终端粘贴" = "终端粘贴";
"编辑" = "编辑";
"编辑快捷键" = "编辑快捷键";
"聚焦搜索" = "聚焦搜索";
"请使用系统预设中的安全选项" = "请使用系统预设中的安全选项";
"请先选词或取消组词" = "请先选词或取消组词";
"请输入有效的快捷键名称" = "请输入有效的快捷键名称";
"请选择有效的按键" = "请选择有效的按键";
"运行窗口" = "运行窗口";
"返回" = "返回";
"还没有自定义快捷键" = "还没有自定义快捷键";
"适用远端" = "适用远端";
"选择桌面方案" = "选择桌面方案";
"选择桌面方案以加载系统预设" = "选择桌面方案以加载系统预设";
"键盘上方常用栏" = "键盘上方常用栏";
@@ -295,3 +295,59 @@
"配对信息已复制。" = "配對資訊已複製。";
"无法复制配对信息,请重试。" = "無法複製配對資訊,請重試。";
"安全存储不可用,请解锁设备后重试" = "安全儲存不可用,請解鎖裝置後重試";
"上一页" = "上一頁";
"下一页" = "下一頁";
"主按键" = "主按鍵";
"任务管理器" = "工作管理員";
"保存" = "儲存";
"修饰键" = "修飾鍵";
"全选" = "全選";
"切换应用" = "切換應用程式";
"切换窗口" = "切換視窗";
"剪切" = "剪下";
"发送组合" = "傳送組合";
"名称" = "名稱";
"向上移动" = "向上移動";
"固定到键盘上方" = "固定到鍵盤上方";
"复制" = "複製";
"安全选项" = "安全性選項";
"已有相同组合的快捷键" = "已有相同組合的快捷鍵";
"常用" = "常用";
"应用列表" = "應用程式列表";
"强制退出面板" = "強制結束面板";
"快捷键" = "快速鍵";
"我的" = "我的";
"按键" = "按鍵";
"撤销" = "復原";
"收起键盘" = "收起鍵盤";
"新建" = "新增";
"新建快捷键" = "新增快速鍵";
"显示桌面" = "顯示桌面";
"更多快捷键" = "更多快速鍵";
"最多保存 16 个自定义快捷键" = "最多儲存 16 個自訂快速鍵";
"最多固定 8 个快捷键" = "最多固定 8 個快速鍵";
"本地编辑,输入不会发送到远端" = "本機編輯,輸入不會傳送到遠端";
"添加到常用栏" = "加入常用列";
"移除" = "移除";
"窗口菜单" = "視窗選單";
"等待远端平台信息" = "等待遠端平台資訊";
"管理与自定义" = "管理與自訂";
"管理快捷键" = "管理快速鍵";
"粘贴" = "貼上";
"系统" = "系統";
"终端复制" = "終端機複製";
"终端粘贴" = "終端機貼上";
"编辑" = "編輯";
"编辑快捷键" = "編輯快速鍵";
"聚焦搜索" = "Spotlight 搜尋";
"请使用系统预设中的安全选项" = "請使用系統預設中的安全性選項";
"请先选词或取消组词" = "請先選字或取消組字";
"请输入有效的快捷键名称" = "請輸入有效的快速鍵名稱";
"请选择有效的按键" = "請選擇有效的按鍵";
"运行窗口" = "執行視窗";
"返回" = "返回";
"还没有自定义快捷键" = "尚無自訂快速鍵";
"适用远端" = "適用遠端";
"选择桌面方案" = "選擇桌面環境";
"选择桌面方案以加载系统预设" = "選擇桌面環境以載入系統預設";
"键盘上方常用栏" = "鍵盤上方常用列";
+20 -4
View File
@@ -282,8 +282,9 @@ the insertion point, and Return starts a new context. Text edited directly on
the host is not mirrored back; automatic corrections assume the remote caret
still follows the text entered from the phone.
Only the **电脑键盘** switch and keyboard hide button are visible in the 44-point
native input row, anchored to `UIKeyboardLayoutGuide`. Its background continues
The 44-point native input row contains the **电脑键盘** switch, a horizontally
scrollable favorites bar (Escape, Tab, Copy and Paste by default), More and Hide.
It is anchored to `UIKeyboardLayoutGuide`. Its background continues
behind the system keyboard; no input accessory slot is reserved. The IME's
candidate bar remains native. Chinese nine-key Pinyin reserves its own
spelling-choice row above the candidates (for example `m`, `n`, `o`); this may
@@ -310,8 +311,8 @@ iOS does not expose the native keyboard's intensity for custom controls to read.
One feedback generator is reused
while the keyboard is visible and released when it closes. Haptics require
supported hardware; the simulator can only verify the feedback trigger.
Latched modifiers survive page changes and are released when the
keyboard closes or switches back to the system keyboard. The panel reuses the
Modifiers are selected locally and applied to the next key, then cleared. A
whole chord is queued as one RTC task and releases modifiers in reverse order. The panel reuses the
last measured native keyboard height, including the 44-point control row, for
the current window width and orientation. Before landscape has been measured,
the iPhone fallback uses a 161-point compact keybed plus the actual bottom safe
@@ -705,3 +706,18 @@ the applicable [export-compliance determination](https://developer.apple.com/hel
After that determination, supply the appropriate declaration and any required
approval code in the distribution build. The repository deliberately does not
preselect an exemption.
## Remote shortcuts
More replaces the keybed with Common, Keys, System and Mine groups, using the
last measured native keyboard height. Copy/Paste and other semantic presets
follow the remote OS: Ctrl on Windows/Linux and Cmd on macOS. Linux system
presets require an explicit GNOME or KDE Plasma choice. Windows security
options use the existing Ctrl+Alt+Del service command. Unknown remote platforms
expose generic keys until host information arrives.
Manage & Customize opens a separate local screen. Save up to 16 named custom
chords and pin up to 8 shortcuts per remote platform; reorder, unpin, edit and
delete them locally. Custom chords preserve their selected modifiers when the
target platform changes. Shortcut taps flush committed text and reset the IME
correction context; an active composition must be confirmed or cancelled first.