// // LanguageSettingsViewController.swift // KeymanEngine // // Created by Randy Boring on 5/29/19. // Copyright © 2019 SIL International. All rights reserved. // import UIKit import Sentry import os.log private let toolbarButtonTag = 100 class LanguageSettingsViewController: UITableViewController { let language: Language private var settingsArray = [[String: String]]() 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 super.init(nibName: nil, bundle: nil) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } open override func loadView() { super.loadView() tableView?.delegate = self } override func viewDidLoad() { super.viewDidLoad() let titleFormat = NSLocalizedString("menu-langsettings-title", bundle: engineBundle, comment: "") title = String.localizedStringWithFormat(titleFormat, language.name) let message = "viewDidLoad: LanguageSettingsViewController title: \(title ?? "")" os_log("%{public}s", log:KeymanEngineLogger.ui, type: .info, message) if Manager.shared.canAddNewKeyboards { let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(self.addClicked)) navigationItem.rightBarButtonItem = addButton } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) os_log("viewDidAppear: LanguageSettingsViewController", log:KeymanEngineLogger.ui, type: .info) } // MARK: - Table view data source UITableViewDataSource override func numberOfSections(in tableView: UITableView) -> Int { return 2 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if 0 == section { // so far as I know, there is always at least one keyboard or else we won't show let kbct = language.keyboards!.count return kbct } else { return 3 // 4 in future when we can manage a user dictionary } } public func frameAtRightOfCell(cell cellFrame: CGRect, controlSize: CGSize) -> CGRect { let rightOffset = cellFrame.size.width let switchWidth: CGFloat = 20 let switchX = rightOffset - switchWidth let switchHeight = controlSize.height let cellSwitchHeightDiff = cellFrame.size.height - switchHeight let switchY = cellFrame.origin.y + 0.5 * cellSwitchHeightDiff let switchFrame = CGRect(x: switchX, y: switchY, width: switchWidth, height: cellFrame.size.height) return switchFrame } func refreshModelIfNeeded() { let userDefaults = Storage.active.userDefaults 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) } } } func refreshSwitchVisibility() { let userDefaults = Storage.active.userDefaults let mayPredict = userDefaults.predictSettingForLanguage(languageID: self.language.id) self.doCorrectionsSwitch?.isHidden = !mayPredict self.doCorrectionsLabel?.isEnabled = mayPredict self.correctionsCell?.isUserInteractionEnabled = mayPredict let mayCorrect = userDefaults.correctSettingForLanguage(languageID: self.language.id) self.doAutocorrectionsSwitch?.isHidden = !(mayPredict && mayCorrect) self.doAutocorrectionsLabel?.isEnabled = mayPredict && mayCorrect self.autocorrectionsCell?.isUserInteractionEnabled = mayPredict && mayCorrect } @objc func predictionSwitchValueChanged(source: UISwitch) { let value = source.isOn; let userDefaults = Storage.active.userDefaults userDefaults.set(predictSetting: value, forLanguageID: self.language.id) refreshSwitchVisibility() refreshModelIfNeeded() } @objc func correctionSwitchValueChanged(source: UISwitch) { let value = source.isOn; let userDefaults = Storage.active.userDefaults userDefaults.set(correctSetting: value, forLanguageID: self.language.id) refreshSwitchVisibility() 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 { let userDefaults = Storage.active.userDefaults let cellIdentifier = 0 == indexPath.section ? "KeyboardInLanguageSettingsCell" : "LanguageSettingsCell" let reusableCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) if let cell = reusableCell { return cell } let cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier) if 1 == indexPath.section { if 0 == indexPath.row { doPredictionsSwitch = UISwitch() self.addSwitchToTableCell( doPredictionsSwitch!, cell: cell, isOn: userDefaults.predictSettingForLanguage(languageID: self.language.id), selector: #selector(self.predictionSwitchValueChanged) ) } else if 1 == indexPath.row { correctionsCell = cell doCorrectionsSwitch = UISwitch() self.addSwitchToTableCell( doCorrectionsSwitch!, cell: cell, isOn: userDefaults.correctSettingForLanguage(languageID: self.language.id), selector: #selector(self.correctionSwitchValueChanged) ) refreshSwitchVisibility() } 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) ) refreshSwitchVisibility() } else { // rows 3 and 4 cell.accessoryType = .disclosureIndicator } } let selectionColor = UIView() selectionColor.backgroundColor = Colors.selectionPrimary cell.selectedBackgroundView = selectionColor cell.textLabel?.font = cell.textLabel?.font?.withSize(16.0) return cell } // MARK: - UITableViewDelegate // fixed font style. use custom view (UILabel) if you want something different override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { var title: String switch (section) { case 0: // We do NOT use a general-use "keyboards" entry because different languages have different // pluralization rules, and it's a major assumption to assume which is preferred for general use. title = NSLocalizedString("menu-langsettings-section-keyboards", bundle: engineBundle, comment: "") case 1: title = NSLocalizedString("menu-langsettings-section-settings", bundle: engineBundle, comment: "") default: title = "unknown header" } return title } override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { if (section == 0 || section == 1) { return 32.0; } else { return CGFloat.leastNormalMagnitude; } } override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { return CGFloat.leastNonzeroMagnitude } override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { cell.isUserInteractionEnabled = true cell.textLabel?.isEnabled = true cell.detailTextLabel?.isEnabled = true if 0 == indexPath.section { // keyboard list guard let keyboard = language.keyboards?[safe: indexPath.row] else { return } cell.textLabel?.text = keyboard.name cell.detailTextLabel?.text = keyboard.version + " " + keyboard.id cell.accessoryType = .disclosureIndicator } else { // language settings cell.accessoryType = .none switch indexPath.row { case 0: cell.textLabel?.text = NSLocalizedString("menu-langsettings-toggle-predict", bundle: engineBundle, comment: "") case 1: doCorrectionsLabel = cell.textLabel 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 if modelCt != 1 { let formatString = NSLocalizedString("menu-langsettings-lexical-model-count", bundle: engineBundle, comment: "") cell.detailTextLabel?.text = String.localizedStringWithFormat(formatString, modelCt) } else { cell.detailTextLabel?.text = "\(language.lexicalModels![0].name)" } default: cell.textLabel?.text = NSLocalizedString("alert-error-title", bundle: engineBundle, comment: "") } } } override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.cellForRow(at: indexPath)?.isSelected = false performAction(for: indexPath) } override open func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) { performAction(for: indexPath) } override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { if !Manager.shared.canRemoveKeyboards { return false } // No deletion of the settings toggles! if indexPath.section != 0 { return false } // Will we have at least one keyboard left somewhere if this one is deleted? (Even if not same language) if Storage.active.userDefaults.userKeyboards?.count ?? 0 > 1 { return true } return false } override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { if let globalIndex = getKeyboardIndex(kb: (language.keyboards?[safe: indexPath.row])!) { if Manager.shared.removeKeyboard(at: globalIndex) { // For now, a pop-back will be sufficient. navigationController?.popToRootViewController(animated: true) } } } // Do nothing for now. } @objc func addClicked(_ sender: Any) { let keyboardSearchVC = KeyboardSearchViewController(languageCode: self.language.id, keyboardSelectionBlock: KeyboardSearchViewController.defaultDownloadClosure() { result in switch result { case .cancelled: break case .error(let error): if let error = error { // Note: Errors may result from network issues. let errorMessage = "\(String(describing: error))" os_log("%{public}s", log:KeymanEngineLogger.ui, type: .error, errorMessage) } case .success(let package, let fullID): ResourceFileManager.shared.doInstallPrompt(for: package as! KeyboardKeymanPackage, defaultLanguageCode: fullID.languageID, in: self.navigationController!, withAssociators: [.lexicalModels]) } }) navigationController!.pushViewController(keyboardSearchVC, animated: true) } private func performAction(for indexPath: IndexPath) { switch indexPath.section { case 0: showKeyboardInfoView(kb: (language.keyboards?[safe: indexPath.row])!) case 1: switch indexPath.row { // case 0, 1, 2: the toggles - but a general 'click' not on the toggle itself. case 3: showLexicalModelsView() default: break } default: break } } func getKeyboardIndex(kb: Keyboard) -> Int? { let matchingFullID = FullKeyboardID(keyboardID: kb.id, languageID: language.id) let userData = Storage.active.userDefaults // If user defaults for keyboards list does not exist, do nothing. guard let globalUserKeyboards = userData.userKeyboards else { let message = "no keyboards in the global keyboards list!" os_log("%{public}s", log:KeymanEngineLogger.resources, type: .error, message) SentryManager.capture(message) return nil } if let index = globalUserKeyboards.firstIndex(where: { $0.fullID == matchingFullID }) { guard index < globalUserKeyboards.count else { return nil } return index } else { let event = Sentry.Event(level: .debug) event.message = SentryMessage(formatted: "Keyboard index requested for uninstalled keyboard") event.extra = ["id": matchingFullID] SentrySDK.capture(event: event) let errorMessage = "this keyboard \(matchingFullID) not found among user's installed keyboards!" os_log("%{public}s", log:KeymanEngineLogger.resources, type: .error, errorMessage) return nil } } func showKeyboardInfoView(kb: Keyboard) { let matchingFullID = FullKeyboardID(keyboardID: kb.id, languageID: language.id) let userData = Storage.active.userDefaults // If user defaults for keyboards list does not exist, do nothing. guard let globalUserKeyboards = userData.userKeyboards else { let message = "No keyboards in the global keyboards list!" os_log("%{public}s", log: KeymanEngineLogger.settings, type: .error, message) SentryManager.capture("no keyboards in the global keyboards list!") return } if let index = getKeyboardIndex(kb: kb) { guard index < globalUserKeyboards.count else { return } let kbIndex:Int = index let thisKb = globalUserKeyboards[kbIndex] let mayDelete = mayDeleteKeyboard(keyboardIndex: index, keyboardCount: globalUserKeyboards.count) let infoView = ResourceInfoViewController(for: thisKb, mayDelete: mayDelete) navigationController?.pushViewController(infoView, animated: true) } else { let event = Sentry.Event(level: .debug) event.message = SentryMessage(formatted: "Keyboard index requested for uninstalled keyboard") event.extra = ["id": matchingFullID] SentrySDK.capture(event: event) let errorMessage = "this keyboard \(matchingFullID) not found among user's installed keyboards!" os_log("%{public}s", log:KeymanEngineLogger.resources, type: .error, errorMessage) return } } private func mayDeleteKeyboard(keyboardIndex: Int, keyboardCount: Int) -> Bool { if !Manager.shared.canRemoveKeyboards { return false } if !Manager.shared.canRemoveDefaultKeyboard { return keyboardIndex != 0 } if keyboardIndex > 0 { return true } return keyboardCount > 1 } func showLexicalModelsView() { //LanguageLexicalModelPickerViewController? (should show just the models for this language) let lmListView = LexicalModelPickerViewController(self.language) lmListView.language = self.language navigationController?.pushViewController(lmListView, animated: true) } /* // MARK: - Navigation // In a storyboard-based application, you will often want to do a little preparation before navigation override func prepare(for segue: UIStoryboardSegue, sender: Any?) { // Get the new view controller using segue.destination. // Pass the selected object to the new view controller. } */ }