spiegel-keyman/ios/engine/KMEI/KeymanEngine/Classes/SettingsViewController.swift
sgschantz 026edfe388 fix(ios): reworked initialization of keyboardSize
first check UserDefaults for saved values
if no saved values, determine default keyboard heights
write default keyboard heights to UserDefaults
2024-12-07 21:39:00 +07:00

471 lines
19 KiB
Swift

//
// SettingsViewController.swift
// KeymanEngine
//
// Created by Randy Boring on 5/13/19.
// Copyright © 2019 SIL International. All rights reserved.
//
import UIKit
import os.log
open class SettingsViewController: UITableViewController {
private var itemsArray = [[String: String]]()
private var userLanguages: [String: Language] = [:]
private var previousPortraitKeyboardHeight: Double = 0.0
private var previousLandscapeKeyboardHeight: Double = 0.0
override open func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadUserLanguages()
os_log("viewWillAppear: SettingsViewController", log:KeymanEngineLogger.settings, type: .info)
}
override open func viewDidLoad() {
super.viewDidLoad()
title = NSLocalizedString("menu-settings-title", bundle: engineBundle, comment: "")
let doneButton = UIBarButtonItem(title: NSLocalizedString("Done", bundle: engineBundle, comment: ""), style: .plain, target: self,
action: #selector(self.doneClicked))
navigationItem.leftBarButtonItem = doneButton
navigationController?.toolbar?.barTintColor = Colors.statusToolbar
}
@objc func doneClicked(_ sender: Any) {
// This resets KMW so that any new and/or updated resources can be properly loaded.
os_log("SettingsViewController doneClicked", log:KeymanEngineLogger.settings, type: .info)
Manager.shared.dismissKeyboardPicker(self)
let newPortraitHeight = Storage.active.userDefaults.portraitKeyboardHeight
let newLandscapeHeight = Storage.active.userDefaults.landscapeKeyboardHeight
if ((previousPortraitKeyboardHeight != newPortraitHeight) || (previousLandscapeKeyboardHeight != newLandscapeHeight)) {
Manager.shared.keyboardHeightChanged()
}
}
open func launchSettings(launchingVC: UIViewController, sender: Any?) -> Void {
let sb : UIStoryboard = UIStoryboard(name: "Settings", bundle: nil)
if let vc = sb.instantiateInitialViewController() {
launchingVC.present(vc, animated: true, completion: {
os_log("presented settings", log:KeymanEngineLogger.settings, type: .info)
})
}
}
//
// convenience init() {
// self.init(style: UITableViewStyle.grouped)
// }
public init(/*storage: Storage*/) {
// self.storage = storage
super.init(nibName: nil, bundle: nil)
os_log("init settings", log:KeymanEngineLogger.settings, type: .default)
itemsArray = [[String: String]]()
itemsArray.append([
"title": NSLocalizedString("menu-installed-languages-title", bundle: engineBundle, comment: ""),
"subtitle": "0", //count of installed languages as string
"reuseid" : "languages"
])
itemsArray.append([
"title": NSLocalizedString("menu-settings-startup-get-started", bundle: engineBundle, comment: ""),
"subtitle": "",
"reuseid" : "showgetstarted"
])
itemsArray.append([
"title": NSLocalizedString("menu-settings-error-report", bundle: engineBundle, comment: ""),
"subtitle": NSLocalizedString("menu-settings-error-report-description", bundle: engineBundle, comment: ""),
"reuseid": "enablecrashreporting"
])
itemsArray.append([
"title": NSLocalizedString("menu-settings-spacebar-text", bundle: engineBundle, comment: ""),
"subtitle": "",
"reuseid": "spacebartext"
])
itemsArray.append([
"title": NSLocalizedString("menu-settings-adjust-keyboard-height", bundle: engineBundle, comment: ""),
"subtitle": "",
"reuseid": "adjustkeyboardheight"
])
if let _ = URL(string: UIApplication.openSettingsURLString) {
itemsArray.append([
"title": NSLocalizedString("menu-settings-system-keyboard-menu", bundle: engineBundle, comment: ""),
"subtitle": "",
"reuseid": "systemkeyboardsettings"
])
}
itemsArray.append([
"title": NSLocalizedString("menu-settings-install-from-file", bundle: engineBundle, comment: ""),
"subtitle": NSLocalizedString("menu-settings-install-from-file-description", bundle: engineBundle, comment: ""),
"reuseid" : "installfile"
])
#if DEBUG && !NO_SENTRY
itemsArray.append([
"title": "Force a crash",
"subtitle": "Test Sentry error-reporting integration",
"reuseid" : "forcederror"
])
#endif
_ = view
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Table view data source
override open func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itemsArray.count
}
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
}
override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = itemsArray[indexPath.row]["reuseid"]
if let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier!) {
return cell
}
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier)
cell.selectionStyle = .none
switch(cellIdentifier) {
case "languages":
break
case "showgetstarted":
let showAgainSwitch = UISwitch()
showAgainSwitch.translatesAutoresizingMaskIntoConstraints = false
let switchFrame = frameAtRightOfCell(cell: cell.frame, controlSize: showAgainSwitch.frame.size)
showAgainSwitch.frame = switchFrame
showAgainSwitch.isOn = showGetStarted
showAgainSwitch.addTarget(self, action: #selector(self.showGetStartedSwitchValueChanged),
for: .valueChanged)
cell.addSubview(showAgainSwitch)
cell.contentView.isUserInteractionEnabled = false
showAgainSwitch.rightAnchor.constraint(equalTo: cell.layoutMarginsGuide.rightAnchor).isActive = true
showAgainSwitch.centerYAnchor.constraint(equalTo: cell.layoutMarginsGuide.centerYAnchor).isActive = true
case "enablecrashreporting":
let enableReportingSwitch = UISwitch()
enableReportingSwitch.translatesAutoresizingMaskIntoConstraints = false
let switchFrame = frameAtRightOfCell(cell: cell.frame, controlSize: enableReportingSwitch.frame.size)
enableReportingSwitch.frame = switchFrame
enableReportingSwitch.isOn = SentryManager.enabled
enableReportingSwitch.addTarget(self, action: #selector(self.reportingSwitchValueChanged),
for: .valueChanged)
cell.addSubview(enableReportingSwitch)
cell.contentView.isUserInteractionEnabled = false
enableReportingSwitch.rightAnchor.constraint(equalTo: cell.layoutMarginsGuide.rightAnchor).isActive = true
enableReportingSwitch.centerYAnchor.constraint(equalTo: cell.layoutMarginsGuide.centerYAnchor).isActive = true
case "systemkeyboardsettings", "installfile", "forcederror", "spacebartext", "adjustkeyboardheight":
break
default:
let message = "unknown cellIdentifier(\"\(cellIdentifier ?? "EMPTY")\")"
os_log("%{public}s", log:KeymanEngineLogger.settings, type: .error, message)
SentryManager.capture(message)
cell.accessoryType = .none
}
return cell
}
@objc func reportingSwitchValueChanged(_ sender: Any) {
if let toggle = sender as? UISwitch {
// Propagate the effects
SentryManager.enabled = toggle.isOn
}
}
@objc func showGetStartedSwitchValueChanged(_ sender: Any) {
let userData = Storage.active.userDefaults
if let toggle = sender as? UISwitch {
userData.set(toggle.isOn, forKey: Key.optShouldShowGetStarted)
userData.synchronize()
}
}
private var showGetStarted: Bool {
let userData = Storage.active.userDefaults
return userData.bool(forKey: Key.optShouldShowGetStarted)
}
override open func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.accessoryType = .none
// Remember, UITableViewCells may be reused, so we should always reset relevant properties.
cell.textLabel?.text = itemsArray[indexPath.row]["title"]
cell.detailTextLabel?.text = itemsArray[indexPath.row]["subtitle"]
cell.textLabel?.isEnabled = true
cell.tag = indexPath.row
cell.isUserInteractionEnabled = true
let cellIdentifier = itemsArray[indexPath.row]["reuseid"]
switch (cellIdentifier) {
case "languages", "installfile", "systemkeyboardsettings", "forcederror":
cell.accessoryType = .disclosureIndicator
cell.detailTextLabel?.isEnabled = true
case "enablecrashreporting":
cell.detailTextLabel?.isEnabled = true
break
case "spacebartext":
cell.accessoryType = .disclosureIndicator
cell.detailTextLabel?.text = NSLocalizedString("menu-settings-spacebar-hint-"+Manager.shared.spacebarText.rawValue, bundle: engineBundle, comment: "")
cell.detailTextLabel?.isEnabled = true
break
case "adjustkeyboardheight":
cell.accessoryType = .disclosureIndicator
cell.detailTextLabel?.isEnabled = false
break
case "showbanner", "showgetstarted":
cell.detailTextLabel?.isEnabled = false
default:
let message = "unknown cellIdentifier(\"\(cellIdentifier ?? "EMPTY")\")"
os_log("%{public}s", log:KeymanEngineLogger.settings, type: .error, message)
SentryManager.capture(message)
}
}
// In a xib-based application, navigation from a table can be handled in -tableView:didSelectRowAtIndexPath:
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)
}
private func performAction(for indexPath: IndexPath) {
switch indexPath.section {
case 0:
let cellIdentifier = itemsArray[indexPath.row]["reuseid"]
switch cellIdentifier {
case "languages":
showLanguages()
case "systemkeyboardsettings":
guard let appSettings = URL(string: UIApplication.openSettingsURLString) else {
// It is an error if the option is displayed but unusable. That's bad UI.
let message = "Could not launch keyboard settings menu"
os_log("%{public}s", log:KeymanEngineLogger.settings, type: .error, message)
SentryManager.capture(message)
return
}
UniversalLinks.externalLinkLauncher?(appSettings)
case "installfile":
if let block = Manager.shared.fileBrowserLauncher {
block(navigationController!)
} else {
let message = "Listener for framework signal to launch file browser is missing"
os_log("%{public}s", log:KeymanEngineLogger.settings, type: .error, message)
SentryManager.capture(message)
}
case "forcederror":
SentryManager.forceError()
case "spacebartext":
showSpacebarText()
case "adjustkeyboardheight":
showAdjustKeyboardHeight()
default:
break
}
default:
break
}
}
/*
// Override to support conditional editing of the table view.
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
*/
/*
// Override to support editing the table view.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// Delete the row from the data source
tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
*/
/*
// Override to support rearranging the table view.
override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) {
}
*/
/*
// Override to support conditional rearranging of the table view.
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the item to be re-orderable.
return true
}
*/
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override open func prepare(for segue: UIStoryboardSegue, sender: Any?) {
os_log("prepare for segue", log:KeymanEngineLogger.settings, type: .info)
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
// MARK: - language access -
/** returns an array of LexicalModel created from an array of InstallableLexicalModel
* @param installedList: The InstallableLexicalModel are probably already installed
* The returned LexicalModels are not complete (usually we go the other way round)
* but sufficient for future API calls
*/
public static func installed2API(_ installedList: [InstallableLexicalModel]) -> [LexicalModel] {
var returnList = [LexicalModel]()
for ilm in installedList {
returnList.append(LexicalModel(id: ilm.id, name: ilm.name, license: "", version: ilm.version, languages: [], authorName: "", fileSize: 0, filename: "no filename", sourcePath: nil, authorEmail: nil, description: nil, packageFileSize: 0, packageFilename: "", packageIncludes: nil, isDefault: false, lastModified: nil, minKeymanVersion: nil))
}
return returnList
}
// MARK: - language access -
private func loadUserLanguages() {
//iterate the list of installed languages and save their names
// Get keyboards list if it exists in user defaults, otherwise create a new one
let userDefaults = Storage.active.userDefaults
let userKeyboards = userDefaults.userKeyboards ?? []
var keyboardLanguages = [String: Language]()
for k in userKeyboards {
let l = k.languageID
var kbds: [Keyboard]
if let existingLanguage = keyboardLanguages[l] {
kbds = existingLanguage.keyboards ?? []
kbds.append(Keyboard(name: k.name, id: k.id, filename: "no filename", isDefault: nil, isRTL: k.isRTL, lastModified: Date(), fileSize: 0, version: k.version, languages: nil, font: nil, oskFont: nil))
} else {
kbds = [Keyboard(name: k.name, id: k.id, filename: "no filename", isDefault: nil, isRTL: k.isRTL, lastModified: Date(), fileSize: 0, version: k.version, languages: nil, font: nil, oskFont: nil)]
}
let userDefaults : UserDefaults = Storage.active.userDefaults
let lmListInstalled: [InstallableLexicalModel] = userDefaults.userLexicalModelsForLanguage(languageID: l) ?? []
let lmList = SettingsViewController.installed2API(lmListInstalled)
keyboardLanguages[l] = Language(name: k.languageName, id: k.languageID, keyboards: kbds, lexicalModels: lmList, font: nil, oskFont: nil)
}
// there shouldn't be any lexical models for languages that don't have a keyboard installed
// but check
let userLexicalModels = userDefaults.userLexicalModels ?? []
for lm in userLexicalModels {
let l = lm.languageID
if let langName = keyboardLanguages[l]?.name {
let message = "keyboard language \(l) \(langName) has lexical model"
os_log("%{public}s", log:KeymanEngineLogger.settings, type: .info, message)
} else {
// Legacy behavior: we automatically install all MTNT language codes, even without
// a matching keyboard for the more specific variant(s).
let message = "lexical model language \(l) has no keyboard installed!"
os_log("%{public}s", log:KeymanEngineLogger.settings, type: .info, message)
SentryManager.breadcrumb(message)
}
}
userLanguages = keyboardLanguages
let formatString = NSLocalizedString("settings-languages-installed-count", bundle: engineBundle, comment: "")
itemsArray[0]["subtitle"] = String.localizedStringWithFormat(formatString, userLanguages.count)
tableView.reloadData()
}
public func setIsDoneButtonEnabled(_ nc: UINavigationController, _ value: Bool) {
let doneOrCancel = value
if doneOrCancel {
let doneButton = UIBarButtonItem(title: NSLocalizedString("command-done", bundle: engineBundle, comment: ""), style: .plain, target: self,
action: nil /* #selector(self.doneClicked) */ )
nc.navigationItem.leftBarButtonItem = doneButton
} else {
let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self,
action: nil /* #selector(self.cancelClicked) */ )
nc.navigationItem.leftBarButtonItem = cancelButton
}
}
func showLanguages() {
let vc = InstalledLanguagesViewController(userLanguages)
if let nc = navigationController {
nc.pushViewController(vc, animated: true)
setIsDoneButtonEnabled(nc, true)
} else {
let message = ("No navigation controller for showing languages???")
os_log("%{public}s", log:KeymanEngineLogger.settings, type: .error, message)
SentryManager.capture(message)
}
}
func showSpacebarText() {
let vc = SpacebarTextViewController()
if let nc = navigationController {
nc.pushViewController(vc, animated: true)
setIsDoneButtonEnabled(nc, true)
} else {
let message = ("No navigation controller for showing spacebarText options")
os_log("%{public}s", log:KeymanEngineLogger.settings, type: .error, message)
SentryManager.capture(message)
}
}
func showAdjustKeyboardHeight() {
let vc = KeyboardHeightViewController()
if let nc = navigationController {
self.previousPortraitKeyboardHeight = Storage.active.userDefaults.portraitKeyboardHeight
self.previousLandscapeKeyboardHeight = Storage.active.userDefaults.landscapeKeyboardHeight
let message = "selected Adjust Keyboard Height, previousPortraitKeyboardHeight: \(previousPortraitKeyboardHeight), previousLandscapeKeyboardHeight: \(previousLandscapeKeyboardHeight)"
os_log("%{public}s", log:KeymanEngineLogger.settings, type: .default, message)
nc.pushViewController(vc, animated: true)
setIsDoneButtonEnabled(nc, true)
} else {
let message = ("No navigation controller for showing keyboard height view")
os_log("%{public}s", log:KeymanEngineLogger.settings, type: .error, message)
SentryManager.capture(message)
}
}
}