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
27 changes: 27 additions & 0 deletions Sources/LockIME/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ final class AppState {
private(set) var menuBarIconHidden: Bool = false
@ObservationIgnored static let menuBarIconHiddenKey = "menuBarIconHidden"

/// The `AppRuleMode` a newly added per-app rule starts in — the seed the App
/// Rules pane applies when you add an app, so someone who mostly wants
/// "Switch to" (or "Ignore") never has to re-pick it on every rule. Per-device
/// like `apiEnabled`/`menuBarIconHidden`: its own `UserDefaults` key,
/// deliberately **not** part of `LockConfiguration`, so it never travels
/// through config export/import (importing a shared config must not silently
/// change your authoring default) and carries **no runtime locking effect** —
/// it only templates new rows. Defaults to `.locked`, preserving the original
/// behavior. The source a new rule targets stays the global default; this
/// picks only the mode.
private(set) var newAppRuleMode: AppRuleMode = .locked
@ObservationIgnored private static let newAppRuleModeKey = "newAppRuleMode"

/// The configured global toggle-lock shortcut, mirrored as observable state
/// so the menu-bar header re-renders the moment the user binds or clears it
/// in Settings (a plain `getShortcut` read isn't tracked by `@Observable`).
Expand Down Expand Up @@ -118,6 +131,11 @@ final class AppState {
languagePreference = .load()
apiEnabled = UserDefaults.standard.bool(forKey: Self.apiEnabledKey) // absent ⇒ false (opt-in)
menuBarIconHidden = UserDefaults.standard.bool(forKey: Self.menuBarIconHiddenKey) // absent ⇒ false (icon shown)
// Stored as the raw string; an absent OR unrecognized value maps to
// `.locked` (the historical default), matching the lenient rawValue
// decoding used for `AppRuleMode` on disk.
newAppRuleMode = UserDefaults.standard.string(forKey: Self.newAppRuleModeKey)
.flatMap(AppRuleMode.init(rawValue:)) ?? .locked
ThirdPartyBundleLocalization.apply(language: languagePreference.effectiveLanguage)
}

Expand All @@ -138,6 +156,15 @@ final class AppState {
UserDefaults.standard.set(hidden, forKey: Self.menuBarIconHiddenKey)
}

/// Choose the `AppRuleMode` a newly added app rule starts in. Persisted
/// immediately (its own key) so the choice survives relaunch. It only seeds
/// the next "Add App…"; it never rewrites an existing rule and never
/// re-applies the engine (nothing about the live lock changed).
func setNewAppRuleMode(_ mode: AppRuleMode) {
newAppRuleMode = mode
UserDefaults.standard.set(mode.rawValue, forKey: Self.newAppRuleModeKey)
}

/// GitHub URL of the URL-scheme API reference, in the app's current language
/// (mirrors the `docs/URL-Scheme-API/README.<code>.md` naming). Points at
/// `main`, so it resolves once this work lands there.
Expand Down
156 changes: 156 additions & 0 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": {
"New app rules": {
"localizations": {
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "新增应用规则"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "新增 App 規則"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "新しいアプリのルール"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Nouvelles règles d’app"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Neue App-Regeln"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Nuevas reglas de app"
}
},
"pt": {
"stringUnit": {
"state": "translated",
"value": "Novas regras de app"
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Новые правила приложений"
}
}
}
},
"Default behavior": {
"localizations": {
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "默认行为"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "預設行為"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "デフォルトの動作"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Comportement par défaut"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Standardverhalten"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Comportamiento predeterminado"
}
},
"pt": {
"stringUnit": {
"state": "translated",
"value": "Comportamento padrão"
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Поведение по умолчанию"
}
}
}
},
"The behavior a newly added app rule starts with. You can change any rule afterward.": {
"localizations": {
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "新增应用规则时采用的初始行为。之后你仍可修改任意规则。"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "新增 App 規則時採用的初始行為。之後你仍可修改任何規則。"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "新しく追加したアプリのルールに最初に適用される動作です。追加後も各ルールを変更できます。"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Le comportement appliqué à une règle d’app nouvellement ajoutée. Vous pouvez modifier chaque règle par la suite."
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Das Verhalten, mit dem eine neu hinzugefügte App-Regel beginnt. Sie können jede Regel nachträglich ändern."
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "El comportamiento con el que empieza una regla de app recién añadida. Puedes cambiar cualquier regla después."
}
},
"pt": {
"stringUnit": {
"state": "translated",
"value": "O comportamento com que uma regra de app recém-adicionada começa. Você pode alterar qualquer regra depois."
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Поведение, с которого начинается только что добавленное правило приложения. Любое правило можно изменить позже."
}
}
}
},
"By default, LockIME respects macOS secure input and doesn't change your input source in password fields. Turn this on to keep enforcing the locked source even there.": {
"localizations": {
"de": {
Expand Down
26 changes: 25 additions & 1 deletion Sources/LockIME/UI/Settings/AppRulesSettingsPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ struct AppRulesSettingsPane: View {
get: { state.config.defaultSourceID },
set: { state.setDefaultSource($0) }
)
let newRuleModeBinding = Binding<AppRuleMode>(
get: { state.newAppRuleMode },
set: { state.setNewAppRuleMode($0) }
)

Form {
Section {
Expand All @@ -26,6 +30,19 @@ struct AppRulesSettingsPane: View {
SectionFooter("Used whenever the frontmost app has no rule of its own.")
}

Section {
Picker("Default behavior", selection: newRuleModeBinding) {
Text("Lock to").tag(AppRuleMode.locked)
Text("Switch to").tag(AppRuleMode.switched)
Text("Ignore").tag(AppRuleMode.ignored)
Text("Use default").tag(AppRuleMode.useDefault)
}
} header: {
Text("New app rules")
} footer: {
SectionFooter("The behavior a newly added app rule starts with. You can change any rule afterward.")
}

Section {
if state.config.appRules.isEmpty {
emptyState
Expand Down Expand Up @@ -62,8 +79,15 @@ struct AppRulesSettingsPane: View {
.sheet(isPresented: $isPickingApp) {
AppPickerSheet { app in
withAnimation(DS.Motion.list) {
// Seed the row with the user's chosen default mode (issue #54);
// the source stays the global default, so switching the seeded
// mode to a non-pinning one later just ignores it.
state.upsertRule(
AppRule(bundleID: app.bundleID, mode: .locked, lockedSourceID: state.config.defaultSourceID)
AppRule(
bundleID: app.bundleID,
mode: state.newAppRuleMode,
lockedSourceID: state.config.defaultSourceID
)
)
}
}
Expand Down
Loading