From 49df3b2a05d4d0674bc4e1caab4c9b9cd81f2d50 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Thu, 27 Jan 2022 16:52:58 +0700 Subject: [PATCH] connect most controls to keyboard state objects, call Keyman API to get lexical models array --- .../ios/FirstVoices.xcodeproj/project.pbxproj | 8 ++ .../FirstVoices/Base.lproj/Main.storyboard | 22 ++++- .../ios/FirstVoices/FVKeyboardPackage.swift | 46 +++++---- .../ios/FirstVoices/FVKeyboardState.swift | 79 +++++++++++++++ .../ios/FirstVoices/FVLexicalModels.swift | 68 +++++++++++++ .../ios/FirstVoices/KeyboardDetailCell.swift | 14 ++- .../KeyboardDetailController.swift | 99 +++++++++++++++---- .../ios/FirstVoices/KeyboardsScreen.swift | 57 ++++++----- 8 files changed, 322 insertions(+), 71 deletions(-) create mode 100644 oem/firstvoices/ios/FirstVoices/FVKeyboardState.swift create mode 100644 oem/firstvoices/ios/FirstVoices/FVLexicalModels.swift diff --git a/oem/firstvoices/ios/FirstVoices.xcodeproj/project.pbxproj b/oem/firstvoices/ios/FirstVoices.xcodeproj/project.pbxproj index 8101a729d4..4fa0704ab3 100644 --- a/oem/firstvoices/ios/FirstVoices.xcodeproj/project.pbxproj +++ b/oem/firstvoices/ios/FirstVoices.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 294D40E9279FE62600DB37F6 /* FVKeyboardPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294D40E8279FE62600DB37F6 /* FVKeyboardPackage.swift */; }; + 294D40EC27A110BC00DB37F6 /* FVKeyboardState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294D40EB27A110BC00DB37F6 /* FVKeyboardState.swift */; }; + 29694DC227A27EC300EA6C18 /* FVLexicalModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29694DC127A27EC300EA6C18 /* FVLexicalModels.swift */; }; 2990208E278FF402004F18CF /* KeyboardsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2990208D278FF402004F18CF /* KeyboardsScreen.swift */; }; 2990209127901DD7004F18CF /* KeyboardDetailCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2990209027901DD7004F18CF /* KeyboardDetailCell.swift */; }; 2993F6E62791621D009E1352 /* KeyboardDetailController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2993F6E52791621D009E1352 /* KeyboardDetailController.swift */; }; @@ -100,6 +102,8 @@ /* Begin PBXFileReference section */ 294D40E8279FE62600DB37F6 /* FVKeyboardPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FVKeyboardPackage.swift; sourceTree = ""; }; + 294D40EB27A110BC00DB37F6 /* FVKeyboardState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FVKeyboardState.swift; sourceTree = ""; }; + 29694DC127A27EC300EA6C18 /* FVLexicalModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FVLexicalModels.swift; sourceTree = ""; }; 2990208D278FF402004F18CF /* KeyboardsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardsScreen.swift; sourceTree = ""; }; 2990209027901DD7004F18CF /* KeyboardDetailCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardDetailCell.swift; sourceTree = ""; }; 2993F6E52791621D009E1352 /* KeyboardDetailController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardDetailController.swift; sourceTree = ""; }; @@ -226,6 +230,8 @@ 37EA1F2E228B7458003E710C /* CheckBox.swift */, 37C183B822932AD2009F9EFD /* FVKeyboardList.swift */, 294D40E8279FE62600DB37F6 /* FVKeyboardPackage.swift */, + 29694DC127A27EC300EA6C18 /* FVLexicalModels.swift */, + 294D40EB27A110BC00DB37F6 /* FVKeyboardState.swift */, 37EA1F4522921BAD003E710C /* KeyboardInfo.swift */, 37EA1F29228B69AF003E710C /* KeyboardsViewController.swift */, 2990208D278FF402004F18CF /* KeyboardsScreen.swift */, @@ -492,7 +498,9 @@ buildActionMask = 2147483647; files = ( 37EA1F26228B5D6E003E710C /* AppDelegate.swift in Sources */, + 29694DC227A27EC300EA6C18 /* FVLexicalModels.swift in Sources */, 37EA1F2A228B69AF003E710C /* KeyboardsViewController.swift in Sources */, + 294D40EC27A110BC00DB37F6 /* FVKeyboardState.swift in Sources */, 2993F6E62791621D009E1352 /* KeyboardDetailController.swift in Sources */, 2990209127901DD7004F18CF /* KeyboardDetailCell.swift in Sources */, 37EA1F4622921BAD003E710C /* KeyboardInfo.swift in Sources */, diff --git a/oem/firstvoices/ios/FirstVoices/Base.lproj/Main.storyboard b/oem/firstvoices/ios/FirstVoices/Base.lproj/Main.storyboard index 552f51f086..b21425d4a2 100644 --- a/oem/firstvoices/ios/FirstVoices/Base.lproj/Main.storyboard +++ b/oem/firstvoices/ios/FirstVoices/Base.lproj/Main.storyboard @@ -71,6 +71,9 @@ + + + @@ -89,7 +92,7 @@ - + @@ -113,6 +116,23 @@ + + + + + + + + + + + diff --git a/oem/firstvoices/ios/FirstVoices/FVKeyboardPackage.swift b/oem/firstvoices/ios/FirstVoices/FVKeyboardPackage.swift index 02c12fd947..7d094fa54d 100644 --- a/oem/firstvoices/ios/FirstVoices/FVKeyboardPackage.swift +++ b/oem/firstvoices/ios/FirstVoices/FVKeyboardPackage.swift @@ -8,16 +8,36 @@ * * Created by Shawn Schantz on 2022-01-25. * - * Description... + * Class that loads data related to the definition of the Keyboard and Package. + * Includes methods to read from the First Voices kmp file and load available keyboards. + * When loading keyboards from the kmp file, information is saved in an FVKeyboardDefinition + * object to associate it with its corresponding language and language tag. + * */ import Foundation import KeymanEngine +class FVKeyboardDefinition { + let name: String + let keyboardId: String + let keyboardVersion: String + let languageTag: String + let languageName: String + + init(name: String, keyboardId: String, keyboardVersion: String, languageTag: String, languageName: String) { + self.name = name + self.keyboardId = keyboardId + self.keyboardVersion = keyboardVersion + self.languageTag = languageTag + self.languageName = languageName + } +} + class FVKeyboardPackage { - static private var _availableKeyboards: [String:FVKeyboardDescriptor] = [:] - static public var availableKeyboards: [String:FVKeyboardDescriptor] { + static private var _availableKeyboards: [String:FVKeyboardDefinition] = [:] + static public var availableKeyboards: [String:FVKeyboardDefinition] { get { return FVKeyboardPackage._availableKeyboards } @@ -55,27 +75,11 @@ class FVKeyboardPackage { // assume one keyboard per package per language as that's currently all we have in practice let keyboard = keyboardArray[0] let keyboardId = keyboard.id - let keyboardDescriptor = FVKeyboardDescriptor(name: keyboard.name, keyboardId: keyboardId, + let keyboardDefinition = FVKeyboardDefinition(name: keyboard.name, keyboardId: keyboardId, keyboardVersion: keyboard.version, languageTag: keyboard.lgCode, languageName: keyboard.languageName) - _availableKeyboards[keyboardId] = keyboardDescriptor + _availableKeyboards[keyboardId] = keyboardDefinition } } } - -class FVKeyboardDescriptor { - let name: String - let keyboardId: String - let keyboardVersion: String - let languageTag: String - let languageName: String - - init(name: String, keyboardId: String, keyboardVersion: String, languageTag: String, languageName: String) { - self.name = name - self.keyboardId = keyboardId - self.keyboardVersion = keyboardVersion - self.languageTag = languageTag - self.languageName = languageName - } -} diff --git a/oem/firstvoices/ios/FirstVoices/FVKeyboardState.swift b/oem/firstvoices/ios/FirstVoices/FVKeyboardState.swift new file mode 100644 index 0000000000..263583c5be --- /dev/null +++ b/oem/firstvoices/ios/FirstVoices/FVKeyboardState.swift @@ -0,0 +1,79 @@ +/* + * FVKeyboardState.swift + * FirstVoices app + * + * License: MIT + * + * Copyright © 2022 FirstVoices. + * + * Created by Shawn Schantz on 2022-01-26. + * + * Class that encapsualtes the state of a keyboard + * + */ + +import Foundation + +class FVKeyboardState { + static private var cache:[String:FVKeyboardState] = [:] + + static private var _savedKeyboards: [String]! + + static private var savedKeyboards: [String] { + get { + if _savedKeyboards == nil { + _savedKeyboards = FVRegionStorage.loadKeyboardListFromUserDefaults() + } + return _savedKeyboards! + } + } + + let definition: FVKeyboardDefinition + var isEnabled: Bool + var suggestCorrections: Bool + var suggestPredictions: Bool + var selectedDictionary: String? + var name: String { + get { + return definition.name } + } + var languageTag: String { + get { + return definition.languageTag } + } + var version: String { + get { + return definition.keyboardVersion } + } + + internal init(definition: FVKeyboardDefinition, isEnabled: Bool, offerCorrections: Bool, offerPredictions: Bool, selectedDictionary: String?) { + self.definition = definition + self.isEnabled = isEnabled + self.suggestCorrections = offerCorrections + self.suggestPredictions = offerPredictions + self.selectedDictionary = selectedDictionary + } + + static func loadKeyboardState(keyboardId: String) -> FVKeyboardState? { + + var state:FVKeyboardState? = nil; + + let keyboardEnabled: Bool = savedKeyboards.contains(keyboardId) + + // load from cache + if let state = cache[keyboardId] { + return state + } else { + // create default keyboard state + if let keyboardDefinition: FVKeyboardDefinition = FVKeyboardPackage.availableKeyboards[keyboardId] { + state = FVKeyboardState(definition: keyboardDefinition, isEnabled: keyboardEnabled, + offerCorrections: false, offerPredictions: false, + selectedDictionary: "") + + // add to cache + cache[keyboardId] = state + } + return state + } + } +} diff --git a/oem/firstvoices/ios/FirstVoices/FVLexicalModels.swift b/oem/firstvoices/ios/FirstVoices/FVLexicalModels.swift new file mode 100644 index 0000000000..96d841d2ef --- /dev/null +++ b/oem/firstvoices/ios/FirstVoices/FVLexicalModels.swift @@ -0,0 +1,68 @@ +/* + * FVLexicalModels.swift + * FirstVoices app + * + * License: MIT + * + * Copyright © 2022 FirstVoices. + * + * Created by Shawn Schantz on 2022-01-27. + * + * Class to call the Keyman API to query about lexical models for a given language. + * + */ + +import UIKit +//import KeymanEngine + +class FVLexicalModels { + static let keymanLexicalModelApiUrl = "https://api.keyman.com/model?q=bcp47:" + + class func getAvailableLexicalModels(languageTag: String) -> [FVLexicalModel] { + var modelArray: [FVLexicalModel] = [] + let keymanApiUrl: URL = URL.init(string: "\(keymanLexicalModelApiUrl)\(languageTag)")! + // UIApplication.shared.openURL(keymanApiUrl) + + do { + let lexicalModelData = try Data(contentsOf: keymanApiUrl, options: NSData.ReadingOptions()) + + do { + // make sure this JSON is in the format we expect + if let jsonArray = try JSONSerialization.jsonObject(with: lexicalModelData, options: []) as? [[String : Any]] { + // try to read array + if !jsonArray.isEmpty { + let jsonModelMap = jsonArray.first + let name = jsonModelMap!["description"] as! String + let packageUrl = jsonModelMap!["packageFilename"] as! String + let version = jsonModelMap!["version"] as! String + let model = FVLexicalModel(name: name, packageUrl: packageUrl, languageTag: languageTag, version: version) + modelArray.append(model) + } + } + } catch let error as NSError { + print("Failed to load: \(error.localizedDescription)") + } + print(lexicalModelData) + } catch { + // TODO: handle errors + print(error) + } + + return modelArray + } +} + +class FVLexicalModel { + let name: String + let packageUrl: String + let languageTag: String + let version: String + + internal init(name: String, packageUrl: String, languageTag: String, version: String) { + self.name = name + self.packageUrl = packageUrl + self.languageTag = languageTag + self.version = version + } + +} diff --git a/oem/firstvoices/ios/FirstVoices/KeyboardDetailCell.swift b/oem/firstvoices/ios/FirstVoices/KeyboardDetailCell.swift index adf1b14aab..f76f99ab91 100644 --- a/oem/firstvoices/ios/FirstVoices/KeyboardDetailCell.swift +++ b/oem/firstvoices/ios/FirstVoices/KeyboardDetailCell.swift @@ -13,12 +13,24 @@ import UIKit +typealias Callback = (Bool) -> Bool + class KeyboardDetailCell: UITableViewCell { + var callback: Callback? = nil + @IBOutlet weak var switchLabel: UILabel! @IBOutlet weak var detailSwitch: UISwitch! - func configure(label: String, enabled: Bool) { + private var title: String = "" + + @IBAction func didToggle(_ sender: Any) { + let result = callback!(detailSwitch.isOn) + } + + func configure(label: String, enabled: Bool, callback: @escaping Callback) { switchLabel.text = label + self.title = label + self.callback = callback detailSwitch.setOn(enabled, animated: false) } } diff --git a/oem/firstvoices/ios/FirstVoices/KeyboardDetailController.swift b/oem/firstvoices/ios/FirstVoices/KeyboardDetailController.swift index 361c203bcd..3e01dabdc0 100644 --- a/oem/firstvoices/ios/FirstVoices/KeyboardDetailController.swift +++ b/oem/firstvoices/ios/FirstVoices/KeyboardDetailController.swift @@ -8,23 +8,29 @@ * * Created by Shawn Schantz on 2022-01-14. * - * Description... + * Class responsible for the display and editing of settings for a single keyboard. + * */ import UIKit class KeyboardDetailController: UITableViewController { - var keyboard: FVKeyboard? = nil - + var keyboardState: FVKeyboardState? = nil + var delegate: RefreshKeyboardCheckmark? = nil + var lexicalModels: [FVLexicalModel] = [] + override func viewDidLoad() { super.viewDidLoad() - if let name = keyboard?.name { + if let name = self.keyboardState?.name { self.navigationItem.title = name + print("Loaded details for keyboard \(name)") + } + + if let languageTag = self.keyboardState?.languageTag { + lexicalModels = FVLexicalModels.getAvailableLexicalModels(languageTag: languageTag) } - - print("Loaded KeyboardDetailController") } /* override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { @@ -32,6 +38,19 @@ class KeyboardDetailController: UITableViewController { headerView.textLabel?.textColor = UIColor.darkGray } */ + + /* + * Used to display the values for the keyboard that was tapped and caused the segue to the detail view. + */ + func configure(delegate: RefreshKeyboardCheckmark, keyboard: FVKeyboardState) { + self.delegate = delegate + self.keyboardState = keyboard + } + + func saveKeyboard() { + + } + override func numberOfSections(in tableView: UITableView) -> Int { return 4 } @@ -63,7 +82,7 @@ class KeyboardDetailController: UITableViewController { numberOfRows = 2 case 2: // TODO: varies - numberOfRows = 1 + numberOfRows = max(1, self.lexicalModels.count) case 3: numberOfRows = 1 default: @@ -74,34 +93,76 @@ class KeyboardDetailController: UITableViewController { // TODO: return empty cell if it cannot be dequeued override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + var tableCell: UITableViewCell? = nil + if indexPath.section == 3 { let versionCell = tableView.dequeueReusableCell(withIdentifier: "attributeCell") versionCell!.textLabel!.text = "Version" - let versionText = FVKeyboardPackage.availableKeyboards[keyboard!.id]?.keyboardVersion + let versionText = self.keyboardState?.version versionCell!.detailTextLabel!.text = versionText - return versionCell! + tableCell = versionCell } else { let switchCell = tableView.dequeueReusableCell(withIdentifier: "switchCell") as! KeyboardDetailCell switch indexPath.section { case 0: - let labelName = "Enable \(keyboard!.name) Keyboard" - switchCell.configure(label: labelName, enabled: false) + let actionCallBack: Callback = { (enable) in + self.keyboardState?.isEnabled = enable + self.delegate?.refreshCheckmark() + return true + } + switchCell.configure(label: (self.keyboardState?.name)!, enabled: self.keyboardState!.isEnabled, + callback: actionCallBack) + tableCell = switchCell case 1: if indexPath.row == 0 { - switchCell.configure(label: "Enable Predictions", enabled: false) + let actionCallBack: Callback = { (enable) in + self.keyboardState?.suggestPredictions = enable + return true + } + switchCell.configure(label: "Suggest Predictions", enabled: self.keyboardState!.suggestPredictions, + callback: actionCallBack) + tableCell = switchCell } else if indexPath.row == 1 { - switchCell.configure(label: "Enable Corrections", enabled: false) - } + let actionCallBack: Callback = { (enable) in + self.keyboardState?.suggestCorrections = enable + return true + } + switchCell.configure(label: "Suggest Corrections", enabled: self.keyboardState!.suggestCorrections, + callback: actionCallBack) + tableCell = switchCell + } case 2: // TODO: varies from zero to n - switchCell.configure(label: "Saanich", enabled: true) + if indexPath.row == 0 { + if self.lexicalModels.isEmpty { + let labelCell = tableView.dequeueReusableCell(withIdentifier: "labelCell") + labelCell?.textLabel!.text = "none" + tableCell = labelCell + } else { + let actionCallBack: Callback = { (enable) in + if enable { + print("Dictionary turned on.") + } else { + print("Dictionary turned off.") + } + return true + } + let availableModelName = self.lexicalModels.first!.name + let selectedModel = (self.keyboardState?.selectedDictionary)! + let modelEnabled = availableModelName.elementsEqual(selectedModel) + switchCell.configure(label: self.lexicalModels.first!.name, enabled: modelEnabled, + callback: actionCallBack) + tableCell = switchCell + } + } + default: - switchCell.configure(label: "Enable Keyboard", enabled: false) + let labelCell = tableView.dequeueReusableCell(withIdentifier: "labelCell") + labelCell?.textLabel!.text = "n/a" + tableCell = labelCell } - - return switchCell } + return tableCell! } - } diff --git a/oem/firstvoices/ios/FirstVoices/KeyboardsScreen.swift b/oem/firstvoices/ios/FirstVoices/KeyboardsScreen.swift index eb624d1a2e..c5c938e349 100644 --- a/oem/firstvoices/ios/FirstVoices/KeyboardsScreen.swift +++ b/oem/firstvoices/ios/FirstVoices/KeyboardsScreen.swift @@ -15,14 +15,24 @@ import UIKit let keymanHelpSite: String = "https://help.keyman.com/keyboard/" -class KeyboardsScreen: UIViewController { +/* + * define protocol so that KeyboardDetailController can send message to update state of checkmark in keyboard list + */ +protocol RefreshKeyboardCheckmark { + func refreshCheckmark() +} + +class KeyboardsScreen: UIViewController, RefreshKeyboardCheckmark { + func refreshCheckmark() { + self.tableView.reloadRows(at: [selectedKeyboardIndex], with: UITableView.RowAnimation.top) + //self.tableView.setNeedsDisplay() + } @IBOutlet weak var tableView: UITableView! var selectedKeyboardIndex: IndexPath = IndexPath(row: 0, section: 0) private var _keyboardList: FVRegionList! - private var keyboardList: FVRegionList { get { if _keyboardList == nil { @@ -32,25 +42,30 @@ class KeyboardsScreen: UIViewController { } } - private var _loadedKeyboards: [String] = [] - override func viewDidLoad() { - _loadedKeyboards = FVRegionStorage.loadKeyboardListFromUserDefaults() - super.viewDidLoad() tableView.delegate = self tableView.dataSource = self } - // In a storyboard-based application, you will often want to do a little preparation before navigation + /* + * Segue to keyboard details screen. + * Before the segue, call the details view controller and pass the state of the keyboard and + * a reference to self to be called back to update the row if the state changes. + */ override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "keyboardDetails" { let detailsController = segue.destination as! KeyboardDetailController let indexPath = self.tableView.indexPathForSelectedRow let keyboards = (self.keyboardList[indexPath!.section]).keyboards let keyboard = keyboards[indexPath!.row] - detailsController.keyboard = keyboard + + if let keyboardState: FVKeyboardState = FVKeyboardState.loadKeyboardState(keyboardId: keyboard.id) { + detailsController.configure(delegate: self, keyboard: keyboardState) + } else { + print("Could not find keyboard \(keyboard.id) in available keyboards list.") + } } } @@ -84,21 +99,9 @@ extension KeyboardsScreen: UITableViewDataSource, UITableViewDelegate { return self.keyboardList[section].name } - /* func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - super self.selectedKeyboardIndex = indexPath - //tableView.deselectRow(at: <#T##IndexPath#>, animated: <#T##Bool#>) - //tableView.deselectRow(at: indexPath, animated: true) - //let keyboards = (self.keyboardList[indexPath.section]).keyboards - - /* - guard let cell = tableView.cellForRow(at: indexPath) else { return } - cell.accessoryType = .checkmark - */ - self.performSegue(withIdentifier: "keyboardDetails", sender: indexPath) - } - */ + } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.keyboardList[section].keyboards.count @@ -108,18 +111,14 @@ extension KeyboardsScreen: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let keyboards = (self.keyboardList[indexPath.section]).keyboards let keyboard = keyboards[indexPath.row] + let keyboardState = FVKeyboardState.loadKeyboardState(keyboardId: keyboard.id) + //let cell = tableView.dequeueReusableCell(withIdentifier: "KeyboardCell") as! KeyboardCell let cell = tableView.dequeueReusableCell(withIdentifier: "KeyboardCell") - cell!.textLabel!.text = keyboard.name + cell!.textLabel!.text = keyboardState?.name - if(_loadedKeyboards.contains(keyboard.id)) { - cell!.imageView?.isHidden = false - print("loaded keyboard = \(keyboard.name), id = \(keyboard.id), legacyId = \(keyboard.legacyId)") - } else { - cell!.imageView?.isHidden = true - } + cell!.imageView?.isHidden = !keyboardState!.isEnabled return cell! } - }