diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Constants.swift b/ios/engine/KMEI/KeymanEngine/Classes/Constants.swift index 01d28628381..2d820d710e5 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Constants.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Constants.swift @@ -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" diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift b/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift index 8991660dae3..4158dc51f2d 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift @@ -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 + } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift index 4bd5375bc92..53e4e327eaa 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift @@ -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) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.swift index 0c98a1cacc3..816a7aea2cb 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.swift @@ -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 diff --git a/ios/engine/KMEI/KeymanEngine/en.lproj/Localizable.strings b/ios/engine/KMEI/KeymanEngine/en.lproj/Localizable.strings index 05a1723ebb0..0e5b70769be 100644 --- a/ios/engine/KMEI/KeymanEngine/en.lproj/Localizable.strings +++ b/ios/engine/KMEI/KeymanEngine/en.lproj/Localizable.strings @@ -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"; diff --git a/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js b/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js index 25af94b5bd2..afb561b1761 100644 --- a/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js +++ b/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js @@ -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); } diff --git a/web/src/engine/src/main/headless/languageProcessor.ts b/web/src/engine/src/main/headless/languageProcessor.ts index b98d4246dfd..c5d08c5ca91 100644 --- a/web/src/engine/src/main/headless/languageProcessor.ts +++ b/web/src/engine/src/main/headless/languageProcessor.ts @@ -166,6 +166,12 @@ export class LanguageProcessor extends EventEmitter { 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 { if(!this.isActive) { return null; @@ -373,6 +379,9 @@ export class LanguageProcessor extends EventEmitter { 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 { 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;