diff --git a/ios/.gitignore b/ios/.gitignore index 55de8601d0..95c1a2de20 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -2,6 +2,7 @@ DerivedData .DS_Store project.xcworkspace xcuserdata +*.xcscheme build keyman/Keyman/KeymanEngine.framework samples/KMSample1/KeymanEngine.framework diff --git a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj index 94a94a8610..9789c00731 100644 --- a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj +++ b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj @@ -104,8 +104,27 @@ 9A31E2CE224AE8B100D9A491 /* LexicalModelRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A9CB083224170F700231FB9 /* LexicalModelRepository.swift */; }; 9A31E2CF224AE8B800D9A491 /* LexicalModelRepositoryDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A9CB0852241717C00231FB9 /* LexicalModelRepositoryDelegate.swift */; }; 9A31E2D0224AE8BF00D9A491 /* APILexicalModelRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A9CB0812241704800231FB9 /* APILexicalModelRepository.swift */; }; + 9A3B14BA228D333E0052A11F /* KeyboardPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08C62161F67CFB800268D03 /* KeyboardPickerViewController.swift */; }; + 9A3B14BB228F592F0052A11F /* LanguageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C04C2A6C1F6B7D9A00BA42B6 /* LanguageViewController.swift */; }; + 9A3B14BC228F59380052A11F /* LanguageDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C092D8381F6A70C8005C5485 /* LanguageDetailViewController.swift */; }; + 9A3B14BD228F593C0052A11F /* LexicalModelPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A9CB07F22416E5400231FB9 /* LexicalModelPickerViewController.swift */; }; + 9A3B14BE228F59420052A11F /* LexicalModelInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A4609962241B39B00B0BFD1 /* LexicalModelInfoViewController.swift */; }; + 9A3B14BF228F59450052A11F /* KeyboardInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08C62141F67C8D500268D03 /* KeyboardInfoViewController.swift */; }; + 9A3B14C0228F59490052A11F /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A60764322893A4E003BCFBA /* SettingsViewController.swift */; }; + 9A3B14C1228F595A0052A11F /* KeyboardError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165EB3A02098993900040A69 /* KeyboardError.swift */; }; + 9A3B14C3228F59C10052A11F /* KeyboardPickerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08C620F1F67BD4500268D03 /* KeyboardPickerButton.swift */; }; + 9A3B14C4228F59C70052A11F /* KeyboardPickerBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08C620B1F6792A200268D03 /* KeyboardPickerBarButtonItem.swift */; }; + 9A3B14C6228F59E00052A11F /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C040E50F1F8606E300901EE4 /* TextField.swift */; }; + 9A3B14C7228F59E40052A11F /* TextFieldDelegateProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0324B8C1F87480700AF3785 /* TextFieldDelegateProxy.swift */; }; + 9A3B14C8228F59EE0052A11F /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0324B8E1F8750B700AF3785 /* TextView.swift */; }; + 9A3B14C9228F59F10052A11F /* TextViewDelegateProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0324B901F8763E100AF3785 /* TextViewDelegateProxy.swift */; }; + 9A3B14CA228F5AF60052A11F /* KeyboardNameTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08C62121F67C31100268D03 /* KeyboardNameTableViewCell.swift */; }; + 9A3B14D2229370B20052A11F /* InstalledLanguagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A3B14D1229370B20052A11F /* InstalledLanguagesViewController.swift */; }; + 9A3B14D3229370B20052A11F /* InstalledLanguagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A3B14D1229370B20052A11F /* InstalledLanguagesViewController.swift */; }; 9A4609972241B39B00B0BFD1 /* LexicalModelInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A4609962241B39B00B0BFD1 /* LexicalModelInfoViewController.swift */; }; 9A4609992242047400B0BFD1 /* LexicalModelAPICall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A4609982242047400B0BFD1 /* LexicalModelAPICall.swift */; }; + 9A60764422893A4E003BCFBA /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A60764322893A4E003BCFBA /* SettingsViewController.swift */; }; + 9A7EEFA222DCF64F00877C22 /* LanguageSpecificViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7EEFA122DCF64F00877C22 /* LanguageSpecificViewController.swift */; }; 9A5091B122B418140094B99C /* KeyboardCommandStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE67D960228A6F190029F2B5 /* KeyboardCommandStructs.swift */; }; 9A5091B222B418A90094B99C /* KeyboardPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08C62161F67CFB800268D03 /* KeyboardPickerViewController.swift */; }; 9A5091B322B418CA0094B99C /* KeyboardInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08C62141F67C8D500268D03 /* KeyboardInfoViewController.swift */; }; @@ -116,6 +135,10 @@ 9A9CB0822241704800231FB9 /* APILexicalModelRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A9CB0812241704800231FB9 /* APILexicalModelRepository.swift */; }; 9A9CB084224170F700231FB9 /* LexicalModelRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A9CB083224170F700231FB9 /* LexicalModelRepository.swift */; }; 9A9CB0862241717C00231FB9 /* LexicalModelRepositoryDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A9CB0852241717C00231FB9 /* LexicalModelRepositoryDelegate.swift */; }; + 9AD4F53C229F85AC007992D3 /* LanguageSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD4F53A229F85AC007992D3 /* LanguageSettingsViewController.swift */; }; + 9AD4F53D229F85AC007992D3 /* LanguageSettingsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9AD4F53B229F85AC007992D3 /* LanguageSettingsViewController.xib */; }; + 9AD4F53E22A8A25B007992D3 /* LanguageSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD4F53A229F85AC007992D3 /* LanguageSettingsViewController.swift */; }; + 9AD4F53F22A8A286007992D3 /* LanguageSettingsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9AD4F53B229F85AC007992D3 /* LanguageSettingsViewController.xib */; }; C007C4651F9F52D8006461B9 /* LanguagesAPICall.swift in Sources */ = {isa = PBXBuildFile; fileRef = C007C4641F9F52D8006461B9 /* LanguagesAPICall.swift */; }; C024C9941FA6EB470060583B /* NotificationName.swift in Sources */ = {isa = PBXBuildFile; fileRef = C024C9931FA6EB470060583B /* NotificationName.swift */; }; C024C9961FA6EC650060583B /* NotificationCenter+Typed.swift in Sources */ = {isa = PBXBuildFile; fileRef = C024C9951FA6EC650060583B /* NotificationCenter+Typed.swift */; }; @@ -312,13 +335,19 @@ 9A079E3F223B602B00581263 /* LexicalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LexicalModel.swift; sourceTree = ""; }; 9A079E42223B61AE00581263 /* FullLexicalModelID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullLexicalModelID.swift; sourceTree = ""; }; 9A082558227589360051EBB0 /* Formatter+ISODateExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Formatter+ISODateExtension.swift"; sourceTree = ""; }; + 9A3B14D1229370B20052A11F /* InstalledLanguagesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InstalledLanguagesViewController.swift; path = Settings/InstalledLanguagesViewController.swift; sourceTree = ""; }; 9A0FC9FC22D66D9E00D33F86 /* Reachability.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Reachability.framework; path = ../../Carthage/Build/iOS/Reachability.framework; sourceTree = ""; }; 9A4609962241B39B00B0BFD1 /* LexicalModelInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LexicalModelInfoViewController.swift; sourceTree = ""; }; 9A4609982242047400B0BFD1 /* LexicalModelAPICall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LexicalModelAPICall.swift; sourceTree = ""; }; + 9A60763C22892485003BCFBA /* Settings.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Settings.storyboard; path = Settings/Settings.storyboard; sourceTree = ""; }; + 9A60764322893A4E003BCFBA /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SettingsViewController.swift; path = Settings/SettingsViewController.swift; sourceTree = ""; }; + 9A7EEFA122DCF64F00877C22 /* LanguageSpecificViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageSpecificViewController.swift; sourceTree = ""; }; 9A9CB07F22416E5400231FB9 /* LexicalModelPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LexicalModelPickerViewController.swift; sourceTree = ""; }; 9A9CB0812241704800231FB9 /* APILexicalModelRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APILexicalModelRepository.swift; sourceTree = ""; }; 9A9CB083224170F700231FB9 /* LexicalModelRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LexicalModelRepository.swift; sourceTree = ""; }; 9A9CB0852241717C00231FB9 /* LexicalModelRepositoryDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LexicalModelRepositoryDelegate.swift; sourceTree = ""; }; + 9AD4F53A229F85AC007992D3 /* LanguageSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LanguageSettingsViewController.swift; path = Settings/LanguageSettingsViewController.swift; sourceTree = ""; }; + 9AD4F53B229F85AC007992D3 /* LanguageSettingsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = LanguageSettingsViewController.xib; path = Settings/LanguageSettingsViewController.xib; sourceTree = ""; }; C007C4641F9F52D8006461B9 /* LanguagesAPICall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguagesAPICall.swift; sourceTree = ""; }; C024C9931FA6EB470060583B /* NotificationName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationName.swift; sourceTree = ""; }; C024C9951FA6EC650060583B /* NotificationCenter+Typed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationCenter+Typed.swift"; sourceTree = ""; }; @@ -592,6 +621,18 @@ path = KeymanEngineTests; sourceTree = ""; }; + 9A60763E22892495003BCFBA /* Settings */ = { + isa = PBXGroup; + children = ( + 9A60763C22892485003BCFBA /* Settings.storyboard */, + 9A60764322893A4E003BCFBA /* SettingsViewController.swift */, + 9A3B14D1229370B20052A11F /* InstalledLanguagesViewController.swift */, + 9AD4F53A229F85AC007992D3 /* LanguageSettingsViewController.swift */, + 9AD4F53B229F85AC007992D3 /* LanguageSettingsViewController.xib */, + ); + name = Settings; + sourceTree = ""; + }; C024C9921FA6EB340060583B /* Notification */ = { isa = PBXGroup; children = ( @@ -761,6 +802,7 @@ 9A9CB07F22416E5400231FB9 /* LexicalModelPickerViewController.swift */, C092D8381F6A70C8005C5485 /* LanguageDetailViewController.swift */, C04C2A6C1F6B7D9A00BA42B6 /* LanguageViewController.swift */, + 9A7EEFA122DCF64F00877C22 /* LanguageSpecificViewController.swift */, 98C0B11817A22DB5007ECAFC /* PickerButtons */, 98C0B11C17A2302E007ECAFC /* TableViewCells */, ); @@ -777,6 +819,7 @@ C024C9921FA6EB340060583B /* Notification */, C07A9D881FD1760800828ADD /* KeyboardRepository */, C0A93A531F8B21240079948B /* Manager.swift */, + 9A60763E22892495003BCFBA /* Settings */, CE25CCBB1F1DA72A005AA2BC /* HTTPRequest */, 987F00041AB8FCB700998116 /* KeyboardMenuView */, 980CC8E119D51EF00089BB57 /* URLProtocol */, @@ -986,6 +1029,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9AD4F53F22A8A286007992D3 /* LanguageSettingsViewController.xib in Resources */, 9A0FCA0922D7C58B00D33F86 /* Keyman.bundle in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -995,6 +1039,7 @@ buildActionMask = 2147483647; files = ( C06D37601F82095200F61AE0 /* Keyman.bundle in Resources */, + 9AD4F53D229F85AC007992D3 /* LanguageSettingsViewController.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1091,6 +1136,10 @@ buildActionMask = 2147483647; files = ( 9A079E45223DBEF000581263 /* PopoverView.swift in Sources */, + 9A3B14C4228F59C70052A11F /* KeyboardPickerBarButtonItem.swift in Sources */, + 9A3B14BE228F59420052A11F /* LexicalModelInfoViewController.swift in Sources */, + 9A3B14C7228F59E40052A11F /* TextFieldDelegateProxy.swift in Sources */, + 9A3B14CA228F5AF60052A11F /* KeyboardNameTableViewCell.swift in Sources */, 9A31E2CC224AE89F00D9A491 /* NotificationCenter+Typed.swift in Sources */, 9A079E2C22361B4B00581263 /* KeyboardRepository.swift in Sources */, 9A31E2C8224AE87600D9A491 /* RegisteredFont.swift in Sources */, @@ -1104,21 +1153,28 @@ 9A31E2C6224AE85000D9A491 /* UserDefaults+Types.swift in Sources */, 9A079E44223B61AE00581263 /* FullLexicalModelID.swift in Sources */, 9A079DE72236134B00581263 /* HTTPDownloader.swift in Sources */, + 9AD4F53E22A8A25B007992D3 /* LanguageSettingsViewController.swift in Sources */, 9A079E46223DBEF900581263 /* KeyPreviewView.swift in Sources */, 9A5091B122B418140094B99C /* KeyboardCommandStructs.swift in Sources */, 9A079DE32231A16600581263 /* Constants.swift in Sources */, 9A31E2C1224AAD8300D9A491 /* FontManager.swift in Sources */, + 9A3B14C6228F59E00052A11F /* TextField.swift in Sources */, + 9A3B14C3228F59C10052A11F /* KeyboardPickerButton.swift in Sources */, 9A079E29223619F100581263 /* InputViewController.swift in Sources */, 9A079E28223619D300581263 /* Options.swift in Sources */, 9A079DE42231A1ED00581263 /* Log.swift in Sources */, 9A31E2C5224AE83D00D9A491 /* JSONDecoder.DateDecodingStrategy+ISO8601Fallback.swift in Sources */, 9A079DDB2231A0F100581263 /* KMPKeyboard.swift in Sources */, + 9A3B14BD228F593C0052A11F /* LexicalModelPickerViewController.swift in Sources */, 9A079DDF2231A13A00581263 /* Keyboard.swift in Sources */, 9A5091B222B418A90094B99C /* KeyboardPickerViewController.swift in Sources */, 9A31E2C2224AAD9A00D9A491 /* Notifications.swift in Sources */, 9A079DDD2231A11C00581263 /* InstallableKeyboard.swift in Sources */, + 9A7EEFA222DCF64F00877C22 /* LanguageSpecificViewController.swift in Sources */, 9A079DD2223194B100581263 /* KeymanEngineTests.swift in Sources */, 9A079E41223B602B00581263 /* LexicalModel.swift in Sources */, + 9A3B14C0228F59490052A11F /* SettingsViewController.swift in Sources */, + 9A3B14BB228F592F0052A11F /* LanguageViewController.swift in Sources */, 9A31E2CE224AE8B100D9A491 /* LexicalModelRepository.swift in Sources */, 9A079E2E22361B8100581263 /* KeymanResponder+Types.swift in Sources */, 9A079DE12231A14D00581263 /* Storage.swift in Sources */, @@ -1129,17 +1185,24 @@ 9A31E2CB224AE89600D9A491 /* LexicalModelKeymanPackage.swift in Sources */, 9A31E2CF224AE8B800D9A491 /* LexicalModelRepositoryDelegate.swift in Sources */, 9A31E2C9224AE88600D9A491 /* LanguagesAPICall.swift in Sources */, + 9A3B14C1228F595A0052A11F /* KeyboardError.swift in Sources */, 9A079E2B22361A3200581263 /* KeymanWebViewController.swift in Sources */, 9A862D2822B429FB005FE26D /* KeyboardNameTableViewCell.swift in Sources */, 9A31E2CD224AE8A700D9A491 /* NotificationObserver.swift in Sources */, 9A079E382238680700581263 /* KMPLexicalModel.swift in Sources */, + 9A3B14BC228F59380052A11F /* LanguageDetailViewController.swift in Sources */, 9A31E2C4224AB2A300D9A491 /* UIButton+Helpers.swift in Sources */, 9A079DDE2231A13100581263 /* FullKeyboardID.swift in Sources */, + 9A3B14C9228F59F10052A11F /* TextViewDelegateProxy.swift in Sources */, 9A31E2CA224AE88E00D9A491 /* LexicalModelAPICall.swift in Sources */, 9A079E3222361C3B00581263 /* Migrations.swift in Sources */, + 9A3B14D3229370B20052A11F /* InstalledLanguagesViewController.swift in Sources */, 9A08255A227589360051EBB0 /* Formatter+ISODateExtension.swift in Sources */, + 9A3B14BA228D333E0052A11F /* KeyboardPickerViewController.swift in Sources */, 9A079E2A223619FC00581263 /* KeymanWebDelegate.swift in Sources */, + 9A3B14BF228F59450052A11F /* KeyboardInfoViewController.swift in Sources */, 9A31E2C3224AADBB00D9A491 /* NotificationName.swift in Sources */, + 9A3B14C8228F59EE0052A11F /* TextView.swift in Sources */, 9A862D2922B42A0C005FE26D /* LanguageDetailViewController.swift in Sources */, 9A079DDA2231A0B900581263 /* KeymanPackage.swift in Sources */, 9A079DE22231A15900581263 /* Manager.swift in Sources */, @@ -1172,6 +1235,7 @@ C06D37361F81F5C300F61AE0 /* KeyboardMenuView.swift in Sources */, CE67D961228A6F190029F2B5 /* KeyboardCommandStructs.swift in Sources */, 9A4609972241B39B00B0BFD1 /* LexicalModelInfoViewController.swift in Sources */, + 9AD4F53C229F85AC007992D3 /* LanguageSettingsViewController.swift in Sources */, 9A9CB08022416E5400231FB9 /* LexicalModelPickerViewController.swift in Sources */, 9A079E3D223B5FAF00581263 /* InstallableLexicalModel.swift in Sources */, C06D37381F81F5C400F61AE0 /* SubKeysView.swift in Sources */, @@ -1186,6 +1250,7 @@ C040E50E1F85FF8A00901EE4 /* KeyPreviewView.swift in Sources */, 9A079E3A223AFF3C00581263 /* KeyboardKeymanPackage.swift in Sources */, C06D37401F81F5C400F61AE0 /* LanguageDetailViewController.swift in Sources */, + 9A7EEFA222DCF64F00877C22 /* LanguageSpecificViewController.swift in Sources */, 9A9CB0862241717C00231FB9 /* LexicalModelRepositoryDelegate.swift in Sources */, C0CB633C1FA6F61500617CDB /* Notifications.swift in Sources */, C0EF3E7B1F95B65300CE9BD4 /* KeymanWebDelegate.swift in Sources */, @@ -1201,6 +1266,7 @@ C06D37431F81F5C400F61AE0 /* KeyboardPickerBarButtonItem.swift in Sources */, C082CE151F90AFD400860F02 /* Collection+SafeAccess.swift in Sources */, C06D37441F81F5C400F61AE0 /* KeyboardNameTableViewCell.swift in Sources */, + 9A3B14D2229370B20052A11F /* InstalledLanguagesViewController.swift in Sources */, C07A9D8A1FD1762700828ADD /* KeyboardRepository.swift in Sources */, C08E69911FDA6F6F0026056B /* FullKeyboardID.swift in Sources */, 9A9CB0822241704800231FB9 /* APILexicalModelRepository.swift in Sources */, @@ -1219,6 +1285,7 @@ C024C9941FA6EB470060583B /* NotificationName.swift in Sources */, C0C16A891FA8146300F090BA /* KeymanWebViewController.swift in Sources */, 9A079DCA222E050E00581263 /* LexicalModelKeymanPackage.swift in Sources */, + 9A60764422893A4E003BCFBA /* SettingsViewController.swift in Sources */, C06085B41F9485E40057E5B9 /* UIButton+Helpers.swift in Sources */, C07A9D8E1FD1798900828ADD /* APIKeyboardRepository.swift in Sources */, C0959CD41F99C44E00B616BC /* Constants.swift in Sources */, diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift b/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift index 0f1108c3fb..23a7cabd04 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift @@ -9,7 +9,8 @@ import Foundation public extension UserDefaults { - func installableKeyboards(forKey key: String) -> [InstallableKeyboard]? { + func installableKeyboards(forKey key: String) -> [InstallableKeyboard]? { + guard let array = array(forKey: key) as? [Data] else { return nil } @@ -190,6 +191,12 @@ public extension UserDefaults { func userLexicalModel(withFullID fullID: FullLexicalModelID) -> InstallableLexicalModel? { return userLexicalModels?.first { $0.fullID == fullID } } + + func userLexicalModelsForLanguage(languageID: String) -> [InstallableLexicalModel]? { + return userLexicalModels?.filter({ + $0.languageID == languageID + }) ?? [] + } var migrationLevel: Int { get { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/APILexicalModelRepository.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/APILexicalModelRepository.swift index 07330f265f..3a47d48a2b 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/APILexicalModelRepository.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/APILexicalModelRepository.swift @@ -75,7 +75,7 @@ public class APILexicalModelRepository: LexicalModelRepository { log.info("Request list completed -- \(result.lexicalModels.count) lexical models.") DispatchQueue.main.async { - self.delegate?.lexicalModelRepositoryDidFetch(self) + self.delegate?.lexicalModelRepositoryDidFetchList(self) fetchCompletionHandler(lexicalModels, nil) } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/LexicalModelRepositoryDelegate.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/LexicalModelRepositoryDelegate.swift index e9e2d5b2c5..a78750f182 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/LexicalModelRepositoryDelegate.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/LexicalModelRepositoryDelegate.swift @@ -9,6 +9,6 @@ import Foundation public protocol LexicalModelRepositoryDelegate: class { - func lexicalModelRepositoryDidFetch(_ repository: LexicalModelRepository) + func lexicalModelRepositoryDidFetchList(_ repository: LexicalModelRepository) func lexicalModelRepository(_ repository: LexicalModelRepository, didFailFetch error: Error) } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardInfoViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardInfoViewController.swift index 31608594f8..d6c93e5820 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardInfoViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardInfoViewController.swift @@ -35,7 +35,7 @@ class KeyboardInfoViewController: UITableViewController, UIAlertViewDelegate { ]) } infoArray.append([ - "title": "Delete", + "title": "Uninstall keyboard", "subtitle": "" ]) } @@ -43,7 +43,8 @@ class KeyboardInfoViewController: UITableViewController, UIAlertViewDelegate { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) navigationController?.setToolbarHidden(true, animated: true) - } + log.info("didAppear: KeyboardInfoViewController") +} override func numberOfSections(in tableView: UITableView) -> Int { return 1 diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardPickerViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardPickerViewController.swift index 3f9ac7ffb8..4a8d56434c 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardPickerViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardPickerViewController.swift @@ -53,6 +53,21 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { forName: Notifications.keyboardDownloadFailed, observer: self, function: KeyboardPickerViewController.keyboardDownloadFailed) + + lexicalModelDownloadStartedObserver = NotificationCenter.default.addObserver( + forName: Notifications.lexicalModelDownloadStarted, + observer: self, + function: KeyboardPickerViewController.lexicalModelDownloadStarted) + lexicalModelDownloadCompletedObserver = NotificationCenter.default.addObserver( + forName: Notifications.lexicalModelDownloadCompleted, + observer: self, + function: KeyboardPickerViewController.lexicalModelDownloadCompleted) + lexicalModelDownloadFailedObserver = NotificationCenter.default.addObserver( + forName: Notifications.lexicalModelDownloadFailed, + observer: self, + function: KeyboardPickerViewController.lexicalModelDownloadFailed) + + log.info("didLoad: KeyboardPickerViewController (registered lexicalModelDownloadCompleted et al)") } override func viewWillAppear(_ animated: Bool) { @@ -85,6 +100,7 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { navigationController?.setToolbarHidden(false, animated: true) scroll(toSelectedKeyboard: false) + log.info("didAppear: KeyboardPickerViewController") } override func numberOfSections(in tableView: UITableView) -> Int { @@ -182,12 +198,14 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { } private func keyboardDownloadStarted(_ keyboards: [InstallableKeyboard]) { + log.info("keyboardDownloadStarted") view.isUserInteractionEnabled = false navigationItem.leftBarButtonItem?.isEnabled = false navigationItem.rightBarButtonItem?.isEnabled = false } private func lexicalModelDownloadStarted(_ lexicalModels: [InstallableLexicalModel]) { + log.info("lexicalModelDownloadStarted") view.isUserInteractionEnabled = false navigationItem.leftBarButtonItem?.isEnabled = false navigationItem.rightBarButtonItem?.isEnabled = false @@ -212,11 +230,8 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { Manager.shared.downloadKeyboard(withID: kbID, languageID: langID, isUpdate: true) } else { loadUserKeyboards() - view.isUserInteractionEnabled = true - navigationItem.leftBarButtonItem?.isEnabled = true - if navigationItem.rightBarButtonItem != nil { - navigationItem.rightBarButtonItem?.isEnabled = true - } + restoreNavigation() + updateQueue = nil let label = navigationController?.toolbar?.viewWithTag(toolbarLabelTag) as? UILabel label?.text = "Keyboards successfully updated!" @@ -231,11 +246,7 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(self.hideToolbarDelayed), userInfo: nil, repeats: false) - view.isUserInteractionEnabled = true - navigationItem.leftBarButtonItem?.isEnabled = true - if navigationItem.rightBarButtonItem != nil { - navigationItem.rightBarButtonItem?.isEnabled = true - } + restoreNavigation() // Add keyboard. for keyboard in keyboards { @@ -245,9 +256,30 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { navigationController?.popToRootViewController(animated: true) } + log.info("keyboardDownloadCompleted KeyboardPicker") } - + + private func restoreNavigation() { + view.isUserInteractionEnabled = true + navigationItem.leftBarButtonItem?.isEnabled = true + if navigationItem.rightBarButtonItem != nil { + navigationItem.rightBarButtonItem?.isEnabled = true + } + } + + private func lexicalModelDownloadCompleted(_ lexicalModels: [InstallableLexicalModel]) { + log.info("lexicalModelDownloadCompleted KeyboardPicker") + // Add models. + for lexicalModel in lexicalModels { + Manager.shared.addLexicalModel(lexicalModel) + _ = Manager.shared.registerLexicalModel(lexicalModel) + } + restoreNavigation() + navigationController?.popToRootViewController(animated: true) + } + private func keyboardDownloadFailed(_ notification: KeyboardDownloadFailedNotification) { + log.info("keyboardDownloadFailed KeyboardPicker") view.isUserInteractionEnabled = true navigationItem.leftBarButtonItem?.isEnabled = true if let item = navigationItem.rightBarButtonItem { @@ -271,6 +303,28 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { self.present(alertController, animated: true, completion: nil) } + + private func lexicalModelDownloadFailed(_ notification: LexicalModelDownloadFailedNotification) { + log.info("lexicalModelDownloadFailed KeyboardPicker") + restoreNavigation() + + let title: String + if view == navigationController?.topViewController?.view { + updateQueue = nil + title = "Dictionary Update Error" + } else { + title = "Dictionary Download Error" + } + navigationController?.setToolbarHidden(true, animated: true) + + let alertController = UIAlertController(title: title, message: notification.error.localizedDescription, + preferredStyle: UIAlertControllerStyle.alert) + alertController.addAction(UIAlertAction(title: "OK", + style: UIAlertActionStyle.cancel, + handler: nil)) + + self.present(alertController, animated: true, completion: nil) + } private func switchKeyboard(_ index: Int) { // Switch keyboard and register to user defaults. diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageDetailViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageDetailViewController.swift index d4832dc0c7..1aa7ffea90 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageDetailViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageDetailViewController.swift @@ -44,6 +44,13 @@ class LanguageDetailViewController: UITableViewController, UIAlertViewDelegate { forName: Notifications.keyboardDownloadFailed, observer: self, function: LanguageDetailViewController.keyboardDownloadFailed) + log.info("viewDidLoad: LanguageDetailViewController (registered for keyboardDownloadStarted)") + } + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + log.info("didAppear: LanguageDetailViewController (actually willAppear)") } override func numberOfSections(in tableView: UITableView) -> Int { @@ -121,6 +128,7 @@ class LanguageDetailViewController: UITableViewController, UIAlertViewDelegate { } private func keyboardDownloadStarted() { + log.info("keyboardDownloadStarted: LanguageDetailViewController") view.isUserInteractionEnabled = false navigationItem.setHidesBackButton(true, animated: true) @@ -153,6 +161,7 @@ class LanguageDetailViewController: UITableViewController, UIAlertViewDelegate { } private func keyboardDownloadFailed() { + log.info("keyboardDownloadFailed: LanguageDetailViewController") view.isUserInteractionEnabled = true navigationItem.setHidesBackButton(false, animated: true) } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageSpecificViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageSpecificViewController.swift new file mode 100644 index 0000000000..66f04adad7 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageSpecificViewController.swift @@ -0,0 +1,29 @@ +// +// LanguageSpecificViewController.swift +// KeymanEngine +// +// Created by Randy Boring on 7/15/19. +// Copyright © 2019 SIL International. All rights reserved. +// + +import UIKit + +class LanguageSpecificViewController: LanguageViewController { + var language: Language! + + init(_ keyboardRepository: KeyboardRepository, language: Language) { + super.init(keyboardRepository) + self.language = language + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func postLanguageLoad(languageDict: [String: Language]) { + super .postLanguageLoad(languageDict: languageDict) + let langIdx = idxOfLanguage(languageID: language.id) + self.showLanguageDetailView(title: self.title!, languageIndex: langIdx) + } + +} diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageViewController.swift index 44d0c28611..812b36d1d0 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LanguageViewController.swift @@ -40,20 +40,41 @@ class LanguageViewController: UITableViewController, UIAlertViewDelegate { super.init(nibName: nil, bundle: nil) lexicalModelRepository.delegate = self } + + init(keyboardRep keyboardRepository: KeyboardRepository, modelRep lexicalModelRepository: LexicalModelRepository) { + self.keyboardRepository = keyboardRepository + self.lexicalModelRepository = lexicalModelRepository + super.init(nibName: nil, bundle: nil) + keyboardRepository.delegate = self + lexicalModelRepository.delegate = self + } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + + func postLanguageLoad(languageDict: [String: Language]) { + languages = languageList(languageDict) + } + + func idxOfLanguage(languageID: String) -> Int { + let langIdx = self.languages.firstIndex(where: { + $0.id == languageID + }) ?? 0 + return langIdx + } + override func loadView() { super.loadView() if let languageDict = keyboardRepository?.languages { - languages = languageList(languageDict) + self.postLanguageLoad(languageDict: languageDict) } else { + log.info("Fetching repository from API for keyboard download (LanguageViewController)") keyboardRepository?.fetch() } loadUserKeyboards() + loadUserLexicalModels() } override func viewDidLoad() { @@ -76,8 +97,11 @@ class LanguageViewController: UITableViewController, UIAlertViewDelegate { // if no rows to show yet, show a loading indicator if numberOfSections(in: tableView) == 0 { showActivityView() + log.info("didAppear: LanguageViewController, but no rows to show") + } else { + log.info("didAppear: LanguageViewController") } - } + } override func numberOfSections(in tableView: UITableView) -> Int { return languages.count @@ -203,7 +227,7 @@ class LanguageViewController: UITableViewController, UIAlertViewDelegate { showLanguageDetailView(title: title, languageIndex: indexPath.section) } - private func showLanguageDetailView(title: String, languageIndex: Int) { + func showLanguageDetailView(title: String, languageIndex: Int) { let langDetailView = LanguageDetailViewController(language: languages[languageIndex]) langDetailView.title = title navigationController?.pushViewController(langDetailView, animated: true) @@ -332,7 +356,7 @@ class LanguageViewController: UITableViewController, UIAlertViewDelegate { extension LanguageViewController: KeyboardRepositoryDelegate { func keyboardRepositoryDidFetch(_ repository: KeyboardRepository) { if let languageDict = repository.languages { - languages = languageList(languageDict) + self.postLanguageLoad(languageDict: languageDict) } self.dismissActivityView() self.tableView.reloadData() @@ -354,11 +378,11 @@ extension LanguageViewController: KeyboardRepositoryDelegate { } // MARK: - LexicalModelRepositoryDelegate - //may not need this, as we don't plan ever to fetch the whole lexical model repository (or even the list of all available) + extension LanguageViewController: LexicalModelRepositoryDelegate { - func lexicalModelRepositoryDidFetch(_ repository: LexicalModelRepository) { + func lexicalModelRepositoryDidFetchList(_ repository: LexicalModelRepository) { if let languageDict = repository.languages { - languages = languageList(languageDict) + self.postLanguageLoad(languageDict: languageDict) } self.dismissActivityView() self.tableView.reloadData() diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelInfoViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelInfoViewController.swift index c97233f2d1..67e50e5a1a 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelInfoViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelInfoViewController.swift @@ -5,7 +5,7 @@ // Created by Randy Boring on 3/19/19. // Copyright © 2019 SIL International. All rights reserved. // - +import UIKit import Foundation class LexicalModelInfoViewController: UITableViewController, UIAlertViewDelegate { @@ -24,7 +24,7 @@ class LexicalModelInfoViewController: UITableViewController, UIAlertViewDelegate infoArray = [[String: String]]() infoArray.append([ - "title": "Lexical model version", + "title": "Model version", "subtitle": lexicalModelVersion ]) @@ -35,7 +35,7 @@ class LexicalModelInfoViewController: UITableViewController, UIAlertViewDelegate ]) } infoArray.append([ - "title": "Delete", + "title": "Uninstall model", "subtitle": "" ]) } @@ -43,6 +43,7 @@ class LexicalModelInfoViewController: UITableViewController, UIAlertViewDelegate override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) navigationController?.setToolbarHidden(true, animated: true) + log.info("didAppear: LexicalModelInfoViewController") } override func numberOfSections(in tableView: UITableView) -> Int { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelPickerViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelPickerViewController.swift index 749e6e69ef..1e13eab1a3 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelPickerViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/LexicalModelPickerViewController.swift @@ -15,6 +15,7 @@ private let toolbarActivityIndicatorTag = 102 class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelegate { private var userLexicalModels: [InstallableLexicalModel] = [InstallableLexicalModel]() + public var language: Language? = nil private var updateQueue: [InstallableLexicalModel]? private var _isDoneButtonEnabled = false private var isDidUpdateCheck = false @@ -26,8 +27,14 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega override func viewDidLoad() { super.viewDidLoad() - title = "LexicalModels" - setIsDoneButtonEnabled(false) + if let langName = language?.name { + title = "\(langName) Lexical Models" + } else { + title = "Lexical Models" + } + if language == nil { + setIsDoneButtonEnabled(false) + } isDidUpdateCheck = false updateQueue = nil if Manager.shared.canAddNewLexicalModels { @@ -51,6 +58,8 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega forName: Notifications.lexicalModelDownloadFailed, observer: self, function: LexicalModelPickerViewController.lexicalModelDownloadFailed) + + log.info("didLoad: LexicalModelPickerViewController (registered lexicalModelDownloadCompleted et al)") } override func viewWillAppear(_ animated: Bool) { @@ -62,6 +71,7 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + log.info("didAppear: LexicalModelPickerViewController") if isDidUpdateCheck || !checkUpdates() { return } @@ -83,7 +93,9 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega navigationController?.setToolbarHidden(false, animated: true) scroll(toSelectedLexicalModel: false) - } +} + + // MARK: - Table view data source UITableViewDataSource override func numberOfSections(in tableView: UITableView) -> Int { return 1 @@ -112,13 +124,7 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega return false } - if !Manager.shared.canRemoveDefaultLexicalModel { - return indexPath.row != 0 - } - if indexPath.row > 0 { - return true - } - return userLexicalModels.count > 1 + return true } override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, @@ -127,7 +133,8 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega return } - if Manager.shared.removeLexicalModel(at: indexPath.row) { + let globalIndex = local2globalIndex(indexPath.row) + if Manager.shared.removeLexicalModel(at: globalIndex) { loadUserLexicalModels() } setIsDoneButtonEnabled(true) @@ -139,14 +146,16 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega } func showLexicalModelInfoView(with index: Int) { - setIsDoneButtonEnabled(true) + if language == nil { + setIsDoneButtonEnabled(true) + } let lm = userLexicalModels[index] let version = lm.version let infoView = LexicalModelInfoViewController() infoView.title = lm.name infoView.lexicalModelCount = userLexicalModels.count - infoView.lexicalModelIndex = index + infoView.lexicalModelIndex = local2globalIndex(index) infoView.lexicalModelID = lm.id infoView.languageID = lm.languageID infoView.lexicalModelVersion = version @@ -154,20 +163,22 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega navigationController?.pushViewController(infoView, animated: true) } + // MARK: - UITableViewDelegate + override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { cell.selectionStyle = .none let lm = userLexicalModels[indexPath.row] - cell.textLabel?.text = lm.languageID // maybe do a lookup for lm.languageName - cell.detailTextLabel?.text = lm.name + cell.textLabel?.text = lm.name + cell.detailTextLabel?.text = lm.id // maybe put a longer description here? cell.tag = indexPath.row if Manager.shared.currentLexicalModelID == lm.fullID { cell.selectionStyle = .blue cell.isSelected = true - cell.accessoryType = .detailDisclosureButton + cell.accessoryType = .checkmark } else { cell.selectionStyle = .none cell.isSelected = false @@ -179,6 +190,19 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega switchLexicalModel(indexPath.row) } + func local2globalIndex(_ localIndex: Int) -> Int { + let globalIndex: Int + if let lang = self.language { + let lm = userLexicalModels[localIndex] + globalIndex = Storage.active.userDefaults.userLexicalModels?.firstIndex(where: { + $0.languageID == lang.id && $0.id == lm.id + }) ?? 0 + } else { + globalIndex = localIndex + } + return globalIndex + } + private func lexicalModelDownloadStarted(_ lexicalModels: [InstallableLexicalModel]) { view.isUserInteractionEnabled = false navigationItem.leftBarButtonItem?.isEnabled = false @@ -186,6 +210,7 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega } private func lexicalModelDownloadCompleted(_ lexicalModels: [InstallableLexicalModel]) { + log.info("lexicalModelDownloadCompleted LexicalModelPicker") if view == navigationController?.topViewController?.view { if updateQueue == nil { return @@ -275,8 +300,15 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega } } + // if we have a language set, filter models down to those of that language private func loadUserLexicalModels() { - userLexicalModels = Storage.active.userDefaults.userLexicalModels ?? [] + if let lang = self.language { + userLexicalModels = Storage.active.userDefaults.userLexicalModels?.filter({ + $0.languageID == lang.id + }) ?? [] + } else { + userLexicalModels = Storage.active.userDefaults.userLexicalModels ?? [] + } tableView.reloadData() } @@ -391,7 +423,9 @@ class LexicalModelPickerViewController: UITableViewController, UIAlertViewDelega button?.isEnabled = false let vc = LanguageViewController(Manager.shared.apiLexicalModelRepository) //may need to be different for models navigationController?.pushViewController(vc, animated: true) - setIsDoneButtonEnabled(true) + if language == nil { + setIsDoneButtonEnabled(true) + } } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift index c7fca82746..8feb400c85 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift @@ -121,10 +121,9 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat public var canRemoveLexicalModels = true /// Allow the default lexical model to be removed. - /// The last lexical model cannot be removed, so the default lexical model cannot be removed if it - /// is the only lexical model in the list, regardless of the value of this property. - /// The default value is false. - public var canRemoveDefaultLexicalModel = false + /// The last lexical model CAN be removed, as this is an optional feature + /// The default value is true. + public var canRemoveDefaultLexicalModel = true public let apiLexicalModelRepository: APILexicalModelRepository @@ -136,7 +135,24 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat public var openURL: ((URL) -> Bool)? var currentKeyboardID: FullKeyboardID? - var currentLexicalModelID: FullLexicalModelID? + private var _currentLexicalModelID: FullLexicalModelID? + var currentLexicalModelID: FullLexicalModelID? { + get { + if _currentLexicalModelID == nil { + let userData = Util.isSystemKeyboard ? UserDefaults.standard : Storage.active.userDefaults + _currentLexicalModelID = userData.currentLexicalModelID + } + return _currentLexicalModelID + } + + set(value) { + _currentLexicalModelID = value + let userData = Util.isSystemKeyboard ? UserDefaults.standard : Storage.active.userDefaults + userData.currentLexicalModelID = _currentLexicalModelID + userData.synchronize() + } + } + var currentRequest: HTTPDownloadRequest? var shouldReloadKeyboard = false var shouldReloadLexicalModel = false @@ -219,6 +235,15 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat } // MARK: - Keyboard management + + public func showKeymanEngineSettings(inVC: UIViewController) -> Void { + hideKeyboard() + let settingsVC = SettingsViewController() + let nc = UINavigationController(rootViewController: settingsVC) + nc.modalTransitionStyle = .coverVertical + nc.modalPresentationStyle = .pageSheet + inVC.present(nc, animated: true) + } /// Sets the current keyboard, querying from the user's list of keyboards. /// @@ -276,7 +301,7 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat inputViewController.showHelpBubble(afterDelay: 1.5) } - //HERE getAssociatedLexicalModel(langID) and setLexicalModel + // getAssociatedLexicalModel(langID) and setLexicalModel NotificationCenter.default.post(name: Notifications.keyboardChanged, object: self, @@ -284,7 +309,6 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat let userDefaults: UserDefaults = Storage.active.userDefaults // If we have a lexical model for the keyboard's language, activate it. - //let lm = Storage.active.userDefaults.userLexicalModel(withFullID: valid_models[0].fullID)! if let preferred_model = preferredLexicalModel(userDefaults, forLanguage: kb.languageID) { _ = Manager.shared.registerLexicalModel(preferred_model) } @@ -342,10 +366,6 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat inputViewController.registerLexicalModel(lm) - let userData = Util.isSystemKeyboard ? UserDefaults.standard : Storage.active.userDefaults - userData.currentLexicalModelID = lm.fullID - userData.synchronize() - if isKeymanHelpOn { inputViewController.showHelpBubble(afterDelay: 1.5) } @@ -358,8 +378,11 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat return true } - /// Adds a new lexical model to the list in the lexical model picker if it doesn't already exist. - /// The lexical model must be downloaded (see `downloadLexicalModel()`) or preloaded (see `preloadLanguageFile()`) + + /** Adds a new lexical model to the list in the lexical model picker if it doesn't already exist. + * The lexical model must be downloaded (see `downloadLexicalModel()`) or preloaded (see `preloadLanguageFile()`) + * I believe this is background-thread-safe (no UI done) + */ public func addLexicalModel(_ lexicalModel: InstallableLexicalModel) { let lexicalModelPath = Storage.active.lexicalModelURL(for: lexicalModel).path if !FileManager.default.fileExists(atPath: lexicalModelPath) { @@ -381,7 +404,8 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat userDefaults.userLexicalModels = userLexicalModels userDefaults.set([Date()], forKey: Key.synchronizeSWLexicalModel) userDefaults.synchronize() - } + log.info("Added lexical model ID: \(lexicalModel.id) name: \(lexicalModel.name)") +} /// Removes a keyboard from the list in the keyboard picker if it exists. /// - Returns: The keyboard exists and was removed @@ -469,21 +493,21 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat return false } - let kb = userLexicalModels[index] + let lm = userLexicalModels[index] userLexicalModels.remove(at: index) userData.userLexicalModels = userLexicalModels userData.set([Date()], forKey: Key.synchronizeSWLexicalModel) userData.synchronize() - log.info("Removing lexical model with ID \(kb.id) and languageID \(kb.languageID)") + log.info("Removing lexical model with ID \(lm.id) and languageID \(lm.languageID)") // Set a new lexical model if deleting the current one - if kb.fullID == currentLexicalModelID { + if lm.fullID == currentLexicalModelID { _ = registerLexicalModel(userLexicalModels[0]) } - if !userLexicalModels.contains(where: { $0.id == kb.id }) { - let lexicalModelDir = Storage.active.lexicalModelDir(forID: kb.id) + if !userLexicalModels.contains(where: { $0.id == lm.id }) { + let lexicalModelDir = Storage.active.lexicalModelDir(forID: lm.id) FontManager.shared.unregisterFonts(in: lexicalModelDir, fromSystemOnly: false) log.info("Deleting directory \(lexicalModelDir)") if (try? FileManager.default.removeItem(at: lexicalModelDir)) == nil { @@ -493,7 +517,7 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat log.info("User has another language installed. Skipping delete of lexical model files.") } - NotificationCenter.default.post(name: Notifications.lexicalModelRemoved, object: self, value: kb) + NotificationCenter.default.post(name: Notifications.lexicalModelRemoved, object: self, value: lm) return true } @@ -970,16 +994,19 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat return nil } - func installLexicalModelPackage(downloadedPackageFile: URL) { + // return a lexical model so caller can use it in a downloadSucceeded call + func installLexicalModelPackage(downloadedPackageFile: URL) -> InstallableLexicalModel? { + var installedLexicalModel: InstallableLexicalModel? = nil let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] var destination = documentsDirectory destination.appendPathComponent("temp/\(downloadedPackageFile.lastPathComponent)") KeymanPackage.extract(fileUrl: downloadedPackageFile, destination: destination, complete: { kmp in - if let kmp = kmp { + if let kmp = kmp as! LexicalModelKeymanPackage? { do { try Manager.shared.parseLMKMP(kmp.sourceFolder) - log.info("successfully installed the lexical model from: \(kmp.sourceFolder)") + log.info("successfully parsed the lexical model in: \(kmp.sourceFolder)") + installedLexicalModel = kmp.models[0].installableLexicalModels[0] //this can fail gracefully and not show errors to users try FileManager.default.removeItem(at: downloadedPackageFile) } catch { @@ -989,6 +1016,7 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat log.error("Error extracting the lexical model from the package: \(KMPError.invalidPackage)") } }) + return installedLexicalModel } func downloadLexicalModelPackage(string lexicalModelPackageURLString: String) -> Void { @@ -1003,20 +1031,28 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat error: Error?) { if let error = error { log.error("Failed to fetch lexical model KMP file") - downloadFailed(forKeyboards: [], error: error) + downloadFailed(forLexicalModelPackage: lexicalModelPackageURLString, error: error) } else { do { try data!.write(to: dest) } catch { log.error("Error writing the lexical model download data: \(error)") } - installLexicalModelPackage(downloadedPackageFile: dest) + if let lm = installLexicalModelPackage(downloadedPackageFile: dest) { + downloadSucceeded(forLexicalModel: lm) + } else { + let installError = NSError(domain: "Keyman", code: 0, + userInfo: [NSLocalizedDescriptionKey: "installError"]) + downloadFailed(forLexicalModelPackage: lexicalModelPackageURLString, error: installError ) + } } } log.info("downloading lexical model from Keyman cloud: \(lexicalModelKMPURL).") let task = URLSession.shared.dataTask(with: lexicalModelKMPURL) { (data, response, error) in - lexicalModelDownloaded(data: data, response: response, dest: destinationUrl, error: error) + DispatchQueue.main.async { + lexicalModelDownloaded(data: data, response: response, dest: destinationUrl, error: error) + } } task.resume() @@ -1230,7 +1266,7 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat downloadQueue!.userInfo = commonUserData let request = HTTPDownloadRequest(url: lexicalModelURL, userInfo: commonUserData) - request.destinationFile = Storage.active.lexicalModelURL(forID: lexicalModel.id, version: lexicalModel.version ?? "0.1.0").path + request.destinationFile = Storage.active.lexicalModelURL(forID: lexicalModel.id, version: lexicalModel.version ?? InstallableConstants.defaultVersion).path request.tag = 0 downloadQueue!.addRequest(request) @@ -1381,6 +1417,20 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat object: self, value: notification) } + + private func downloadFailed(forLexicalModelPackage packageURL: String, error: Error) { + let notification = LexicalModelDownloadFailedNotification(lmOrLanguageID: packageURL, error: error) + NotificationCenter.default.post(name: Notifications.lexicalModelDownloadFailed, + object: self, + value: notification) + } + + private func downloadSucceeded(forLexicalModel lm: InstallableLexicalModel) { + let notification = LexicalModelDownloadCompletedNotification([lm]) + NotificationCenter.default.post(name: Notifications.lexicalModelDownloadCompleted, + object: self, + value: notification) + } // MARK: - Loading custom keyboards /// Preloads the JS and font files required for a keyboard. diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableLexicalModel.swift b/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableLexicalModel.swift index 460340c8b9..6786d52d1f 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableLexicalModel.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableLexicalModel.swift @@ -8,6 +8,10 @@ import Foundation +struct InstallableConstants { + static let defaultVersion = "0.1.0" +} + /// Mainly differs from the API `LexicalModel` by having an associated language. public struct InstallableLexicalModel: Codable { public var id: String @@ -38,7 +42,7 @@ public struct InstallableLexicalModel: Codable { self.name = lexicalModel.name self.languageID = languageID // self.languageName = language.name - self.version = lexicalModel.version ?? "0.1.0" + self.version = lexicalModel.version ?? InstallableConstants.defaultVersion self.isCustom = isCustom } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/Language.swift b/ios/engine/KMEI/KeymanEngine/Classes/Model/Language.swift index 8b4b80eaf3..d4b9c07446 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Model/Language.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Model/Language.swift @@ -19,7 +19,7 @@ public struct Language: Codable { /// Corresponding Keyboard objects. public let keyboards: [Keyboard]? - /// Corresponding Keyboard objects. + /// Corresponding lexical models. public let lexicalModels: [LexicalModel]? /// Font for input fields (and OSK if `oskFont` is not present). diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModelKeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModelKeymanPackage.swift index 321d039729..e5c117a914 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModelKeymanPackage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Model/LexicalModelKeymanPackage.swift @@ -9,7 +9,7 @@ import Foundation public class LexicalModelKeymanPackage : KeymanPackage { - private var models : [KMPLexicalModel]! + public var models : [KMPLexicalModel]! public override func defaultInfoHtml() -> String { var str = "Found lexical models in package:
" diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Settings/InstalledLanguagesViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Settings/InstalledLanguagesViewController.swift new file mode 100644 index 0000000000..0260f10e16 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/Settings/InstalledLanguagesViewController.swift @@ -0,0 +1,485 @@ +// +// InstalledLanguagesViewController.swift +// KeymanEngine +// +// Created by Randy Boring on 5/20/19. +// Copyright © 2019 SIL International. All rights reserved. +// +// +import QuartzCore +import UIKit + +private let activityViewTag = -2 +private let toolbarButtonTag = 100 +private let toolbarLabelTag = 101 +private let toolbarActivityIndicatorTag = 102 + +class InstalledLanguagesViewController: UITableViewController, UIAlertViewDelegate { + private var userKeyboards: [String: InstallableKeyboard] = [:] + private var userLexicalModels: [String: InstallableLexicalModel] = [:] + private var sectionIndexTitles: [String] = [] + private var indices: [Int] = [] + private var selectedSection = 0 + private var isUpdate = false + private var installedLanguages: [String: Language] + private var languages: [Language] = [] + private let keyboardRepository: KeyboardRepository? + private let lexicalModelRepository: LexicalModelRepository? + + private var keyboardDownloadStartedObserver: NotificationObserver? + private var keyboardDownloadCompletedObserver: NotificationObserver? + private var keyboardDownloadFailedObserver: NotificationObserver? + private var lexicalModelDownloadStartedObserver: NotificationObserver? + private var lexicalModelDownloadCompletedObserver: NotificationObserver? + private var lexicalModelDownloadFailedObserver: NotificationObserver? + + init(_ givenLanguages: [String: Language]) { + self.installedLanguages = givenLanguages + self.keyboardRepository = nil + self.lexicalModelRepository = nil + super.init(nibName: nil, bundle: nil) +// keyboardRepository.delegate = self + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + super.loadView() + languages = languageList(installedLanguages) + } + + override func viewDidLoad() { + super.viewDidLoad() + title = "Installed Languages" + selectedSection = NSNotFound + keyboardDownloadStartedObserver = NotificationCenter.default.addObserver( + forName: Notifications.keyboardDownloadStarted, + observer: self, + function: InstalledLanguagesViewController.keyboardDownloadStarted) + keyboardDownloadCompletedObserver = NotificationCenter.default.addObserver( + forName: Notifications.keyboardDownloadCompleted, + observer: self, + function: InstalledLanguagesViewController.keyboardDownloadCompleted) + keyboardDownloadFailedObserver = NotificationCenter.default.addObserver( + forName: Notifications.keyboardDownloadFailed, + observer: self, + function: InstalledLanguagesViewController.keyboardDownloadFailed) + + lexicalModelDownloadStartedObserver = NotificationCenter.default.addObserver( + forName: Notifications.lexicalModelDownloadStarted, + observer: self, + function: InstalledLanguagesViewController.lexicalModelDownloadStarted) + lexicalModelDownloadCompletedObserver = NotificationCenter.default.addObserver( + forName: Notifications.lexicalModelDownloadCompleted, + observer: self, + function: InstalledLanguagesViewController.lexicalModelDownloadCompleted) + lexicalModelDownloadFailedObserver = NotificationCenter.default.addObserver( + forName: Notifications.lexicalModelDownloadFailed, + observer: self, + function: InstalledLanguagesViewController.lexicalModelDownloadFailed) + + if Manager.shared.canAddNewKeyboards { + let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, + action: #selector(self.addClicked)) + navigationItem.rightBarButtonItem = addButton + } + + log.info("viewDidLoad: InstalledLanguagesViewController (registered for keyboardDownloadStarted)") + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + loadUserKeyboards() + loadUserLexicalModels() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + navigationController?.setToolbarHidden(true, animated: true) + // if no rows to show yet, show a loading indicator + if numberOfSections(in: tableView) == 0 { + showActivityView() + } + log.info("didAppear: InstalledLanguagesViewController") + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return languages.count + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cellIdentifierType1 = "CellType1" + let cellIdentifierType2 = "CellType2" + let keyboards = languages[indexPath.section].keyboards! + let cellIdentifier = (keyboards.count < 2) ? cellIdentifierType1 : cellIdentifierType2 + let cell: UITableViewCell + if let reusedCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) { + cell = reusedCell + } else { + let selectionColor = UIView() + selectionColor.backgroundColor = UIColor(red: 204.0 / 255.0, green: 136.0 / 255.0, + blue: 34.0 / 255.0, alpha: 1.0) + if keyboards.count < 2 { + cell = KeyboardNameTableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier) + } else { + cell = UITableViewCell(style: .subtitle, reuseIdentifier: cellIdentifier) + } + cell.selectedBackgroundView = selectionColor + cell.accessoryType = .disclosureIndicator + } + cell.detailTextLabel?.text = keyboards.count < 2 ? (keyboards.first?.name ?? "") : "\(keyboards.count) installed" + return cell + } + + // Create sections by first letter + override func sectionIndexTitles(for tableView: UITableView) -> [String] { + if !sectionIndexTitles.isEmpty { + return sectionIndexTitles + } + + sectionIndexTitles = [] + indices = [] + for (index, language) in languages.enumerated() { + let firstLetter = String(language.name.prefix(1)).uppercased() + if !sectionIndexTitles.contains(firstLetter) { + sectionIndexTitles.append(firstLetter) + indices.append(index) + } + } + return sectionIndexTitles + } + + override func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { + return indices[index] + } + + override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard let language = languages[safe: indexPath.section] else { + return + } + + cell.textLabel?.text = language.name + + if cell.accessoryType == .disclosureIndicator { + return + } + guard let cell = cell as? KeyboardNameTableViewCell else { + return + } + + let keyboardID = language.keyboards![0].id + + cell.accessoryType = .none + cell.isUserInteractionEnabled = true + cell.textLabel?.isEnabled = true + cell.detailTextLabel?.isEnabled = true + + let kbState = Manager.shared.stateForKeyboard(withID: keyboardID) + cell.setKeyboardState(kbState, selected: false, defaultAccessoryType: cell.accessoryType) + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + selectedSection = indexPath.section + tableView.cellForRow(at: indexPath)?.isSelected = false + if tableView.cellForRow(at: indexPath)?.accessoryType == .disclosureIndicator { + let title = tableView.cellForRow(at: indexPath)?.textLabel?.text ?? "" + showLanguageSettingsView(title: title, languageIndex: indexPath.section) + return + } + + let language = languages[indexPath.section] + let keyboardIndex = 0; + let keyboard = language.keyboards![keyboardIndex] + + //TODO: launch real Language Settings controller here + let languageSettingsController = UIAlertController(title: "\(language.name): \(keyboard.name)", + message: "This should be Language Settings", + preferredStyle: UIAlertControllerStyle.alert) + languageSettingsController.addAction(UIAlertAction(title: "Cancel", + style: UIAlertActionStyle.cancel, + handler: nil)) +// languageSettingsController.addAction(UIAlertAction(title: "Download", +// style: UIAlertActionStyle.default, +// handler: {_ in self.downloadHandler(keyboardIndex)} )) + self.present(languageSettingsController, animated: true, completion: nil) + } + + override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) { + let title = tableView.cellForRow(at: indexPath)?.textLabel?.text ?? "" + showLanguageSettingsView(title: title, languageIndex: indexPath.section) + } + + private func showLanguageSettingsView(title: String, languageIndex: Int) { + let langSettingsView = LanguageSettingsViewController(languages[languageIndex]) + langSettingsView.title = title + navigationController?.pushViewController(langSettingsView, animated: true) + } + + func errorAcknowledgmentHandler(withAction action: UIAlertAction) { + if !languages.isEmpty { + navigationController?.popToRootViewController(animated: true) + } + } + + func downloadHandler(_ keyboardIndex: Int) { + let language = languages[selectedSection] + let keyboard = language.keyboards![keyboardIndex] + Manager.shared.downloadKeyboard(withID: keyboard.id, languageID: language.id, isUpdate: isUpdate) + } + + private func restoreNavigation() { + view.isUserInteractionEnabled = true + navigationItem.setHidesBackButton(false, animated: true) + navigationItem.leftBarButtonItem?.isEnabled = true + if navigationItem.rightBarButtonItem != nil { + navigationItem.rightBarButtonItem?.isEnabled = true + } + } + + private func showDownloading(_ downloadLabel: String) { + guard let toolbar = navigationController?.toolbar else { + return + } + + let labelFrame = CGRect(origin: toolbar.frame.origin, + size: CGSize(width: toolbar.frame.width * 0.95, + height: toolbar.frame.height * 0.7)) + let label = UILabel(frame: labelFrame) + label.backgroundColor = UIColor.clear + label.textColor = UIColor.white + label.textAlignment = .center + label.center = CGPoint(x: toolbar.frame.width * 0.5, y: toolbar.frame.height * 0.5) + label.text = "Downloading \(downloadLabel)\u{2026}" + label.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, + .flexibleBottomMargin, .flexibleWidth, .flexibleHeight] + label.tag = toolbarLabelTag + + let indicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray) + indicatorView.center = CGPoint(x: toolbar.frame.width - indicatorView.frame.width, + y: toolbar.frame.height * 0.5) + indicatorView.autoresizingMask = [.flexibleLeftMargin, .flexibleTopMargin, .flexibleBottomMargin] + indicatorView.tag = toolbarActivityIndicatorTag + indicatorView.startAnimating() + toolbar.viewWithTag(toolbarButtonTag)?.removeFromSuperview() + toolbar.viewWithTag(toolbarLabelTag)?.removeFromSuperview() + toolbar.viewWithTag(toolbarActivityIndicatorTag)?.removeFromSuperview() + toolbar.addSubview(label) + toolbar.addSubview(indicatorView) + navigationController?.setToolbarHidden(false, animated: true) + } + + private func hideDownloading() { + navigationController?.toolbar?.viewWithTag(toolbarActivityIndicatorTag)?.removeFromSuperview() + navigationController?.toolbar?.viewWithTag(toolbarLabelTag)?.removeFromSuperview() + Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(self.hideToolbarDelayed), + userInfo: nil, repeats: false) + } + + private func keyboardDownloadStarted() { + log.info("keyboardDownloadStarted: InstalledLanguagesViewController") + view.isUserInteractionEnabled = false + navigationItem.setHidesBackButton(true, animated: true) + showDownloading("keyboard") + } + + private func lexicalModelDownloadStarted(_ lexicalModels: [InstallableLexicalModel]) { + log.info("lexicalModelDownloadStarted") + showDownloading("dictionary") + } + + private func keyboardDownloadCompleted(_ keyboards: [InstallableKeyboard]) { + log.info("keyboardDownloadCompleted: InstalledLanguagesViewController") + Manager.shared.shouldReloadKeyboard = true + + // Update keyboard version + for keyboard in keyboards { + Manager.shared.updateUserKeyboards(with: keyboard) + } + let label = navigationController?.toolbar?.viewWithTag(toolbarLabelTag) as? UILabel + label?.text = "Keyboard successfully downloaded!" + hideDownloading() + restoreNavigation() + + // Add keyboard. + for keyboard in keyboards { + Manager.shared.addKeyboard(keyboard) + _ = Manager.shared.setKeyboard(keyboard) + } + + navigationController?.popToRootViewController(animated: true) + } + + private func lexicalModelDownloadCompleted(_ lexicalModels: [InstallableLexicalModel]) { + log.info("lexicalModelDownloadCompleted: InstalledLanguagesViewController") + // Add models. + for lexicalModel in lexicalModels { + Manager.shared.addLexicalModel(lexicalModel) + _ = Manager.shared.registerLexicalModel(lexicalModel) + } + hideDownloading() + restoreNavigation() + navigationController?.popToRootViewController(animated: true) + } + + private func keyboardDownloadFailed() { + log.info("keyboardDownloadFailed: InstalledLanguagesViewController") + restoreNavigation() + } + + private func lexicalModelDownloadFailed() { + log.info("lexicalModelDownloadFailed: InstalledLanguagesViewController") + restoreNavigation() + + let title = "Lexical Model Download Error" + navigationController?.setToolbarHidden(true, animated: true) + + let alertController = UIAlertController(title: title, message: "", + preferredStyle: UIAlertControllerStyle.alert) + alertController.addAction(UIAlertAction(title: "OK", + style: UIAlertActionStyle.cancel, + handler: nil)) + + self.present(alertController, animated: true, completion: nil) + } + + func showActivityView() { + view.isUserInteractionEnabled = false + let indicatorView = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge) + let activityView = UIView(frame: indicatorView.bounds.insetBy(dx: -10.0, dy: -10.0)) + activityView.backgroundColor = UIColor(white: 0.5, alpha: 0.8) + activityView.layer.cornerRadius = 6.0 + activityView.center = view.center + activityView.tag = activityViewTag + activityView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, + .flexibleBottomMargin] + + indicatorView.center = CGPoint(x: activityView.bounds.size.width * 0.5, y: activityView.bounds.size.height * 0.5) + indicatorView.startAnimating() + activityView.addSubview(indicatorView) + view.addSubview(activityView) + } + + func dismissActivityView() { + let activityView = view.viewWithTag(activityViewTag) + activityView?.removeFromSuperview() + view.isUserInteractionEnabled = true + } + + func loadUserKeyboards() { + userKeyboards = [:] + guard let userKbList = Storage.active.userDefaults.userKeyboards else { + return + } + + for kb in userKbList { + let dictKey = "\(kb.languageID)_\(kb.id)" + userKeyboards[dictKey] = kb + } + tableView.reloadData() + } + + func loadUserLexicalModels() { + userLexicalModels = [:] + guard let userLmList = Storage.active.userDefaults.userLexicalModels else { + return + } + + for lm in userLmList { + let dictKey = "\(lm.languageID)_\(lm.id)" + userLexicalModels[dictKey] = lm + } + tableView.reloadData() + } + + private func isAdded(languageID: String?, keyboardID: String?) -> Bool { + guard let languageID = languageID, let keyboardID = keyboardID else { + return false + } + return userKeyboards["\(languageID)_\(keyboardID)"] != nil + } + + private func showConnectionErrorAlert() { + dismissActivityView() + let alertController = UIAlertController(title: "Connection Error", + message: "Could not reach Keyman server. Please try again later.", + preferredStyle: UIAlertControllerStyle.alert) + alertController.addAction(UIAlertAction(title: "OK", + style: UIAlertActionStyle.default, + handler: errorAcknowledgmentHandler)) + + self.present(alertController, animated: true, completion: nil) + } +} + +// MARK: - KeyboardRepositoryDelegate +extension InstalledLanguagesViewController: KeyboardRepositoryDelegate { + func keyboardRepositoryDidFetch(_ repository: KeyboardRepository) { + if let languageDict = repository.languages { + languages = languageList(languageDict) + } + self.dismissActivityView() + self.tableView.reloadData() + if self.numberOfSections(in: self.tableView) == 0 { + self.showConnectionErrorAlert() + } + } + + func keyboardRepository(_ repository: KeyboardRepository, didFailFetch error: Error) { + dismissActivityView() + showConnectionErrorAlert() + } + + private func languageList(_ languageDict: [String: Language]) -> [Language] { + return languageDict.values.sorted { a, b -> Bool in + a.name.localizedCaseInsensitiveCompare(b.name) == .orderedAscending + } + } +} + +// MARK: - LexicalModelRepositoryDelegate + +extension InstalledLanguagesViewController: LexicalModelRepositoryDelegate { + /** lexicalModelRepositoryDidFetchList callback on successful fetching of list of lexical models for a language + * caller should wrap in DispatchQueue.main.async {} if not already on main thread + */ + func lexicalModelRepositoryDidFetchList(_ repository: LexicalModelRepository) { + if let languageDict = repository.languages { + languages = languageList(languageDict) + } + self.dismissActivityView() + self.tableView.reloadData() + if self.numberOfSections(in: self.tableView) == 0 { + self.showConnectionErrorAlert() + } + } + + /** lexicalModelRepository didFailFetch callback on failure to fetch list of lexical models for a language + * caller should wrap in DispatchQueue.main.async {} if not already on main thread + */ + func lexicalModelRepository(_ repository: LexicalModelRepository, didFailFetch error: Error) { + dismissActivityView() + showConnectionErrorAlert() + } + + @objc func hideToolbarDelayed(_ timer: Timer) { + navigationController?.setToolbarHidden(true, animated: true) + } + + @objc func addClicked(_ sender: Any) { + showAddKeyboard() + } + + func showAddKeyboard() { + let button: UIButton? = (navigationController?.toolbar?.viewWithTag(toolbarButtonTag) as? UIButton) + button?.isEnabled = false + let vc = LanguageViewController(keyboardRep: Manager.shared.apiKeyboardRepository, modelRep: Manager.shared.apiLexicalModelRepository) + navigationController?.pushViewController(vc, animated: true) + } +} + diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.swift new file mode 100644 index 0000000000..c14188de96 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.swift @@ -0,0 +1,317 @@ +// +// LanguageSettingsViewController.swift +// KeymanEngine +// +// Created by Randy Boring on 5/29/19. +// Copyright © 2019 SIL International. All rights reserved. +// + +import UIKit + +private let toolbarButtonTag = 100 + +class LanguageSettingsViewController: UITableViewController { + let language: Language + private var userKeyboards: [String: Language] = [:] + private var settingsArray = [[String: String]]() + + 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() + title = "\(language.name) Settings" + log.info("viewDidLoad: LanguageSettingsViewController title: \(title ?? "")") + + 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) + + log.info("didAppear: LanguageSettingsViewController") + } + + + // 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 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + 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 { + //TODO: find the settings these are to show + if 0 == indexPath.row { + cell.accessoryType = .none + let doPredictionsSwitch = UISwitch() + let switchFrame = frameAtRightOfCell(cell: cell.frame, controlSize: doPredictionsSwitch.frame.size) + doPredictionsSwitch.frame = switchFrame + doPredictionsSwitch.isOn = false + // showBannerSwitch.addTarget(self, action: #selector(self.predictionSwitchValueChanged), for: .valueChanged) + cell.addSubview(doPredictionsSwitch) + } else if 1 == indexPath.row { + cell.accessoryType = .none + let doCorrectionsSwitch = UISwitch() + let switchFrame = frameAtRightOfCell(cell: cell.frame, controlSize: doCorrectionsSwitch.frame.size) + doCorrectionsSwitch.frame = switchFrame + doCorrectionsSwitch.isOn = false + // showBannerSwitch.addTarget(self, action: #selector(self.correctionSwitchValueChanged), for: .valueChanged) + cell.addSubview(doCorrectionsSwitch) + } else { // rows 3 and 4 + cell.accessoryType = .disclosureIndicator + } + } + let selectionColor = UIView() + selectionColor.backgroundColor = UIColor(red: 95.0 / 255.0, green: 196.0 / 255.0, blue: 217.0 / 255.0, alpha: 1.0) + 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: + title = "keyboards" + case 1: + title = "language settings" + 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 = "Enable corrections" + case 1: + cell.textLabel?.text = "Enable predictions" + case 2: + cell.textLabel?.text = "Model" + cell.accessoryType = .disclosureIndicator + if let modelCt = language.lexicalModels?.count { + switch modelCt { + case 0: + cell.detailTextLabel?.text = "no models installed" + case 1: + cell.detailTextLabel?.text = "one model installed" + default: + cell.detailTextLabel?.text = "\(modelCt) models installed" + } + } else { + cell.detailTextLabel?.text = "no models installed" + } + case 3: // future + cell.textLabel?.text = "Manage dictionary" + cell.accessoryType = .disclosureIndicator + cell.isUserInteractionEnabled = false + + default: + cell.textLabel?.text = "error" + } + } + } + + 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) + } + + @objc func addClicked(_ sender: Any) { + showAddLanguageKeyboard() + } + + func showAddLanguageKeyboard() { + let button: UIButton? = (navigationController?.toolbar?.viewWithTag(toolbarButtonTag) as? UIButton) + button?.isEnabled = false + let vc = LanguageSpecificViewController(Manager.shared.apiKeyboardRepository, language: language) + vc.title = "Add new \(language.name) keyboard" + navigationController?.pushViewController(vc, 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 2: + showLexicalModelsView() + default: + break + } + default: + break + } + } + + func showKeyboardInfoView(kb: Keyboard) { + let version = kb.version + 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 var globalUserKeyboards = userData.userKeyboards else { + log.error("no keyboards in the global keyboards list!") + return + } + + if let index = globalUserKeyboards.index(where: { $0.fullID == matchingFullID }) { + guard index < globalUserKeyboards.count else { + return + } + let kbIndex:Int = index + let thisKb = globalUserKeyboards[kbIndex] + let infoView = KeyboardInfoViewController() + infoView.title = thisKb.name + infoView.keyboardCount = userKeyboards.count + infoView.keyboardIndex = index + infoView.keyboardID = thisKb.id + infoView.languageID = language.id + infoView.keyboardVersion = version + infoView.isCustomKeyboard = thisKb.isCustom + navigationController?.pushViewController(infoView, animated: true) + } else { + log.error("this keyboard \(matchingFullID) not found among user's installed keyboards!") + return + } + } + + func showLexicalModelsView() { + //LanguageLexicalModelPickerViewController? (should show just the models for this language) + let lmListView = LexicalModelPickerViewController() + lmListView.language = self.language + navigationController?.pushViewController(lmListView, animated: true) + } + + func showLexicalModelInfoView() { + if let lm = language.lexicalModels?[safe: 0] { + let version = lm.version + let matchingFullID = FullLexicalModelID(lexicalModelID: lm.id, languageID: language.id) + + let userData = Storage.active.userDefaults + + if let globalUserLexicalModels = userData.userLexicalModels { + if let index = globalUserLexicalModels.index(where: { $0.fullID == matchingFullID }) { + guard index < globalUserLexicalModels.count else { + return + } + let lmIndex:Int = index + let thisLm = globalUserLexicalModels[lmIndex] + let infoView = LexicalModelInfoViewController() + infoView.title = thisLm.name + infoView.lexicalModelCount = globalUserLexicalModels.count + infoView.lexicalModelIndex = index + infoView.lexicalModelID = thisLm.id + infoView.languageID = language.id + infoView.lexicalModelVersion = version ?? InstallableConstants.defaultVersion + infoView.isCustomLexicalModel = thisLm.isCustom + navigationController?.pushViewController(infoView, animated: true) + } else { + log.error("this lexical model \(matchingFullID) not found among language's installed lexical model!") + } + } else { + log.error("no lexical models in the global models list!") + } + } + } + + /* + // 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. + } + */ + +} diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.xib b/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.xib new file mode 100644 index 0000000000..915de2b722 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.xib @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Settings/Settings.storyboard b/ios/engine/KMEI/KeymanEngine/Classes/Settings/Settings.storyboard new file mode 100644 index 0000000000..c5b79081f8 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/Settings/Settings.storyboard @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift new file mode 100644 index 0000000000..57da4fea48 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/Settings/SettingsViewController.swift @@ -0,0 +1,328 @@ +// +// 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("didAppear: SettingsViewController (actually willAppear)") + } + + override open func viewDidLoad() { + super.viewDidLoad() + + title = "Keyman Settings" + let doneButton = UIBarButtonItem(title: "Done", style: .plain, target: self, + action: #selector(self.doneClicked)) + navigationItem.leftBarButtonItem = doneButton + + navigationController?.toolbar?.barTintColor = UIColor(red: 0.5, green: 0.75, + blue: 0.25, alpha: 0.9) + } + + @objc func doneClicked(_ sender: Any) { + 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) +// } + + init(/*storage: Storage*/) { +// self.storage = storage + super.init(nibName: nil, bundle: nil) + + itemsArray = [[String: String]]() + itemsArray.append([ + "title": "Installed Languages", + "subtitle": "0", //count of installed languages as string + "reuseid" : "languages" + ]) + + itemsArray.append([ + "title": "Show Banner", + "subtitle": "", + "reuseid" : "showbanner" + ]) + + itemsArray.append([ + "title": "Show 'Get Started' at startup", + "subtitle": "", + "reuseid" : "showgetstarted" + ]) + _ = 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 { + // #warning Incomplete implementation, return the number of rows + return 3 + } + + 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": + cell.accessoryType = .disclosureIndicator + case "showbanner": + cell.accessoryType = .none + let showBannerSwitch = UISwitch() + let switchFrame = frameAtRightOfCell(cell: cell.frame, controlSize: showBannerSwitch.frame.size) + showBannerSwitch.frame = switchFrame + showBannerSwitch.isOn = false //TODO: find the setting this is to show! + showBannerSwitch.addTarget(self, action: #selector(self.bannerSwitchValueChanged), + for: .valueChanged) + cell.addSubview(showBannerSwitch) + case "showgetstarted": + cell.accessoryType = .none + let dontShowAgainSwitch = UISwitch() +// let rightOffset = cell.frame.size.width +// let switchWidth: CGFloat = 20 +// let switchX = rightOffset - switchWidth +// let dontShowAgainSwitch = UISwitch() +// let switchHeight = dontShowAgainSwitch.frame.size.height +// let cellSwitchHeightDiff = cell.frame.size.height - switchHeight +// let switchY = cell.frame.origin.y + 0.5 * cellSwitchHeightDiff + +// let switchFrame = CGRect(x: switchX, +// y: switchY, +// width: switchWidth, +// height: cell.frame.size.height) + let switchFrame = frameAtRightOfCell(cell: cell.frame, controlSize: dontShowAgainSwitch.frame.size) + dontShowAgainSwitch.frame = switchFrame + dontShowAgainSwitch.isOn = dontShowGetStarted + dontShowAgainSwitch.addTarget(self, action: #selector(self.showGetStartedSwitchValueChanged), + for: .valueChanged) + cell.addSubview(dontShowAgainSwitch) + + default: + log.error("unknown cellIdentifier(\"\(cellIdentifier ?? "EMPTY")\")") + cell.accessoryType = .none + } + + return cell + } + + @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: "ShouldShowBanner") //??? + userData.synchronize() + } + } + + @objc func showGetStartedSwitchValueChanged(_ sender: Any) { + let userData = Storage.active.userDefaults + if let toggle = sender as? UISwitch { + userData.set(toggle.isOn, forKey: "ShouldShowGetStarted") + userData.synchronize() + } + } + + private var dontShowGetStarted: Bool { + let userData = Storage.active.userDefaults + return !userData.bool(forKey: "ShouldShowGetStarted") + } + + override open func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + cell.accessoryType = .none + cell.textLabel?.text = itemsArray[indexPath.row]["title"] + cell.detailTextLabel?.text = itemsArray[indexPath.row]["subtitle"] + cell.tag = indexPath.row + cell.isUserInteractionEnabled = true + + if indexPath.row == 0 { + cell.accessoryType = .disclosureIndicator + } else { + cell.textLabel?.isEnabled = true + cell.detailTextLabel?.isEnabled = false + } + + } + + // 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: + showLanguages() + 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 - + private 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 = 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 { + log.error("lexical model language \(l) has no keyboard installed!") + } + } + + userLanguages = keyboardLanguages + itemsArray[0]["subtitle"] = "\(userLanguages.count) languages installed" + } + + public func setIsDoneButtonEnabled(_ nc: UINavigationController, _ value: Bool) { + let doneOrCancel = value + if doneOrCancel { + let doneButton = UIBarButtonItem(title: "Done", 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 { + log.error("no navigation controller for showing languages???") + } + } + +} diff --git a/ios/keyman/Keyman/Keyman/GetStartedViewController/GetStartedViewController.swift b/ios/keyman/Keyman/Keyman/GetStartedViewController/GetStartedViewController.swift index 24cc430fea..0908aa27fe 100644 --- a/ios/keyman/Keyman/Keyman/GetStartedViewController/GetStartedViewController.swift +++ b/ios/keyman/Keyman/Keyman/GetStartedViewController/GetStartedViewController.swift @@ -185,7 +185,27 @@ class GetStartedViewController: UIViewController, UITableViewDelegate, UITableVi } private var dontShowGetStarted: Bool { + return !shouldShowGetStarted + } + + private var shouldShowGetStarted: Bool { let userData = AppDelegate.activeUserDefaults() - return userData.bool(forKey: dontShowGetStartedKey) + var value: Any? = userData.object(forKey: shouldShowGetStartedKey) + if nil == value { // new key not present, try the old key + value = userData.object(forKey: dontShowGetStartedKey) + if nil == value { + userData.set(true, forKey: shouldShowGetStartedKey) + userData.synchronize() + return true // default to showing when no preference has been set + // so it shows at startup + } else { + userData.removeObject(forKey: dontShowGetStartedKey) + userData.set(false, forKey: shouldShowGetStartedKey) + userData.synchronize() + // remove the old, confusing value and set the new value + return false + } + } + return userData.bool(forKey: shouldShowGetStartedKey) } } diff --git a/ios/keyman/Keyman/Keyman/MainViewController.swift b/ios/keyman/Keyman/Keyman/MainViewController.swift index 3bde31037a..17373dc30a 100644 --- a/ios/keyman/Keyman/Keyman/MainViewController.swift +++ b/ios/keyman/Keyman/Keyman/MainViewController.swift @@ -18,7 +18,8 @@ private let checkedProfilesKey = "CheckedProfiles" // External strings let userTextKey = "UserText" let userTextSizeKey = "UserTextSize" -let dontShowGetStartedKey = "DontShowGetStarted" +let dontShowGetStartedKey = "DontShowGetStarted" // older preference setting name, use shouldShowGetStartedKey +let shouldShowGetStartedKey = "ShouldShowGetStarted" let launchedFromUrlNotification = NSNotification.Name("LaunchedFromUrlNotification") let urlKey = "url" @@ -269,7 +270,14 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg orientation: orientation) actionButton.title = "Share" - dropdownItems = [textSizeButton, trashButton, getStartedButton, infoButton] + let settingsButton = createNavBarButton(with: #imageLiteral(resourceName: "more.png"), // should find a gear image and use that instead of re-using 'more' + highlightedImage: #imageLiteral(resourceName: "more-selected.png"), + imageScale: imageScaleF, + action: #selector(self.settingsButtonClick), + orientation: orientation) + settingsButton.title = "Settings" + + dropdownItems = [textSizeButton, trashButton, infoButton, getStartedButton, settingsButton] var scaleF: CGFloat = (UIScreen.main.scale == 2.0) ? 2.0 : 1.5 scaleF *= (UIDevice.current.userInterfaceIdiom == .phone) ? 1.0 : 2.0 @@ -535,6 +543,13 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg } } + @objc func settingsButtonClick(_ sender: Any) { + _ = dismissDropDownMenu() + popover?.dismiss(animated: false) + + Manager.shared.showKeymanEngineSettings(inVC: self) + } + @objc func actionButtonClick(_ sender: Any) { _ = dismissDropDownMenu() popover?.dismiss(animated: false) @@ -981,6 +996,9 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg if userData.bool(forKey: dontShowGetStartedKey) { return false } + if !userData.bool(forKey: shouldShowGetStartedKey) { + return false + } if !AppDelegate.isKeymanEnabledSystemWide() { return true diff --git a/ios/keyman/Keyman/Keyman/SetUpViewController/SetUpViewController.xib b/ios/keyman/Keyman/Keyman/SetUpViewController/SetUpViewController.xib index c849610801..a53d2a7972 100644 --- a/ios/keyman/Keyman/Keyman/SetUpViewController/SetUpViewController.xib +++ b/ios/keyman/Keyman/Keyman/SetUpViewController/SetUpViewController.xib @@ -1,7 +1,12 @@ - - + + + + + - + + + @@ -25,10 +30,10 @@ - + - + diff --git a/mac/.gitignore b/mac/.gitignore index a162af22df..8a92eb5143 100644 --- a/mac/.gitignore +++ b/mac/.gitignore @@ -1,10 +1,11 @@ **/DerivedData **/project.xcworkspace **/xcuserdata +**/*.xcscheme Keyman4MacIM/output/* Keyman4MacIM/Keyman4MacIM/Frameworks/** Keyman4MacIM/Pods/** Keyman4Mac/Keyman4Mac/Frameworks/** Carthage/** **/build -**/output \ No newline at end of file +**/output