Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ Either way, the app keeps itself up to date via Sparkle.

- **Instant re-lock** — switches the active input source back the moment you (or
another app) change it, globally or per-app.
- **Lock or switch** — per-app and per-URL rules can *lock* an input source
(re-applied whenever it drifts) or just *switch* to it once when you focus the
app or page, then step out of the way and let you change it freely.
- **Menu-bar control** — activate/deactivate, switch the locked input source,
view the current source, and track the activation count from the menu bar.
- **Keyboard shortcuts** — configurable global shortcuts to toggle locking and
Expand Down
4 changes: 3 additions & 1 deletion Sources/LockIME/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,9 @@ final class AppState {
guard let next = SourceCycler.step(
from: reference, in: availableSources.map(\.id), direction: direction
) else { return }
rule.mode = .locked
// Cycling pins a source; keep a `.switched` rule a switch (don't demote
// it to a continuous lock), and turn a non-pinning rule into a lock.
if !rule.mode.pinsSource { rule.mode = .locked }
rule.lockedSourceID = next
upsertRule(rule)
}
Expand Down
174 changes: 165 additions & 9 deletions Sources/LockIME/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,162 @@
"sourceLanguage": "en",
"version": "1.0",
"strings": {
"Switch to": {
"localizations": {
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "切换为"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "切換為"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "切り替え先"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Basculer vers"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Wechseln zu"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Cambiar a"
}
},
"pt": {
"stringUnit": {
"state": "translated",
"value": "Alternar para"
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Переключить на"
}
}
}
},
"Lock to %@": {
"localizations": {
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "锁定为 %@"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "鎖定為 %@"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%@ にロック"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Verrouiller sur %@"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Sperren auf %@"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Bloquear en %@"
}
},
"pt": {
"stringUnit": {
"state": "translated",
"value": "Bloquear em %@"
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Блокировать на %@"
}
}
}
},
"Switch to %@": {
"localizations": {
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "切换为 %@"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "切換為 %@"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%@ に切り替え"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Basculer vers %@"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Wechseln zu %@"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Cambiar a %@"
}
},
"pt": {
"stringUnit": {
"state": "translated",
"value": "Alternar para %@"
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Переключить на %@"
}
}
}
},
"Lock to previous input source": {
"localizations": {
"zh-Hans": {
Expand Down Expand Up @@ -5254,54 +5410,54 @@
}
}
},
"Pin a specific app to its own input source, ignore it, or fall back to the global default.": {
"Lock an app to its own input source, switch to one on activation, ignore it, or fall back to the global default.": {
"localizations": {
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "将某个应用固定到它自己的输入法、忽略它,或回退到全局默认。"
"value": "将应用锁定到它自己的输入法、在激活时切换到某个输入法、忽略它,或回退到全局默认。"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "將特定 App 固定到自己的輸入法、忽略它,或回退到全域預設。"
"value": " App 鎖定到自己的輸入法、在啟用時切換到某個輸入法、忽略它,或回退到全域預設。"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "特定のアプリを独自の入力ソースに固定するか、無視するか、グローバルの既定値を使用します。"
"value": "特定のアプリを独自の入力ソースにロックする、アクティブ化時に切り替える、無視する、またはグローバルの既定値を使用します。"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Épinglez une app à sa propre source de saisie, ignorez-la ou utilisez la valeur par défaut globale."
"value": "Verrouillez une app sur sa propre source de saisie, basculez vers une à l'activation, ignorez-la ou utilisez la valeur par défaut globale."
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Eine bestimmte App auf eine eigene Eingabequelle festlegen, ignorieren oder auf den globalen Standard zurückfallen."
"value": "Eine App auf eine eigene Eingabequelle sperren, beim Aktivieren zu einer wechseln, ignorieren oder auf den globalen Standard zurückfallen."
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Fija una app a su propia fuente de entrada, ignórala o usa el valor predeterminado global."
"value": "Bloquea una app en su propia fuente de entrada, cambia a una al activarse, ignórala o usa el valor predeterminado global."
}
},
"pt": {
"stringUnit": {
"state": "translated",
"value": "Fixe um app à sua própria fonte de entrada, ignore-o ou use o padrão global."
"value": "Bloqueie um app na sua própria fonte de entrada, alterne para uma ao ativar, ignore-o ou use o padrão global."
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Закрепите за приложением свой источник ввода, игнорируйте его или используйте глобальное значение по умолчанию."
"value": "Заблокируйте приложение на его источнике ввода, переключайтесь на него при активации, игнорируйте его или используйте глобальное значение по умолчанию."
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions Sources/LockIME/UI/Settings/AppRulesSettingsPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ struct AppRulesSettingsPane: View {
} header: {
Text("Per-app rules")
} footer: {
SectionFooter("Pin a specific app to its own input source, ignore it, or fall back to the global default.")
SectionFooter("Lock an app to its own input source, switch to one on activation, ignore it, or fall back to the global default.")
}
}
.formStyle(.grouped)
Expand Down Expand Up @@ -100,13 +100,14 @@ private struct AppRuleRow: View {

Picker("", selection: modeBinding) {
Text("Lock to").tag(AppRuleMode.locked)
Text("Switch to").tag(AppRuleMode.switched)
Text("Ignore").tag(AppRuleMode.ignored)
Text("Use default").tag(AppRuleMode.useDefault)
}
.labelsHidden()
.fixedSize()

if rule.mode == .locked {
if rule.mode.pinsSource {
Picker("", selection: sourceBinding) {
Text("Default").tag(InputSourceID?.none)
ForEach(state.availableSources) { source in
Expand Down
49 changes: 36 additions & 13 deletions Sources/LockIME/UI/Settings/ImportReviewSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -420,29 +420,52 @@ struct ImportReviewSheet: View {
}
}

/// The file-side binding as composable `Text`: a source name (a verbatim
/// proper noun) or the localized app-rule mode word when no source is pinned.
/// Returning `Text` keeps it usable inside `HStack`s and recolorable, while
/// still resolving catalog keys against the injected `\.locale`.
/// The file-side binding as composable `Text`. A source-pinning rule reads as
/// "Lock to %@" / "Switch to %@" so lock and switch are visibly parallel (and
/// a same-source lock-vs-switch conflict is distinguishable); a non-pinning
/// app mode reads as its mode word; the global default (always a lock, no
/// ambiguity) and a sourceless binding read as the bare name / "Default".
/// Returning `Text` keeps it recolorable inside `HStack`s while resolving
/// catalog keys against the injected `\.locale`.
private func fileBindingText(_ item: ImportItem) -> Text {
if case .app = item.subject, let mode = item.fileMode, mode != .locked {
return Text(modeKey(mode))
}
if let source = item.fileSource { return Text(verbatim: model.displayName(for: source)) }
return Text("Default")
bindingText(subject: item.subject, mode: item.fileMode, action: item.fileAction, source: item.fileSource)
}

private func localBindingText(_ item: ImportItem) -> Text {
if case .app = item.subject, let mode = item.localMode, mode != .locked {
return Text(modeKey(mode))
bindingText(subject: item.subject, mode: item.localMode, action: item.localAction, source: item.localSource)
}

private func bindingText(
subject: ImportItem.Subject,
mode: AppRuleMode?,
action: RuleAction?,
source: InputSourceID?
) -> Text {
switch subject {
case .globalDefault:
if let source { return Text(verbatim: model.displayName(for: source)) }
return Text("Default")
case .app:
if let mode, !mode.pinsSource { return Text(modeKey(mode)) } // ignore / use default
guard let source else { return Text("Default") }
return pinnedBindingText(isSwitch: mode == .switched, source: source)
case .url:
guard let source else { return Text("Default") }
return pinnedBindingText(isSwitch: action == .switchOnce, source: source)
}
if let source = item.localSource { return Text(verbatim: model.displayName(for: source)) }
return Text("Default")
}

/// A source-pinning binding: the localized "Lock to %@" / "Switch to %@"
/// phrase with the source name interpolated (a verbatim proper noun).
private func pinnedBindingText(isSwitch: Bool, source: InputSourceID) -> Text {
let name = model.displayName(for: source)
return Text(isSwitch ? "Switch to \(name)" : "Lock to \(name)")
}

private func modeKey(_ mode: AppRuleMode) -> LocalizedStringKey {
switch mode {
case .locked: "Lock to"
case .switched: "Switch to"
case .ignored: "Ignore"
case .useDefault: "Use default"
}
Expand Down
Loading
Loading