feat(ios): add per-language autocorrect toggle

Does not actually pass the toggle's setting to KMW yet.

Build-bot: skip build:ios
This commit is contained in:
Joshua Horton 2026-06-23 12:22:43 -05:00
parent e02404b365
commit a7371ea3ec
7 changed files with 138 additions and 50 deletions

View file

@ -33,6 +33,7 @@ public enum Key {
/// Dictionary of prediction/correction toggle settings keyed by language id in UserDefaults
static let userPredictSettings = "UserPredictionEnablementSettings"
static let userCorrectSettings = "UserCorrectionEnablementSettings"
static let userAutocorrectSettings = "UserAutocorrectionEnablementSettings"
// Internal user defaults keys
static let engineVersion = "KeymanEngineVersion"

View file

@ -331,6 +331,17 @@ public extension UserDefaults {
set(prefs, forKey: Key.userCorrectSettings)
}
}
// stores a dictionary of autocorrection-enablement settings keyed to language ids, i.e., [langID: Bool]
var autocorrectionEnablements: [String: Bool]? {
get {
return dictionary(forKey: Key.userAutocorrectSettings) as? [String : Bool]
}
set(prefs) {
set(prefs, forKey: Key.userAutocorrectSettings)
}
}
var portraitKeyboardHeight: Double {
get {
@ -387,4 +398,22 @@ public extension UserDefaults {
prefs?[forLanguageID] = correctSetting
correctionEnablements = prefs
}
func autocorrectSettingForLanguage(languageID: String) -> Bool {
if let dict = autocorrectionEnablements {
return dict[languageID] ?? true
} else {
return true
}
}
func set(autocorrectSetting: Bool, forLanguageID: String) {
var prefs: [String: Bool]?
prefs = autocorrectionEnablements
if prefs == nil {
prefs = [String: Bool]()
}
prefs?[forLanguageID] = autocorrectSetting
autocorrectionEnablements = prefs
}
}

View file

@ -182,7 +182,7 @@ extension KeymanWebViewController {
}
view = nil
}
func languageMenuPosition(_ completion: @escaping (CGRect) -> Void) {
webView!.evaluateJavaScript("langMenuPos();") { result, _ in
guard let result = result as? String, !result.isEmpty else {
@ -422,10 +422,11 @@ extension KeymanWebViewController {
let predict = userDefaults.predictSettingForLanguage(languageID: lexicalModel.languageID)
let correct = userDefaults.correctSettingForLanguage(languageID: lexicalModel.languageID)
let autocorrect = userDefaults.autocorrectSettingForLanguage(languageID: lexicalModel.languageID)
// Pass these off to KMW!
// We do these first so that they're automatically set for the to-be-registered model in advance.
webView!.evaluateJavaScript("enableSuggestions(\(stubString), \(predict), \(correct))")
webView!.evaluateJavaScript("enableSuggestions(\(stubString), \(predict), \(correct), \(autocorrect))")
self.activeModel = predict
} else { // We're registering a model in the background - don't change settings.
webView!.evaluateJavaScript("keyman.addModel(\(stubString));", completionHandler: nil)

View file

@ -18,8 +18,11 @@ class LanguageSettingsViewController: UITableViewController {
private var doPredictionsSwitch: UISwitch?
private var doCorrectionsSwitch: UISwitch?
private var doAutocorrectionsSwitch: UISwitch?
private var doCorrectionsLabel: UILabel?
private var doAutocorrectionsLabel: UILabel?
private var correctionsCell: UITableViewCell?
private var autocorrectionsCell: UITableViewCell?
public init(_ inLanguage: Language) {
language = inLanguage
@ -87,17 +90,8 @@ class LanguageSettingsViewController: UITableViewController {
return switchFrame
}
@objc
func predictionSwitchValueChanged(source: UISwitch) {
let value = source.isOn;
func refreshModelIfNeeded() {
let userDefaults = Storage.active.userDefaults
userDefaults.set(predictSetting: value, forLanguageID: self.language.id)
// Reactively set the corrections switch interactivity state.
self.doCorrectionsSwitch?.isHidden = !value
self.doCorrectionsLabel?.isEnabled = value
self.correctionsCell?.isUserInteractionEnabled = value
if let lm = Manager.shared.preferredLexicalModel(userDefaults, forLanguage: self.language.id) {
if Manager.shared.currentKeyboardID?.languageID == self.language.id {
// re-register the model - that'll enact the settings.
@ -111,23 +105,59 @@ class LanguageSettingsViewController: UITableViewController {
}
}
@objc
func predictionSwitchValueChanged(source: UISwitch) {
let value = source.isOn;
let userDefaults = Storage.active.userDefaults
userDefaults.set(predictSetting: value, forLanguageID: self.language.id)
// Reactively set the corrections switch interactivity state.
self.doCorrectionsSwitch?.isHidden = !value
self.doCorrectionsLabel?.isEnabled = value
let mayCorrect = userDefaults.autocorrectSettingForLanguage(languageID: self.language.id)
self.doAutocorrectionsSwitch?.isHidden = !(value && mayCorrect)
self.doAutocorrectionsLabel?.isEnabled = value && mayCorrect
self.correctionsCell?.isUserInteractionEnabled = value
}
@objc
func correctionSwitchValueChanged(source: UISwitch) {
let value = source.isOn;
let userDefaults = Storage.active.userDefaults
userDefaults.set(correctSetting: value, forLanguageID: self.language.id)
if let lm = Manager.shared.preferredLexicalModel(userDefaults, forLanguage: self.language.id) {
if Manager.shared.currentKeyboardID?.languageID == self.language.id {
// re-register the model - that'll enact the settings.
_ = Manager.shared.registerLexicalModel(lm)
}
// Based on how Manager chooses the model in setKeyboard.
} else if let lm = userDefaults.userLexicalModels?.first(where: { $0.languageID == self.language.id }) {
if Manager.shared.currentKeyboardID?.languageID == self.language.id {
_ = Manager.shared.registerLexicalModel(lm)
}
}
// This may only be triggered if the predict toggle is on,
// so we can rely on just the input value.
self.doAutocorrectionsSwitch?.isHidden = !value
self.doAutocorrectionsLabel?.isEnabled = value
refreshModelIfNeeded()
}
@objc
func autocorrectionSwitchValueChanged(source: UISwitch) {
let value = source.isOn;
let userDefaults = Storage.active.userDefaults
userDefaults.set(autocorrectSetting: value, forLanguageID: self.language.id)
refreshModelIfNeeded()
}
func addSwitchToTableCell(_ toggle: UISwitch, cell: UITableViewCell, isOn: Bool, selector: Selector) {
cell.accessoryType = .none
toggle.translatesAutoresizingMaskIntoConstraints = false
let switchFrame = frameAtRightOfCell(cell: cell.frame, controlSize: toggle.frame.size)
toggle.frame = switchFrame
toggle.isOn = isOn
toggle.addTarget(self, action: selector, for: .valueChanged)
cell.addSubview(toggle)
cell.contentView.isUserInteractionEnabled = false
toggle.rightAnchor.constraint(equalTo: cell.layoutMarginsGuide.rightAnchor).isActive = true
toggle.centerYAnchor.constraint(equalTo: cell.layoutMarginsGuide.centerYAnchor).isActive = true
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
@ -143,40 +173,44 @@ class LanguageSettingsViewController: UITableViewController {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier)
if 1 == indexPath.section {
if 0 == indexPath.row {
cell.accessoryType = .none
doPredictionsSwitch = UISwitch()
doPredictionsSwitch!.translatesAutoresizingMaskIntoConstraints = false
let switchFrame = frameAtRightOfCell(cell: cell.frame, controlSize: doPredictionsSwitch!.frame.size)
doPredictionsSwitch!.frame = switchFrame
doPredictionsSwitch!.isOn = userDefaults.predictSettingForLanguage(languageID: self.language.id)
doPredictionsSwitch!.addTarget(self, action: #selector(self.predictionSwitchValueChanged), for: .valueChanged)
cell.addSubview(doPredictionsSwitch!)
cell.contentView.isUserInteractionEnabled = false
doPredictionsSwitch!.rightAnchor.constraint(equalTo: cell.layoutMarginsGuide.rightAnchor).isActive = true
doPredictionsSwitch!.centerYAnchor.constraint(equalTo: cell.layoutMarginsGuide.centerYAnchor).isActive = true
self.addSwitchToTableCell(
doPredictionsSwitch!,
cell: cell,
isOn: userDefaults.predictSettingForLanguage(languageID: self.language.id),
selector: #selector(self.predictionSwitchValueChanged)
)
} else if 1 == indexPath.row {
correctionsCell = cell
cell.accessoryType = .none
doCorrectionsSwitch = UISwitch()
doCorrectionsSwitch!.translatesAutoresizingMaskIntoConstraints = false
let switchFrame = frameAtRightOfCell(cell: cell.frame, controlSize: doCorrectionsSwitch!.frame.size)
doCorrectionsSwitch!.frame = switchFrame
doCorrectionsSwitch!.isOn = userDefaults.correctSettingForLanguage(languageID: self.language.id)
doCorrectionsSwitch!.addTarget(self, action: #selector(self.correctionSwitchValueChanged), for: .valueChanged)
cell.addSubview(doCorrectionsSwitch!)
cell.contentView.isUserInteractionEnabled = false
doCorrectionsSwitch!.rightAnchor.constraint(equalTo: cell.layoutMarginsGuide.rightAnchor).isActive = true
doCorrectionsSwitch!.centerYAnchor.constraint(equalTo: cell.layoutMarginsGuide.centerYAnchor).isActive = true
self.addSwitchToTableCell(
doCorrectionsSwitch!,
cell: cell,
isOn: userDefaults.correctSettingForLanguage(languageID: self.language.id),
selector: #selector(self.correctionSwitchValueChanged)
)
// Disable interactivity if the prediction toggle is set to 'off'.
doCorrectionsSwitch!.isHidden = !userDefaults.predictSettingForLanguage(languageID: self.language.id)
cell.isUserInteractionEnabled = userDefaults.predictSettingForLanguage(languageID: self.language.id)
} else if 2 == indexPath.row {
autocorrectionsCell = cell
doAutocorrectionsSwitch = UISwitch()
self.addSwitchToTableCell(
doAutocorrectionsSwitch!,
cell: cell,
isOn: userDefaults.autocorrectSettingForLanguage(languageID: self.language.id),
selector: #selector(self.autocorrectionSwitchValueChanged)
)
// Disable interactivity if the prediction or correction toggle is set to 'off'.
let mayPredict = userDefaults.predictSettingForLanguage(languageID: self.language.id)
let mayCorrect = userDefaults.correctSettingForLanguage(languageID: self.language.id)
doAutocorrectionsSwitch!.isHidden = !(mayPredict && mayCorrect)
cell.isUserInteractionEnabled = mayPredict && mayCorrect
} else { // rows 3 and 4
cell.accessoryType = .disclosureIndicator
}
@ -245,6 +279,10 @@ class LanguageSettingsViewController: UITableViewController {
cell.textLabel?.text = NSLocalizedString("menu-langsettings-toggle-correct", bundle: engineBundle, comment: "")
cell.textLabel?.isEnabled = !(doCorrectionsSwitch?.isHidden ?? false)
case 2:
doAutocorrectionsLabel = cell.textLabel
cell.textLabel?.text = NSLocalizedString("menu-langsettings-toggle-autocorrect", bundle: engineBundle, comment: "")
cell.textLabel?.isEnabled = !(doAutocorrectionsSwitch?.isHidden ?? false)
case 3:
cell.textLabel?.text = NSLocalizedString("menu-langsettings-label-lexical-models", bundle: engineBundle, comment: "")
cell.accessoryType = .disclosureIndicator
let modelCt = language.lexicalModels?.count ?? 0
@ -332,8 +370,8 @@ class LanguageSettingsViewController: UITableViewController {
showKeyboardInfoView(kb: (language.keyboards?[safe: indexPath.row])!)
case 1:
switch indexPath.row {
// case 0, 1: the toggles - but a general 'click' not on the toggle itself.
case 2:
// case 0, 1, 2: the toggles - but a general 'click' not on the toggle itself.
case 3:
showLexicalModelsView()
default:
break

View file

@ -154,6 +154,9 @@
/* Label for the toggle that enables corrections that is displayed within a language-specific settings menu */
"menu-langsettings-toggle-correct" = "Enable corrections";
/* Label for the toggle that enables auto-corrections that is displayed within a language-specific settings menu */
"menu-langsettings-toggle-autocorrect" = "Enable autocorrections";
/* Label for the toggle that enables predictions that is displayed within a language-specific settings menu */
"menu-langsettings-toggle-predict" = "Enable predictions";

View file

@ -326,11 +326,12 @@ function toHex(theString) {
return hexString.substr(0, hexString.length-1);
}
function enableSuggestions(model, mayPredict, mayCorrect) {
function enableSuggestions(model, mayPredict, mayCorrect, mayAutocorrect) {
// Set the options first so that KMW's ModelCache can properly handle model enablement states
// the moment we actually register the new model.
keyman.core.languageProcessor.mayPredict = mayPredict;
keyman.core.languageProcessor.mayCorrect = mayCorrect;
keyman.core.languageProcessor.mayAutocorrect = mayAutocorrect;
keyman.addModel(model);
}

View file

@ -166,6 +166,12 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
return !!this.recentTranscriptions.get(transitionId);
}
/**
* When called, this requests the worker to generate predictions for the transcribed
* context transition.
* @param transcription
* @param layerId
*/
public predict(transcription: Transcription, layerId: string): Promise<Suggestion[]> {
if(!this.isActive) {
return null;
@ -373,6 +379,9 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
this.lmEngine.resetContext(context, transcription.token);
}
// The "may correct" setting is enforced here, in the main Web engine - not
// in the worker. As the desired setting may vary language-to-language,
// we'd rather do this than reconfigure the worker constantly.
let alternates = transcription.alternates;
if(!this.mayCorrect || !alternates || alternates.length == 0) {
alternates = [{
@ -386,6 +395,12 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
return promise.then((suggestions: Suggestion[]) => {
if(promise == this.currentPromise) {
if(!this.mayAutoCorrect) {
// We disable the auto-accept flag here, rather than in the worker -
// that way, we don't need to reconfigure the worker when settings
// change.
suggestions.forEach((s) => delete s.autoAccept);
}
const result = new ReadySuggestions(suggestions, transform.id);
this.emit("suggestionsready", result);
this.currentPromise = null;