spiegel-keyman/ios/engine/KMEI/KeymanEngine/Classes/SettingsViewController.swift

455 lines
18 KiB
Swift

//
// SettingsViewController.swift
// KeymanEngine
//
// Created by Randy Boring on 5/13/19.
// Copyright © 2019 SIL International. All rights reserved.
//
import UIKit
open class SettingsViewController: UITableViewController {
private var itemsArray = [[String: String]]()
private var userLanguages: [String: Language] = [:]
override open func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadUserLanguages()
log.info("willAppear: SettingsViewController")
}
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) {
// While the called method might should be renamed, it does the job well enough.
// This resets KMW so that any new and/or updated resources can be properly loaded.
Manager.shared.dismissKeyboardPicker(self)
}
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: {
log.info("presented settings")
})
}
}
//
// convenience init() {
// self.init(style: UITableViewStyle.grouped)
// }
public init(/*storage: Storage*/) {
// self.storage = storage
super.init(nibName: nil, bundle: nil)
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-show-banner", bundle: engineBundle, comment: ""),
"subtitle": "",
"reuseid" : "showbanner"
])
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"
])
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 "showbanner":
let showBannerSwitch = UISwitch()
showBannerSwitch.translatesAutoresizingMaskIntoConstraints = false
let switchFrame = frameAtRightOfCell(cell: cell.frame, controlSize: showBannerSwitch.frame.size)
showBannerSwitch.frame = switchFrame
showBannerSwitch.isOn = showBanner
showBannerSwitch.addTarget(self, action: #selector(self.bannerSwitchValueChanged),
for: .valueChanged)
cell.addSubview(showBannerSwitch)
cell.contentView.isUserInteractionEnabled = false
showBannerSwitch.rightAnchor.constraint(equalTo: cell.layoutMarginsGuide.rightAnchor).isActive = true
showBannerSwitch.centerYAnchor.constraint(equalTo: cell.layoutMarginsGuide.centerYAnchor).isActive = true
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":
break
default:
SentryManager.captureAndLog("unknown cellIdentifier(\"\(cellIdentifier ?? "EMPTY")\")")
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 bannerSwitchValueChanged(_ sender: Any) {
let userData = Storage.active.userDefaults
if let toggle = sender as? UISwitch {
// actually this should call into KMW, which controls the banner
userData.set(toggle.isOn, forKey: Key.optShouldShowBanner)
userData.synchronize()
}
// Necessary for the keyboard to visually update to match
// the new setting.
Manager.shared.inputViewController.setShouldReload()
}
@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 showBanner: Bool {
let userData = Storage.active.userDefaults
return userData.bool(forKey: Key.optShouldShowBanner)
}
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 "showbanner", "showgetstarted":
cell.detailTextLabel?.isEnabled = false
default:
SentryManager.captureAndLog("unknown cellIdentifier(\"\(cellIdentifier ?? "EMPTY")\")")
}
}
// 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.
SentryManager.captureAndLog("Could not launch keyboard settings menu")
return
}
UniversalLinks.externalLinkLauncher?(appSettings)
case "installfile":
if let block = Manager.shared.fileBrowserLauncher {
block(navigationController!)
} else {
SentryManager.captureAndLog("Listener for framework signal to launch file browser is missing")
}
case "forcederror":
SentryManager.forceError()
case "spacebartext":
showSpacebarText()
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?) {
log.info("prepare for segue")
// 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 {
log.info("keyboard language \(l) \(langName) has lexical model")
} else {
// Legacy behavior: we automatically install all MTNT language codes, even without
// a matching keyboard for the more specific variant(s).
SentryManager.breadcrumbAndLog("lexical model language \(l) has no keyboard installed!")
}
}
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 {
SentryManager.captureAndLog("no navigation controller for showing languages???")
}
}
func showSpacebarText() {
let vc = SpacebarTextViewController()
if let nc = navigationController {
nc.pushViewController(vc, animated: true)
setIsDoneButtonEnabled(nc, true)
} else {
SentryManager.captureAndLog("no navigation controller for showing spacebarText options")
}
}
}