[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

@@ -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.