connect most controls to keyboard state objects, call Keyman API to get lexical models array

This commit is contained in:
sgschantz 2022-01-27 16:52:58 +07:00
parent 48b7531b80
commit 49df3b2a05
8 changed files with 322 additions and 71 deletions

View file

@ -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 = "<group>"; };
294D40EB27A110BC00DB37F6 /* FVKeyboardState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FVKeyboardState.swift; sourceTree = "<group>"; };
29694DC127A27EC300EA6C18 /* FVLexicalModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FVLexicalModels.swift; sourceTree = "<group>"; };
2990208D278FF402004F18CF /* KeyboardsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardsScreen.swift; sourceTree = "<group>"; };
2990209027901DD7004F18CF /* KeyboardDetailCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardDetailCell.swift; sourceTree = "<group>"; };
2993F6E52791621D009E1352 /* KeyboardDetailController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardDetailController.swift; sourceTree = "<group>"; };
@ -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 */,

View file

@ -71,6 +71,9 @@
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="XVc-S7-Ce9">
<rect key="frame" x="255" y="11" width="51" height="31.5"/>
<connections>
<action selector="didToggle:" destination="EEw-P3-Ezy" eventType="valueChanged" id="UBL-bH-leY"/>
</connections>
</switch>
</subviews>
<constraints>
@ -89,7 +92,7 @@
<outlet property="switchLabel" destination="VGm-es-KNg" id="QhH-fq-zgE"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="attributeCell" textLabel="hmb-Gy-LXm" detailTextLabel="4gY-nL-15E" style="IBUITableViewCellStyleValue1" id="XYb-N4-6xI" customClass="KeyboardDetailCell" customModule="FirstVoices" customModuleProvider="target">
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="attributeCell" textLabel="hmb-Gy-LXm" detailTextLabel="4gY-nL-15E" style="IBUITableViewCellStyleValue1" id="XYb-N4-6xI">
<rect key="frame" x="0.0" y="109" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="XYb-N4-6xI" id="jbc-l5-Yyr">
@ -113,6 +116,23 @@
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="labelCell" textLabel="Z7V-cv-wnS" style="IBUITableViewCellStyleDefault" id="U3s-AW-OmZ">
<rect key="frame" x="0.0" y="152.5" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="U3s-AW-OmZ" id="nUv-LH-pRe">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Text" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="Z7V-cv-wnS">
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<sections/>
<connections>

View file

@ -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
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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!
}
}

View file

@ -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!
}
}