From bb68cbb07af475f16f8b89a09a10d67babd6c84f Mon Sep 17 00:00:00 2001 From: Purnama S Rahayu Date: Fri, 27 Feb 2026 13:20:32 +0700 Subject: [PATCH 01/31] refactor: remove old form elements --- Keyboards/KeyboardsBase/Keyboard.xib | 432 ++---------------- .../KeyboardViewController.swift | 133 +----- 2 files changed, 32 insertions(+), 533 deletions(-) diff --git a/Keyboards/KeyboardsBase/Keyboard.xib b/Keyboards/KeyboardsBase/Keyboard.xib index 29c5703d..97303185 100644 --- a/Keyboards/KeyboardsBase/Keyboard.xib +++ b/Keyboards/KeyboardsBase/Keyboard.xib @@ -1,9 +1,9 @@ - + - + @@ -15,38 +15,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -76,240 +44,48 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + - + - + - - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Conjugate/ConjugateApp.swift b/Conjugate/ConjugateApp.swift new file mode 100644 index 00000000..cba393de --- /dev/null +++ b/Conjugate/ConjugateApp.swift @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +@main +struct ConjugateApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/Conjugate/ContentView.swift b/Conjugate/ContentView.swift new file mode 100644 index 00000000..ed5bb33b --- /dev/null +++ b/Conjugate/ContentView.swift @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +struct ContentView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + ContentView() +} diff --git a/Conjugate/Navigation/AppNavigation.swift b/Conjugate/Navigation/AppNavigation.swift new file mode 100644 index 00000000..6a4e9f43 --- /dev/null +++ b/Conjugate/Navigation/AppNavigation.swift @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +struct AppNavigation: View { + @ViewBuilder let content: () -> Content + + var body: some View { + if #available(iOS 16, *) { + NavigationStack { + content() + } + } else { + NavigationView { + content() + } + } + } +} diff --git a/Conjugate/Views/ContentView.swift b/Conjugate/Views/ContentView.swift new file mode 100644 index 00000000..275f7954 --- /dev/null +++ b/Conjugate/Views/ContentView.swift @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +struct ContentView: View { + var body: some View { + TabView { + ConjugateTab() + .tabItem { + Label("Conjugate", systemImage: "character.book.closed") + } + + SettingsTab() + .tabItem { + Label("Settings", systemImage: "gearshape") + } + + AboutTab() + .tabItem { + Label("About", systemImage: "info.circle") + } + } + } +} diff --git a/Conjugate/Views/Tabs/About/AboutTab.swift b/Conjugate/Views/Tabs/About/AboutTab.swift new file mode 100644 index 00000000..f3e878c1 --- /dev/null +++ b/Conjugate/Views/Tabs/About/AboutTab.swift @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +struct AboutTab: View { + var body: some View { + AppNavigation { + Text("About") + .font(.largeTitle) + .navigationTitle("About") + } + } +} diff --git a/Conjugate/Views/Tabs/Conjugate/ConjugateTab.swift b/Conjugate/Views/Tabs/Conjugate/ConjugateTab.swift new file mode 100644 index 00000000..278e935e --- /dev/null +++ b/Conjugate/Views/Tabs/Conjugate/ConjugateTab.swift @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +struct ConjugateTab: View { + var body: some View { + AppNavigation { + Text("Conjugate") + .font(.largeTitle) + .navigationTitle("Conjugate") + } + } +} diff --git a/Conjugate/Views/Tabs/ConjugateTab.swift b/Conjugate/Views/Tabs/ConjugateTab.swift new file mode 100644 index 00000000..278e935e --- /dev/null +++ b/Conjugate/Views/Tabs/ConjugateTab.swift @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +struct ConjugateTab: View { + var body: some View { + AppNavigation { + Text("Conjugate") + .font(.largeTitle) + .navigationTitle("Conjugate") + } + } +} diff --git a/Conjugate/Views/Tabs/Settings/SettingsTab.swift b/Conjugate/Views/Tabs/Settings/SettingsTab.swift new file mode 100644 index 00000000..57468676 --- /dev/null +++ b/Conjugate/Views/Tabs/Settings/SettingsTab.swift @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +struct SettingsTab: View { + var body: some View { + AppNavigation { + Text("Settings") + .font(.largeTitle) + .navigationTitle("Settings") + } + } +} diff --git a/ConjugateApp-Info.plist b/ConjugateApp-Info.plist index c482fe3a..b7ea86b6 100644 --- a/ConjugateApp-Info.plist +++ b/ConjugateApp-Info.plist @@ -22,8 +22,6 @@ 1 UILaunchStoryboardName LaunchScreen - UIMainStoryboardFile - ConjugateAppScreen UIRequiredDeviceCapabilities armv7 diff --git a/Scribe.xcodeproj/project.pbxproj b/Scribe.xcodeproj/project.pbxproj index 10673075..281ef490 100644 --- a/Scribe.xcodeproj/project.pbxproj +++ b/Scribe.xcodeproj/project.pbxproj @@ -537,7 +537,6 @@ E91980BB2F2F540A00B5852F /* NavigationStructure.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91980B92F2F540000B5852F /* NavigationStructure.swift */; }; E91980BC2F2F540A00B5852F /* NavigationStructure.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91980B92F2F540000B5852F /* NavigationStructure.swift */; }; E91980BD2F2F540A00B5852F /* NavigationStructure.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91980B92F2F540000B5852F /* NavigationStructure.swift */; }; - E91980BE2F2F540A00B5852F /* NavigationStructure.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91980B92F2F540000B5852F /* NavigationStructure.swift */; }; E91980BF2F2F540A00B5852F /* NavigationStructure.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91980B92F2F540000B5852F /* NavigationStructure.swift */; }; E91980C02F2F540A00B5852F /* NavigationStructure.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91980B92F2F540000B5852F /* NavigationStructure.swift */; }; E91980C12F2F540A00B5852F /* NavigationStructure.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91980B92F2F540000B5852F /* NavigationStructure.swift */; }; @@ -588,7 +587,6 @@ E97E65142F2CDD5B0070810A /* ESInterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97E65092F2CDD5B0070810A /* ESInterfaceVariables.swift */; }; E97E65152F2CDD5B0070810A /* ESInterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97E65092F2CDD5B0070810A /* ESInterfaceVariables.swift */; }; E97E65162F2CDD5B0070810A /* ESInterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97E65092F2CDD5B0070810A /* ESInterfaceVariables.swift */; }; - E97E65172F2CDD730070810A /* ESInterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97E65092F2CDD5B0070810A /* ESInterfaceVariables.swift */; }; E97E65192F2CDEC50070810A /* ESCommandVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97E65182F2CDEC50070810A /* ESCommandVariables.swift */; }; E97E651A2F2CDEC50070810A /* ESCommandVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97E65182F2CDEC50070810A /* ESCommandVariables.swift */; }; E97E651B2F2CDEC50070810A /* ESCommandVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97E65182F2CDEC50070810A /* ESCommandVariables.swift */; }; @@ -598,7 +596,6 @@ E97E651F2F2CDEC50070810A /* ESCommandVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97E65182F2CDEC50070810A /* ESCommandVariables.swift */; }; E97E65202F2CDEC50070810A /* ESCommandVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97E65182F2CDEC50070810A /* ESCommandVariables.swift */; }; E97E65212F2CDEC50070810A /* ESCommandVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97E65182F2CDEC50070810A /* ESCommandVariables.swift */; }; - E97E65222F2CDEC50070810A /* ESCommandVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97E65182F2CDEC50070810A /* ESCommandVariables.swift */; }; E97E65232F2CDEC50070810A /* ESCommandVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97E65182F2CDEC50070810A /* ESCommandVariables.swift */; }; E97E65242F2CDEC50070810A /* ESCommandVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97E65182F2CDEC50070810A /* ESCommandVariables.swift */; }; E97E65252F2CDEC50070810A /* ESCommandVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97E65182F2CDEC50070810A /* ESCommandVariables.swift */; }; @@ -662,7 +659,6 @@ E9ED93872F2A3C45008D7451 /* DynamicConjugationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9ED937C2F2A3C32008D7451 /* DynamicConjugationViewController.swift */; }; E9ED93882F2A3C45008D7451 /* DynamicConjugationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9ED937C2F2A3C32008D7451 /* DynamicConjugationViewController.swift */; }; E9ED93892F2A3C45008D7451 /* DynamicConjugationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9ED937C2F2A3C32008D7451 /* DynamicConjugationViewController.swift */; }; - E9ED938A2F2A3C45008D7451 /* DynamicConjugationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9ED937C2F2A3C32008D7451 /* DynamicConjugationViewController.swift */; }; E9FAC39F2E98972D008E00AC /* IDInterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FAC3892E9894F9008E00AC /* IDInterfaceVariables.swift */; }; E9FAC3A12E98972D008E00AC /* IDKeyboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FAC38A2E9894F9008E00AC /* IDKeyboardViewController.swift */; }; E9FAC3A72E989A84008E00AC /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = E9FAC3A62E989A84008E00AC /* SwiftyJSON */; }; @@ -683,71 +679,8 @@ EDC364732AE409000001E456 /* InterfaceConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC364682AE408F20001E456 /* InterfaceConstants.swift */; }; EDC364742AE409000001E456 /* InterfaceConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC364682AE408F20001E456 /* InterfaceConstants.swift */; }; EDEE62252B2DE65A00A0B9C1 /* UIEdgeInsetsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEE62242B2DE65A00A0B9C1 /* UIEdgeInsetsExtensions.swift */; }; - F740686D2F1FB28E00E8560B /* ConjugateAppScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F740686B2F1FB28E00E8560B /* ConjugateAppScreen.storyboard */; }; - F786BAD42F1E8F70003F7505 /* ThirdPartyLicense.swift in Sources */ = {isa = PBXBuildFile; fileRef = 140158982A430DD000D14E52 /* ThirdPartyLicense.swift */; }; - F786BAD52F1E8F70003F7505 /* (null) in Sources */ = {isa = PBXBuildFile; }; - F786BAD62F1E8F70003F7505 /* AppTextStyling.swift in Sources */ = {isa = PBXBuildFile; fileRef = D17193BF27AEA33A0038660B /* AppTextStyling.swift */; }; - F786BAD72F1E8F70003F7505 /* ScribeColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1378C228F5D7AC00E1CBC2 /* ScribeColor.swift */; }; - F786BAD82F1E8F70003F7505 /* Conjugate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D171946427AF31770038660B /* Conjugate.swift */; }; - F786BAD92F1E8F70003F7505 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147797B42A2CFB490044A53E /* SettingsViewController.swift */; }; - F786BADA2F1E8F70003F7505 /* AboutTableData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1406B7862A2DFCDD001DF45B /* AboutTableData.swift */; }; - F786BADB2F1E8F70003F7505 /* DownloadStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9202DEF2F0FAA0C001590FC /* DownloadStateManager.swift */; }; - F786BADC2F1E8F70003F7505 /* IDInterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FAC3892E9894F9008E00AC /* IDInterfaceVariables.swift */; }; - F786BADF2F1E8F70003F7505 /* Translate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D111E9B127AFE79500746F92 /* Translate.swift */; }; - F786BAE32F1E8F70003F7505 /* ScribeKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D171945727AF237C0038660B /* ScribeKey.swift */; }; - F786BAE42F1E8F70003F7505 /* AppExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1406B78B2A3209CF001DF45B /* AppExtensions.swift */; }; - F786BAE62F1E8F70003F7505 /* FR-AZERTYInterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D180EC0228FDFABF0018E29B /* FR-AZERTYInterfaceVariables.swift */; }; - F786BAE72F1E8F70003F7505 /* ENInterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CDED7A2A859FBF00098546 /* ENInterfaceVariables.swift */; }; - F786BAE82F1E8F70003F7505 /* InstallationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38BD213522D5907F00C6795D /* InstallationVC.swift */; }; - F786BAE92F1E8F70003F7505 /* DEInterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D17193CF27AEC9EC0038660B /* DEInterfaceVariables.swift */; }; - F786BAEA2F1E8F70003F7505 /* SettingsTableData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147797BF2A2D0CDF0044A53E /* SettingsTableData.swift */; }; - F786BAEB2F1E8F70003F7505 /* InformationScreenVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14AC56892A261663006B1DDF /* InformationScreenVC.swift */; }; - F786BAEC2F1E8F70003F7505 /* InfoChildTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147797AE2A2CD3370044A53E /* InfoChildTableViewCell.swift */; }; - F786BAED2F1E8F70003F7505 /* AppUISymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A2DCB327AD3EB50057A10D /* AppUISymbols.swift */; }; - F786BAEE2F1E8F70003F7505 /* KeyboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D190B2592742565500705659 /* KeyboardViewController.swift */; }; - F786BAEF2F1E8F70003F7505 /* InstallationDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = E96111472F04EC62001E4F95 /* InstallationDownload.swift */; }; - F786BAF02F1E8F70003F7505 /* UIEdgeInsetsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDEE62242B2DE65A00A0B9C1 /* UIEdgeInsetsExtensions.swift */; }; - F786BAF12F1E8F70003F7505 /* (null) in Sources */ = {isa = PBXBuildFile; }; - F786BAF22F1E8F70003F7505 /* KeyboardStyling.swift in Sources */ = {isa = PBXBuildFile; fileRef = D171943727AEF0560038660B /* KeyboardStyling.swift */; }; - F786BAF32F1E8F70003F7505 /* Annotate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D111E9B927AFE7B200746F92 /* Annotate.swift */; }; - F786BAF42F1E8F70003F7505 /* KeyboardBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19DC85F92C7772FC006E32FD /* KeyboardBuilder.swift */; }; - F786BAF52F1E8F70003F7505 /* KeyboardProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 198369CB2C7980BA00C1B583 /* KeyboardProvider.swift */; }; - F786BAF62F1E8F70003F7505 /* UIColor+ScribeColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1378C328F5D7AC00E1CBC2 /* UIColor+ScribeColors.swift */; }; - F786BAF72F1E8F70003F7505 /* SVInterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D17193EF27AECB350038660B /* SVInterfaceVariables.swift */; }; - F786BAF92F1E8F70003F7505 /* TableViewTemplateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 140158A12A4EDB2200D14E52 /* TableViewTemplateViewController.swift */; }; - F786BAFB2F1E8F70003F7505 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38BD213322D5907F00C6795D /* AppDelegate.swift */; }; - F786BAFE2F1E8F70003F7505 /* RadioTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A68DA3D2CDE7B7900897FAD /* RadioTableViewCell.swift */; }; - F786BAFF2F1E8F70003F7505 /* CommandVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D190B2462741B24F00705659 /* CommandVariables.swift */; }; - F786BB002F1E8F70003F7505 /* DownloadDataScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9CE5EA72F063D870068A930 /* DownloadDataScreen.swift */; }; - F786BB012F1E8F70003F7505 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16DD3A429E78A1500FB9022 /* Utilities.swift */; }; - F786BB022F1E8F70003F7505 /* BaseTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB460202B03B3E400BEA967 /* BaseTableViewController.swift */; }; - F786BB032F1E8F70003F7505 /* DAInterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CDED742A859DDD00098546 /* DAInterfaceVariables.swift */; }; - F786BB052F1E8F70003F7505 /* UIDeviceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AF4D872C3575EA009AE0D2 /* UIDeviceExtensions.swift */; }; - F786BB062F1E8F70003F7505 /* TipCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69B81EBB2BFB8C77008CAB85 /* TipCardView.swift */; }; - F786BB072F1E8F70003F7505 /* InformationToolTipData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30453963293B9D18003AE55B /* InformationToolTipData.swift */; }; - F786BB092F1E8F70003F7505 /* RUInterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D17193DF27AECAA60038660B /* RUInterfaceVariables.swift */; }; - F786BB0A2F1E8F70003F7505 /* ITInterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B81D5227BBBA360085FE5E /* ITInterfaceVariables.swift */; }; - F786BB0B2F1E8F70003F7505 /* PTInterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D17193D727AECA450038660B /* PTInterfaceVariables.swift */; }; - F786BB0C2F1E8F70003F7505 /* HEInterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12EB9B92C81C0E700181765 /* HEInterfaceVariables.swift */; }; - F786BB0D2F1E8F70003F7505 /* AppStyling.swift in Sources */ = {isa = PBXBuildFile; fileRef = D17193C327AEAD7D0038660B /* AppStyling.swift */; }; - F786BB0E2F1E8F70003F7505 /* Plural.swift in Sources */ = {isa = PBXBuildFile; fileRef = D111E9A927AFE78600746F92 /* Plural.swift */; }; - F786BB0F2F1E8F70003F7505 /* InterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D190B2492741B31F00705659 /* InterfaceVariables.swift */; }; - F786BB102F1E8F70003F7505 /* CommandBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D111E9A127AFE4F300746F92 /* CommandBar.swift */; }; - F786BB132F1E8F70003F7505 /* ColorVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D190B240274056D400705659 /* ColorVariables.swift */; }; - F786BB142F1E8F70003F7505 /* SelectionViewTemplateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A68DA3F2CDE7B7900897FAD /* SelectionViewTemplateViewController.swift */; }; - F786BB152F1E8F70003F7505 /* AboutTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED2486F12B0B4E8C0038AE6A /* AboutTableViewCell.swift */; }; - F786BB162F1E8F70003F7505 /* InstallScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A2DCB027AD37BD0057A10D /* InstallScreen.swift */; }; - F786BB182F1E8F70003F7505 /* KeyboardKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = D171942E27AEDE110038660B /* KeyboardKeys.swift */; }; - F786BB192F1E8F70003F7505 /* ParentTableCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147797B22A2CD5AB0044A53E /* ParentTableCellModel.swift */; }; - F786BB1A2F1E8F70003F7505 /* WrapperCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FEE6CA2EF1433E003A9266 /* WrapperCell.swift */; }; - F786BB1B2F1E8F70003F7505 /* WikimediaAndScribe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1401589A2A45A07200D14E52 /* WikimediaAndScribe.swift */; }; - F786BB1D2F1E8F70003F7505 /* KeyAltChars.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B0719F27C6A1AA00FD7DBD /* KeyAltChars.swift */; }; - F786BB1E2F1E8F70003F7505 /* NBInterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CDED802A85A12400098546 /* NBInterfaceVariables.swift */; }; - F786BB1F2F1E8F70003F7505 /* FR-QWERTYInterfaceVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D17693DC28FC8CC300DF0FBB /* FR-QWERTYInterfaceVariables.swift */; }; - F786BB202F1E8F70003F7505 /* KeyAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B0719627C63C9100FD7DBD /* KeyAnimation.swift */; }; - F786BB212F1E8F70003F7505 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14AC56832A24AED3006B1DDF /* AboutViewController.swift */; }; - F786BB222F1E8F70003F7505 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DD94F022D6A40000FF8845 /* Extensions.swift */; }; - F786BB232F1E8F70003F7505 /* InterfaceConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC364682AE408F20001E456 /* InterfaceConstants.swift */; }; + F725CADE2F6A72BC00A8C950 /* ConjugateApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F725CADD2F6A72BC00A8C950 /* ConjugateApp.swift */; }; + F725CAE72F6A783400A8C950 /* SettingsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = F725CAE62F6A782D00A8C950 /* SettingsTab.swift */; }; F786BB252F1E8F70003F7505 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = F786BACB2F1E8F70003F7505 /* SwiftyJSON */; }; F786BB262F1E8F70003F7505 /* SwipeableTabBarController in Frameworks */ = {isa = PBXBuildFile; productRef = F786BACF2F1E8F70003F7505 /* SwipeableTabBarController */; }; F786BB272F1E8F70003F7505 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = F786BACD2F1E8F70003F7505 /* GRDB */; }; @@ -769,6 +702,10 @@ F786BB392F1E8F70003F7505 /* Spanish.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D1608665270B6D3C00134D48 /* Spanish.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; F786BB3A2F1E8F70003F7505 /* French.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D109A20B275B6888005E2271 /* French.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; F786BB3B2F1E8F70003F7505 /* Russian.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D1671A60275A1E8700A7C118 /* Russian.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + F7A17EBA2F6A8C180040B09B /* AppNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A17EB62F6A8BFE0040B09B /* AppNavigation.swift */; }; + F7A17EBB2F6A8C1C0040B09B /* AboutTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A17EAC2F6A8BFE0040B09B /* AboutTab.swift */; }; + F7A17EBC2F6A8C200040B09B /* ConjugateTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A17EAE2F6A8BFE0040B09B /* ConjugateTab.swift */; }; + F7A17EBD2F6A8C230040B09B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A17EB42F6A8BFE0040B09B /* ContentView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1124,9 +1061,23 @@ EDB460202B03B3E400BEA967 /* BaseTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTableViewController.swift; sourceTree = ""; }; EDC364682AE408F20001E456 /* InterfaceConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterfaceConstants.swift; sourceTree = ""; }; EDEE62242B2DE65A00A0B9C1 /* UIEdgeInsetsExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIEdgeInsetsExtensions.swift; sourceTree = ""; }; - F740686C2F1FB28E00E8560B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ConjugateAppScreen.storyboard; sourceTree = ""; }; + F725CADD2F6A72BC00A8C950 /* ConjugateApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConjugateApp.swift; sourceTree = ""; }; + F725CADF2F6A72D300A8C950 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + F725CAE42F6A782500A8C950 /* ConjugateTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConjugateTab.swift; sourceTree = ""; }; + F725CAE62F6A782D00A8C950 /* SettingsTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTab.swift; sourceTree = ""; }; + F725CAE82F6A783700A8C950 /* AboutTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutTab.swift; sourceTree = ""; }; + F725CAEA2F6A78CA00A8C950 /* AppNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNavigation.swift; sourceTree = ""; }; F786BB3F2F1E8F70003F7505 /* Conjugate.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Conjugate.app; sourceTree = BUILT_PRODUCTS_DIR; }; F786BB422F1E8F70003F7505 /* ConjugateApp-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "ConjugateApp-Info.plist"; path = "/Users/gauthammohanraj/Developer/Scribe-iOS/ConjugateApp-Info.plist"; sourceTree = ""; }; + F7A17EAB2F6A8BFE0040B09B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ContentView.swift; path = Conjugate/ContentView.swift; sourceTree = ""; }; + F7A17EAC2F6A8BFE0040B09B /* AboutTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutTab.swift; sourceTree = ""; }; + F7A17EAE2F6A8BFE0040B09B /* ConjugateTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConjugateTab.swift; sourceTree = ""; }; + F7A17EB02F6A8BFE0040B09B /* SettingsTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTab.swift; sourceTree = ""; }; + F7A17EB22F6A8BFE0040B09B /* ConjugateTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConjugateTab.swift; sourceTree = ""; }; + F7A17EB42F6A8BFE0040B09B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + F7A17EB62F6A8BFE0040B09B /* AppNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppNavigation.swift; path = Conjugate/AppNavigation.swift; sourceTree = ""; }; + F7A17EB72F6A8BFE0040B09B /* ConjugateApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ConjugateApp.swift; path = Conjugate/ConjugateApp.swift; sourceTree = ""; }; + F7A17EB82F6A8BFF0040B09B /* AppNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNavigation.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -1479,14 +1430,29 @@ ); target = D18EA8982760D4A6001E1358 /* Swedish */; }; + F7A17EA92F6A8BC90040B09B /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + CTAButton.swift, + DownloadButton.swift, + ); + target = F786BAB22F1E8F70003F7505 /* Conjugate */; + }; + F7A17EAA2F6A8BE30040B09B /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + ConfirmDialogView.swift, + ); + target = F786BAB22F1E8F70003F7505 /* Conjugate */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - E943457E2F05638700DFDB20 /* Button */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Button; sourceTree = ""; }; + E943457E2F05638700DFDB20 /* Button */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (F7A17EA92F6A8BC90040B09B /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Button; sourceTree = ""; }; E98E73BE2F20D8AA005EEDA3 /* Data */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (E98E74352F20EA5E005EEDA3 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E98E74362F20EA5E005EEDA3 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E98E74372F20EA5E005EEDA3 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E98E74382F20EA5E005EEDA3 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E98E74392F20EA5E005EEDA3 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E98E743A2F20EA5E005EEDA3 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E98E743B2F20EA5E005EEDA3 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E98E743C2F20EA5E005EEDA3 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E98E743D2F20EA5E005EEDA3 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E98E743E2F20EA5E005EEDA3 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E98E743F2F20EA5E005EEDA3 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E98E74402F20EA5E005EEDA3 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Data; sourceTree = ""; }; E98E73BF2F20D8C3005EEDA3 /* DataContracts */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = DataContracts; sourceTree = ""; }; E9B89DCD2F226757003E396F /* DataManager */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (E9B89DCE2F22676B003E396F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E9B89DCF2F22676B003E396F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E9B89DD02F22676B003E396F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E9B89DD12F22676B003E396F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E9B89DD22F22676B003E396F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E9B89DD32F22676B003E396F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E9B89DD42F22676B003E396F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E9B89DD52F22676B003E396F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E9B89DD62F22676B003E396F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E9B89DD72F22676B003E396F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E9B89DD82F22676B003E396F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E9B89DD92F22676B003E396F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E9B89DDA2F22676B003E396F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = DataManager; sourceTree = ""; }; - E9DADB332EF3CF9B00702783 /* ConfirmDialog */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = ConfirmDialog; sourceTree = ""; }; + E9DADB332EF3CF9B00702783 /* ConfirmDialog */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (F7A17EAA2F6A8BE30040B09B /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = ConfirmDialog; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1700,6 +1666,11 @@ 38BD212722D5907E00C6795D = { isa = PBXGroup; children = ( + F7A17EB62F6A8BFE0040B09B /* AppNavigation.swift */, + F7A17EB72F6A8BFE0040B09B /* ConjugateApp.swift */, + F7A17EAB2F6A8BFE0040B09B /* ContentView.swift */, + F7A17EB92F6A8BFF0040B09B /* Navigation */, + F7A17EB52F6A8BFE0040B09B /* Views */, F75BB4282F1FA72200C90392 /* Conjugate */, D1FECBC9270E1A1500C8BC60 /* Keyboards */, 38BD213222D5907F00C6795D /* Scribe */, @@ -2112,14 +2083,121 @@ path = Extensions; sourceTree = ""; }; + F725CAE12F6A77E300A8C950 /* Views */ = { + isa = PBXGroup; + children = ( + F725CAE32F6A781F00A8C950 /* Tabs */, + F725CADF2F6A72D300A8C950 /* ContentView.swift */, + ); + path = Views; + sourceTree = ""; + }; + F725CAE32F6A781F00A8C950 /* Tabs */ = { + isa = PBXGroup; + children = ( + F725CAED2F6A79DC00A8C950 /* About */, + F725CAEE2F6A79E600A8C950 /* Settings */, + F725CAEF2F6A79EF00A8C950 /* Conjugate */, + ); + path = Tabs; + sourceTree = ""; + }; + F725CAED2F6A79DC00A8C950 /* About */ = { + isa = PBXGroup; + children = ( + F725CAE82F6A783700A8C950 /* AboutTab.swift */, + ); + path = About; + sourceTree = ""; + }; + F725CAEE2F6A79E600A8C950 /* Settings */ = { + isa = PBXGroup; + children = ( + F725CAE62F6A782D00A8C950 /* SettingsTab.swift */, + ); + path = Settings; + sourceTree = ""; + }; + F725CAEF2F6A79EF00A8C950 /* Conjugate */ = { + isa = PBXGroup; + children = ( + F725CAE42F6A782500A8C950 /* ConjugateTab.swift */, + ); + path = Conjugate; + sourceTree = ""; + }; + F725CAF02F6A79FF00A8C950 /* Navigation */ = { + isa = PBXGroup; + children = ( + F725CAEA2F6A78CA00A8C950 /* AppNavigation.swift */, + ); + path = Navigation; + sourceTree = ""; + }; F75BB4282F1FA72200C90392 /* Conjugate */ = { isa = PBXGroup; children = ( - F740686B2F1FB28E00E8560B /* ConjugateAppScreen.storyboard */, + F725CAF02F6A79FF00A8C950 /* Navigation */, + F725CAE12F6A77E300A8C950 /* Views */, + F725CADD2F6A72BC00A8C950 /* ConjugateApp.swift */, ); path = Conjugate; sourceTree = ""; }; + F7A17EAD2F6A8BFE0040B09B /* About */ = { + isa = PBXGroup; + children = ( + F7A17EAC2F6A8BFE0040B09B /* AboutTab.swift */, + ); + path = About; + sourceTree = ""; + }; + F7A17EAF2F6A8BFE0040B09B /* Conjugate */ = { + isa = PBXGroup; + children = ( + F7A17EAE2F6A8BFE0040B09B /* ConjugateTab.swift */, + ); + path = Conjugate; + sourceTree = ""; + }; + F7A17EB12F6A8BFE0040B09B /* Settings */ = { + isa = PBXGroup; + children = ( + F7A17EB02F6A8BFE0040B09B /* SettingsTab.swift */, + ); + path = Settings; + sourceTree = ""; + }; + F7A17EB32F6A8BFE0040B09B /* Tabs */ = { + isa = PBXGroup; + children = ( + F7A17EAD2F6A8BFE0040B09B /* About */, + F7A17EAF2F6A8BFE0040B09B /* Conjugate */, + F7A17EB12F6A8BFE0040B09B /* Settings */, + F7A17EB22F6A8BFE0040B09B /* ConjugateTab.swift */, + ); + path = Tabs; + sourceTree = ""; + }; + F7A17EB52F6A8BFE0040B09B /* Views */ = { + isa = PBXGroup; + children = ( + F7A17EB32F6A8BFE0040B09B /* Tabs */, + F7A17EB42F6A8BFE0040B09B /* ContentView.swift */, + ); + name = Views; + path = Conjugate/Views; + sourceTree = ""; + }; + F7A17EB92F6A8BFF0040B09B /* Navigation */ = { + isa = PBXGroup; + children = ( + F7A17EB82F6A8BFF0040B09B /* AppNavigation.swift */, + ); + name = Navigation; + path = Conjugate/Navigation; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -2782,7 +2860,6 @@ F786BB2A2F1E8F70003F7505 /* Settings.bundle in Resources */, F786BB2B2F1E8F70003F7505 /* LaunchScreen.storyboard in Resources */, F786BB2C2F1E8F70003F7505 /* RadioTableViewCell.xib in Resources */, - F740686D2F1FB28E00E8560B /* ConjugateAppScreen.storyboard in Resources */, F786BB2D2F1E8F70003F7505 /* InfoChildTableViewCell.xib in Resources */, F786BB2E2F1E8F70003F7505 /* AboutTableViewCell.xib in Resources */, F786BB2F2F1E8F70003F7505 /* Localizable.xcstrings in Resources */, @@ -3494,74 +3571,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E97E65172F2CDD730070810A /* ESInterfaceVariables.swift in Sources */, - E9ED938A2F2A3C45008D7451 /* DynamicConjugationViewController.swift in Sources */, - F786BAD42F1E8F70003F7505 /* ThirdPartyLicense.swift in Sources */, - F786BAD52F1E8F70003F7505 /* (null) in Sources */, - F786BAD62F1E8F70003F7505 /* AppTextStyling.swift in Sources */, - E91980BE2F2F540A00B5852F /* NavigationStructure.swift in Sources */, - F786BAD72F1E8F70003F7505 /* ScribeColor.swift in Sources */, - F786BAD82F1E8F70003F7505 /* Conjugate.swift in Sources */, - F786BAD92F1E8F70003F7505 /* SettingsViewController.swift in Sources */, - F786BADA2F1E8F70003F7505 /* AboutTableData.swift in Sources */, - F786BADB2F1E8F70003F7505 /* DownloadStateManager.swift in Sources */, - F786BADC2F1E8F70003F7505 /* IDInterfaceVariables.swift in Sources */, - F786BADF2F1E8F70003F7505 /* Translate.swift in Sources */, - F786BAE32F1E8F70003F7505 /* ScribeKey.swift in Sources */, - F786BAE42F1E8F70003F7505 /* AppExtensions.swift in Sources */, - F786BAE62F1E8F70003F7505 /* FR-AZERTYInterfaceVariables.swift in Sources */, - F786BAE72F1E8F70003F7505 /* ENInterfaceVariables.swift in Sources */, - F786BAE82F1E8F70003F7505 /* InstallationVC.swift in Sources */, - F786BAE92F1E8F70003F7505 /* DEInterfaceVariables.swift in Sources */, - F786BAEA2F1E8F70003F7505 /* SettingsTableData.swift in Sources */, - F786BAEB2F1E8F70003F7505 /* InformationScreenVC.swift in Sources */, - F786BAEC2F1E8F70003F7505 /* InfoChildTableViewCell.swift in Sources */, - F786BAED2F1E8F70003F7505 /* AppUISymbols.swift in Sources */, - F786BAEE2F1E8F70003F7505 /* KeyboardViewController.swift in Sources */, - F786BAEF2F1E8F70003F7505 /* InstallationDownload.swift in Sources */, - F786BAF02F1E8F70003F7505 /* UIEdgeInsetsExtensions.swift in Sources */, - F786BAF12F1E8F70003F7505 /* (null) in Sources */, - F786BAF22F1E8F70003F7505 /* KeyboardStyling.swift in Sources */, - F786BAF32F1E8F70003F7505 /* Annotate.swift in Sources */, - F786BAF42F1E8F70003F7505 /* KeyboardBuilder.swift in Sources */, - F786BAF52F1E8F70003F7505 /* KeyboardProvider.swift in Sources */, - F786BAF62F1E8F70003F7505 /* UIColor+ScribeColors.swift in Sources */, - F786BAF72F1E8F70003F7505 /* SVInterfaceVariables.swift in Sources */, - F786BAF92F1E8F70003F7505 /* TableViewTemplateViewController.swift in Sources */, - F786BAFB2F1E8F70003F7505 /* AppDelegate.swift in Sources */, - F786BAFE2F1E8F70003F7505 /* RadioTableViewCell.swift in Sources */, - F786BAFF2F1E8F70003F7505 /* CommandVariables.swift in Sources */, - F786BB002F1E8F70003F7505 /* DownloadDataScreen.swift in Sources */, - F786BB012F1E8F70003F7505 /* Utilities.swift in Sources */, - F786BB022F1E8F70003F7505 /* BaseTableViewController.swift in Sources */, - F786BB032F1E8F70003F7505 /* DAInterfaceVariables.swift in Sources */, - F786BB052F1E8F70003F7505 /* UIDeviceExtensions.swift in Sources */, - E97E65222F2CDEC50070810A /* ESCommandVariables.swift in Sources */, - F786BB062F1E8F70003F7505 /* TipCardView.swift in Sources */, - F786BB072F1E8F70003F7505 /* InformationToolTipData.swift in Sources */, - F786BB092F1E8F70003F7505 /* RUInterfaceVariables.swift in Sources */, - F786BB0A2F1E8F70003F7505 /* ITInterfaceVariables.swift in Sources */, - F786BB0B2F1E8F70003F7505 /* PTInterfaceVariables.swift in Sources */, - F786BB0C2F1E8F70003F7505 /* HEInterfaceVariables.swift in Sources */, - F786BB0D2F1E8F70003F7505 /* AppStyling.swift in Sources */, - F786BB0E2F1E8F70003F7505 /* Plural.swift in Sources */, - F786BB0F2F1E8F70003F7505 /* InterfaceVariables.swift in Sources */, - F786BB102F1E8F70003F7505 /* CommandBar.swift in Sources */, - F786BB132F1E8F70003F7505 /* ColorVariables.swift in Sources */, - F786BB142F1E8F70003F7505 /* SelectionViewTemplateViewController.swift in Sources */, - F786BB152F1E8F70003F7505 /* AboutTableViewCell.swift in Sources */, - F786BB162F1E8F70003F7505 /* InstallScreen.swift in Sources */, - F786BB182F1E8F70003F7505 /* KeyboardKeys.swift in Sources */, - F786BB192F1E8F70003F7505 /* ParentTableCellModel.swift in Sources */, - F786BB1A2F1E8F70003F7505 /* WrapperCell.swift in Sources */, - F786BB1B2F1E8F70003F7505 /* WikimediaAndScribe.swift in Sources */, - F786BB1D2F1E8F70003F7505 /* KeyAltChars.swift in Sources */, - F786BB1E2F1E8F70003F7505 /* NBInterfaceVariables.swift in Sources */, - F786BB1F2F1E8F70003F7505 /* FR-QWERTYInterfaceVariables.swift in Sources */, - F786BB202F1E8F70003F7505 /* KeyAnimation.swift in Sources */, - F786BB212F1E8F70003F7505 /* AboutViewController.swift in Sources */, - F786BB222F1E8F70003F7505 /* Extensions.swift in Sources */, - F786BB232F1E8F70003F7505 /* InterfaceConstants.swift in Sources */, + F7A17EBD2F6A8C230040B09B /* ContentView.swift in Sources */, + F7A17EBC2F6A8C200040B09B /* ConjugateTab.swift in Sources */, + F7A17EBB2F6A8C1C0040B09B /* AboutTab.swift in Sources */, + F7A17EBA2F6A8C180040B09B /* AppNavigation.swift in Sources */, + F725CADE2F6A72BC00A8C950 /* ConjugateApp.swift in Sources */, + F725CAE72F6A783400A8C950 /* SettingsTab.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3712,14 +3727,6 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; - F740686B2F1FB28E00E8560B /* ConjugateAppScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - F740686C2F1FB28E00E8560B /* Base */, - ); - name = ConjugateAppScreen.storyboard; - sourceTree = ""; - }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ From 4ed6ca0cd4abb45d03de91b034f95dcf58466a3c Mon Sep 17 00:00:00 2001 From: Purnama S Rahayu Date: Thu, 5 Mar 2026 16:05:54 +0700 Subject: [PATCH 16/31] add invalid message for invalid translation --- Keyboards/KeyboardsBase/KeyboardViewController.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Keyboards/KeyboardsBase/KeyboardViewController.swift b/Keyboards/KeyboardsBase/KeyboardViewController.swift index ceaf659f..0813a6a6 100644 --- a/Keyboards/KeyboardsBase/KeyboardViewController.swift +++ b/Keyboards/KeyboardsBase/KeyboardViewController.swift @@ -1627,7 +1627,8 @@ class KeyboardViewController: UIInputViewController { autoCapAtStartOfProxy() if commandState == .invalid { - commandBar.text = commandPromptSpacing + invalidCommandMsgWikidata + let invalidMsg = prevToInvalidState == .translate ? invalidTranslationMsg : invalidCommandMsg + commandBar.text = commandPromptSpacing + invalidMsg commandBar.isShowingInfoButton = true } else { commandBar.isShowingInfoButton = false @@ -1655,7 +1656,7 @@ class KeyboardViewController: UIInputViewController { loadKeys() proxy.insertText(selectedText) autoCapAtStartOfProxy() - commandBar.text = commandPromptSpacing + invalidCommandMsgWiktionary + commandBar.text = commandPromptSpacing + invalidTranslationMsg commandBar.isShowingInfoButton = true commandBar.textColor = keyCharColor return From 50f702368ce0e56ba8c2435b96e3ac5d5b5e8233 Mon Sep 17 00:00:00 2001 From: Purnama S Rahayu Date: Thu, 5 Mar 2026 17:44:27 +0700 Subject: [PATCH 17/31] add invalid information texts for invalid translation --- .../Model/InformationToolTipData.swift | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift b/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift index 93625474..2c3d5cfb 100644 --- a/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift +++ b/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift @@ -35,6 +35,61 @@ enum InformationToolTipData { ] ) + static let wiktionaryExplanation = NSMutableAttributedString( + string: NSLocalizedString("i18n.app.keyboard.not_in_wiktionary.explanation_1", + value: "Wiktionary is a collaboratively edited dictionary in hundreds of different languages that's maintained by Wikimedia Foundation.", + comment: ""), + attributes: [ + NSAttributedString.Key.font: UIFont.systemFont( + ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.5 + ) + ] + ) + + static let wiktionaryTranslationOrigin = NSMutableAttributedString( + string: NSLocalizedString("i18n.app.keyboard.not_in_wiktionary.explanation_2", + value: "Scribe uses Wiktionary's translations for our translation functionality! You can choose from multiple translations per word based on the specific meaning.", + comment: ""), + attributes: [ + NSAttributedString.Key.font: UIFont.systemFont( + ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.5 + ) + ] + ) + + static let wiktionaryExplanation = NSMutableAttributedString( + string: NSLocalizedString("i18n.app.keyboard.not_in_wiktionary.explanation_1", + value: "Wiktionary is a collaboratively edited dictionary in hundreds of different languages that's maintained by Wikimedia Foundation.", + comment: ""), + attributes: [ + NSAttributedString.Key.font: UIFont.systemFont( + ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.5 + ) + ] + ) + + static let wiktionaryTranslationOrigin = NSMutableAttributedString( + string: NSLocalizedString("i18n.app.keyboard.not_in_wiktionary.explanation_2", + value: "Scribe uses Wiktionary's translations for our translation functionality! You can choose from multiple translations per word based on the specific meaning.", + comment: ""), + attributes: [ + NSAttributedString.Key.font: UIFont.systemFont( + ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.5 + ) + ] + ) + + static let howToContributeWiktionary = NSMutableAttributedString( + string: NSLocalizedString("i18n.app.keyboard.not_in_wiktionary.explanation_3", + value: "You can make an account at wiktionary.org to join the community that's supporting Scribe and so many other projects. Help us bring free information to the world!", + comment: ""), + attributes: [ + NSAttributedString.Key.font: UIFont.systemFont( + ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.75 + ) + ] + ) + static func getContent() -> [NSMutableAttributedString] { [wikiDataExplanation, wikiDataContationOrigin, howToContribute] } From 4c903521e93a26da1fdead467e12b4651347a80c Mon Sep 17 00:00:00 2001 From: Purnama S Rahayu Date: Tue, 17 Mar 2026 09:45:04 +0700 Subject: [PATCH 18/31] add info tooltip invalid command translations #597 --- .../ToolTip/Model/InformationToolTipData.swift | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift b/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift index 2c3d5cfb..dc8847d1 100644 --- a/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift +++ b/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift @@ -58,9 +58,7 @@ enum InformationToolTipData { ) static let wiktionaryExplanation = NSMutableAttributedString( - string: NSLocalizedString("i18n.app.keyboard.not_in_wiktionary.explanation_1", - value: "Wiktionary is a collaboratively edited dictionary in hundreds of different languages that's maintained by Wikimedia Foundation.", - comment: ""), + string: invalidWiktionaryMsg_1, attributes: [ NSAttributedString.Key.font: UIFont.systemFont( ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.5 @@ -69,9 +67,7 @@ enum InformationToolTipData { ) static let wiktionaryTranslationOrigin = NSMutableAttributedString( - string: NSLocalizedString("i18n.app.keyboard.not_in_wiktionary.explanation_2", - value: "Scribe uses Wiktionary's translations for our translation functionality! You can choose from multiple translations per word based on the specific meaning.", - comment: ""), + string: invalidWiktionaryMsg_2, attributes: [ NSAttributedString.Key.font: UIFont.systemFont( ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.5 @@ -80,9 +76,7 @@ enum InformationToolTipData { ) static let howToContributeWiktionary = NSMutableAttributedString( - string: NSLocalizedString("i18n.app.keyboard.not_in_wiktionary.explanation_3", - value: "You can make an account at wiktionary.org to join the community that's supporting Scribe and so many other projects. Help us bring free information to the world!", - comment: ""), + string: invalidWiktionaryMsg_3, attributes: [ NSAttributedString.Key.font: UIFont.systemFont( ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.75 From e883409aca56eb4d6428820bda154d3e2597ae03 Mon Sep 17 00:00:00 2001 From: Purnama S Rahayu Date: Tue, 24 Mar 2026 17:34:56 +0700 Subject: [PATCH 19/31] fix merge conflict variables --- .../KeyboardsBase/KeyboardViewController.swift | 2 +- .../KeyboardsBase/NavigationStructure.swift | 2 +- .../ToolTip/Model/InformationToolTipData.swift | 2 +- .../Danish/DAInterfaceVariables.swift | 18 +++++++++--------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Keyboards/KeyboardsBase/KeyboardViewController.swift b/Keyboards/KeyboardsBase/KeyboardViewController.swift index 0619a4aa..a20d9a60 100644 --- a/Keyboards/KeyboardsBase/KeyboardViewController.swift +++ b/Keyboards/KeyboardsBase/KeyboardViewController.swift @@ -1627,7 +1627,7 @@ class KeyboardViewController: UIInputViewController { autoCapAtStartOfProxy() if commandState == .invalid { - let invalidMsg = prevToInvalidState == .translate ? invalidTranslationMsg : invalidCommandMsg + let invalidMsg = prevToInvalidState == .translate ? invalidCommandMsgWiktionary : invalidCommandMsgWikidata commandBar.text = commandPromptSpacing + invalidMsg commandBar.isShowingInfoButton = true } else { diff --git a/Keyboards/KeyboardsBase/NavigationStructure.swift b/Keyboards/KeyboardsBase/NavigationStructure.swift index 014cf6d3..932868b1 100644 --- a/Keyboards/KeyboardsBase/NavigationStructure.swift +++ b/Keyboards/KeyboardsBase/NavigationStructure.swift @@ -117,7 +117,7 @@ struct NavigationBuilder { return content.map { attributedText in NavigationLevel( - title: isTranslate ? invalidTranslationMsg : invalidCommandMsg, + title: isTranslate ? invalidCommandMsgWiktionary : invalidCommandMsgWikidata, options: [(label: "", node: .finalValue(attributedText.string))] ) } diff --git a/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift b/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift index ddb8b7f7..eafb147d 100644 --- a/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift +++ b/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift @@ -53,7 +53,7 @@ enum InformationToolTipData { ) static let howToContributeWiktionary = NSMutableAttributedString( - string: invalidWiktionaryMsg_3, + string: invalidCommandTextWiktionary3, attributes: [ NSAttributedString.Key.font: UIFont.systemFont( ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.75 diff --git a/Keyboards/LanguageKeyboards/Danish/DAInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Danish/DAInterfaceVariables.swift index cd7a5a22..567276f2 100644 --- a/Keyboards/LanguageKeyboards/Danish/DAInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Danish/DAInterfaceVariables.swift @@ -141,13 +141,13 @@ func getDAKeys() {} // MARK: Provide Layout func setDAKeyboardLayout() { - invalidCommandMsg = "Findes ikke i Wikidata" - invalidWikidataMsg_1 = "Wikidata er en videnbase, der redigeres i fællesskab og vedligeholdes af Wikimedia Foundation. Den fungerer som en kilde til åbne data for projekter som Wikipedia og utallige andre." - invalidWikidataMsg_2 = "Scribe bruger Wikidatas sprogdata til mange af sine kernefunktioner. Vi får oplysninger som navneords køn, verbkonjugationer og meget mere!" - invalidWikidataMsg_3 = "Du kan oprette en konto på wikidata.org for at blive en del af det fællesskab, der støtter Scribe og så mange andre projekter. Hjælp os med at bringe gratis information ud i verden!" - - invalidTranslationMsg = "Ikke i Wiktionary" - invalidWiktionaryMsg_1 = "Wiktionary er en fællesredigeret ordbog på hundredvis af forskellige sprog, der vedligeholdes af Wikimedia Foundation." - invalidWiktionaryMsg_2 = "Scribe bruger Wiktionarys oversættelser til vores oversættelsesfunktion! Du kan vælge mellem flere oversættelser pr. ord baseret på den specifikke betydning." - invalidWiktionaryMsg_3 = "Du kan oprette en konto på wiktionary.org for at blive en del af det fællesskab, der støtter Scribe og så mange andre projekter. Hjælp os med at bringe gratis information ud til verden!" + invalidCommandMsgWikidata = "Findes ikke i Wikidata" + invalidCommandTextWikidata1 = "Wikidata er en videnbase, der redigeres i fællesskab og vedligeholdes af Wikimedia Foundation. Den fungerer som en kilde til åbne data for projekter som Wikipedia og utallige andre." + invalidCommandTextWikidata2 = "Scribe bruger Wikidatas sprogdata til mange af sine kernefunktioner. Vi får oplysninger som navneords køn, verbkonjugationer og meget mere!" + invalidCommandTextWikidata3 = "Du kan oprette en konto på wikidata.org for at blive en del af det fællesskab, der støtter Scribe og så mange andre projekter. Hjælp os med at bringe gratis information ud i verden!" + + invalidCommandMsgWiktionary = "Ikke i Wiktionary" + invalidCommandTextWiktionary1 = "Wiktionary er en fællesredigeret ordbog på hundredvis af forskellige sprog, der vedligeholdes af Wikimedia Foundation." + invalidCommandTextWiktionary2 = "Scribe bruger Wiktionarys oversættelser til vores oversættelsesfunktion! Du kan vælge mellem flere oversættelser pr. ord baseret på den specifikke betydning." + invalidCommandTextWiktionary3 = "Du kan oprette en konto på wiktionary.org for at blive en del af det fællesskab, der støtter Scribe og så mange andre projekter. Hjælp os med at bringe gratis information ud til verden!" } From c36997892fac57155f767acac16583d356302abd Mon Sep 17 00:00:00 2001 From: Purnama S Rahayu Date: Tue, 24 Mar 2026 17:44:44 +0700 Subject: [PATCH 20/31] fix merge conflict variables --- .../KeyboardsBase/ToolTip/Model/InformationToolTipData.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift b/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift index eafb147d..a7f1ad73 100644 --- a/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift +++ b/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift @@ -35,7 +35,7 @@ enum InformationToolTipData { ) static let wiktionaryExplanation = NSMutableAttributedString( - string: invalidWiktionaryMsg_1, + string: invalidCommandTextWiktionary1, attributes: [ NSAttributedString.Key.font: UIFont.systemFont( ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.5 @@ -44,7 +44,7 @@ enum InformationToolTipData { ) static let wiktionaryTranslationOrigin = NSMutableAttributedString( - string: invalidWiktionaryMsg_2, + string: invalidCommandTextWiktionary2, attributes: [ NSAttributedString.Key.font: UIFont.systemFont( ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.5 From 62a8e9debc88de97de2c51a810aad57561633e9e Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:40:19 +0530 Subject: [PATCH 21/31] feat: improve animation for check for new data #598 (#624) - Replace RadioCircle with CheckDataSpinner component - Add three states: idle (gray circle), checking (rotating arc + X cancel), checked (filled orange + checkmark) - Animate spinner using Timer at ~60fps for smooth rotation - Tap X during check to cancel back to idle - Auto-transitions to checked state after check completes Co-authored-by: Andrew Tavis McAllister --- .../InstallationTab/DownloadDataScreen.swift | 107 ++++++++++++++---- 1 file changed, 87 insertions(+), 20 deletions(-) diff --git a/Scribe/InstallationTab/DownloadDataScreen.swift b/Scribe/InstallationTab/DownloadDataScreen.swift index bd196c3b..c1b5b3c1 100644 --- a/Scribe/InstallationTab/DownloadDataScreen.swift +++ b/Scribe/InstallationTab/DownloadDataScreen.swift @@ -4,30 +4,99 @@ import SwiftUI /// Download data UI for getting new data for keyboards. -struct RadioCircle: View { - @Binding var isSelected: Bool - var onSelect: () -> Void +enum CheckDataState { + case idle + case checking + case checked +} + +struct CheckDataSpinner: View { + @Binding var state: CheckDataState + @State private var rotation: Double = 0 + @State private var spinnerTimer: Timer? var body: some View { ZStack { - Circle() - .stroke(isSelected ? Color("scribeCTA") : Color.gray, lineWidth: 2) - .frame(width: 24, height: 24) - - if isSelected { + switch state { + case .idle: + // Empty gray circle Circle() - .fill(Color("scribeCTA")) - .frame(width: 12, height: 12) + .stroke(Color.gray, lineWidth: 2) + .frame(width: 28, height: 28) + .contentShape(Circle()) + .onTapGesture { startChecking() } + + case .checking: + // Rotating arc (orange) with X cancel + ZStack { + Circle() + .stroke(Color.gray.opacity(0.3), lineWidth: 2.5) + .frame(width: 28, height: 28) + + Circle() + .trim(from: 0, to: 0.7) + .stroke(Color("scribeCTA"), style: StrokeStyle(lineWidth: 2.5, lineCap: .round)) + .frame(width: 28, height: 28) + .rotationEffect(.degrees(rotation)) + .onAppear { startRotation() } + .onDisappear { stopRotation() } + + Image(systemName: "xmark") + .font(.system(size: 10, weight: .bold)) + .foregroundColor(Color("scribeCTA")) + } + .contentShape(Circle()) + .onTapGesture { cancelChecking() } + + case .checked: + // Filled orange circle with checkmark + ZStack { + Circle() + .fill(Color("scribeCTA")) + .frame(width: 28, height: 28) + + Image(systemName: "checkmark") + .font(.system(size: 12, weight: .bold)) + .foregroundColor(.white) + } + .contentShape(Circle()) + .onTapGesture { resetToIdle() } } } - .contentShape(Circle()) - .onTapGesture { - withAnimation(.spring()) { - isSelected.toggle() - if isSelected { onSelect() } - } + .animation(.easeInOut(duration: 0.2), value: state == .idle) + } + + private func startChecking() { + withAnimation { state = .checking } + // Simulate a check completing after 2 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + guard state == .checking else { return } + stopRotation() + withAnimation { state = .checked } } } + + private func cancelChecking() { + stopRotation() + withAnimation { state = .idle } + } + + private func resetToIdle() { + withAnimation { state = .idle } + } + + private func startRotation() { + rotation = 0 + spinnerTimer?.invalidate() + spinnerTimer = Timer.scheduledTimer(withTimeInterval: 0.016, repeats: true) { _ in + rotation += 4 + } + } + + private func stopRotation() { + spinnerTimer?.invalidate() + spinnerTimer = nil + } } struct UpdateDataCardView: View { @@ -52,7 +121,7 @@ struct UpdateDataCardView: View { value: "Regularly update data", comment: "" ) - @State private var isCheckNew = false + @State private var checkState: CheckDataState = .idle @State private var isRegularUpdate = false var body: some View { @@ -70,9 +139,7 @@ struct UpdateDataCardView: View { Spacer() - RadioCircle(isSelected: $isCheckNew, onSelect: { - onInitializeStates() - }) + CheckDataSpinner(state: $checkState) } Divider() } From 80b5bc14e0c2a161f0dd094492598382a5c9ac1a Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Tue, 24 Mar 2026 22:05:26 +0100 Subject: [PATCH 22/31] Convert project over to using Swift standard of four spaces and format (#628) * Convert project over to using Swift standard of four spaces and format all files * Rename swiftlint file * Edit swiftlint rules - comment out some * Edit swiftlint rules - ignore some --- .pre-commit-config.yaml | 2 +- .swiftformat | 2 - .swiftlint.yml | 65 +- Conjugate/ContentView.swift | 2 +- .../DataManager/ConjugationManager.swift | 223 +- Keyboards/DataManager/ContractManager.swift | 16 +- Keyboards/DataManager/DataContract.swift | 4 +- Keyboards/DataManager/DeclensionManager.swift | 117 +- Keyboards/DataManager/GenderManager.swift | 185 +- Keyboards/DataManager/LanguageDBManager.swift | 881 ++-- Keyboards/DataManager/PluralManager.swift | 112 +- Keyboards/InterfaceConstants.swift | 6 +- Keyboards/KeyboardBuilder.swift | 48 +- Keyboards/KeyboardProvider.swift | 24 +- .../DynamicConjugationViewController.swift | 569 +-- Keyboards/KeyboardsBase/Extensions.swift | 154 +- .../KeyboardsBase/InterfaceVariables.swift | 212 +- Keyboards/KeyboardsBase/KeyAltChars.swift | 557 +-- Keyboards/KeyboardsBase/KeyAnimation.swift | 669 +-- Keyboards/KeyboardsBase/KeyboardKeys.swift | 671 +-- Keyboards/KeyboardsBase/KeyboardStyling.swift | 207 +- .../KeyboardViewController.swift | 3996 +++++++++-------- .../KeyboardsBase/NavigationStructure.swift | 210 +- .../ScribeFunctionality/Annotate.swift | 873 ++-- .../ScribeFunctionality/CommandBar.swift | 272 +- .../CommandVariables.swift | 52 +- .../ScribeFunctionality/Conjugate.swift | 46 +- .../ScribeFunctionality/Plural.swift | 91 +- .../ScribeFunctionality/ScribeKey.swift | 165 +- .../ScribeFunctionality/Translate.swift | 63 +- .../Model/InformationToolTipData.swift | 79 +- Keyboards/KeyboardsBase/Utilities.swift | 12 +- .../Danish/DAInterfaceVariables.swift | 251 +- .../Danish/DAKeyboardViewController.swift | 2 +- .../English/ENInterfaceVariables.swift | 491 +- .../English/ENKeyboardViewController.swift | 2 +- .../French/FR-AZERTYInterfaceVariables.swift | 493 +- .../French/FR-QWERTYInterfaceVariables.swift | 246 +- .../French/FRKeyboardViewController.swift | 2 +- .../German/DEInterfaceVariables.swift | 608 +-- .../German/DEKeyboardViewController.swift | 2 +- .../Hebrew/HEInterfaceVariables.swift | 415 +- .../Hebrew/HEKeyboardViewController.swift | 2 +- .../Indonesian/IDInterfaceVariables.swift | 399 +- .../Indonesian/IDKeyboardViewController.swift | 2 +- .../Italian/ITInterfaceVariables.swift | 469 +- .../Italian/ITKeyboardViewController.swift | 2 +- .../Norwegian/NBCommandVariables.swift | 64 +- .../Norwegian/NBInterfaceVariables.swift | 508 ++- .../Norwegian/NBKeyboardViewController.swift | 2 +- .../Portuguese/PTInterfaceVariables.swift | 465 +- .../Portuguese/PTKeyboardViewController.swift | 2 +- .../Russian/RUInterfaceVariables.swift | 441 +- .../Russian/RUKeyboardViewController.swift | 2 +- .../Spanish/ESCommandVariables.swift | 26 +- .../Spanish/ESInterfaceVariables.swift | 600 +-- .../Spanish/ESKeyboardViewController.swift | 2 +- .../Swedish/SVInterfaceVariables.swift | 589 +-- .../Swedish/SVKeyboardViewController.swift | 2 +- Network/APIClient.swift | 25 +- Scribe/AboutTab/AboutTableData.swift | 257 +- Scribe/AboutTab/AboutViewController.swift | 523 ++- Scribe/AboutTab/InformationScreenVC.swift | 320 +- Scribe/AppDelegate.swift | 256 +- Scribe/AppExtensions.swift | 68 +- Scribe/AppStyling.swift | 14 +- Scribe/AppTexts/AppTextStyling.swift | 351 +- Scribe/AppTexts/InstallScreen.swift | 82 +- Scribe/AppTexts/ThirdPartyLicense.swift | 63 +- Scribe/AppTexts/WikimediaAndScribe.swift | 40 +- Scribe/AppUISymbols.swift | 121 +- Scribe/Button/CTAButton.swift | 14 +- Scribe/Button/DownloadButton.swift | 182 +- Scribe/Colors/ColorVariables.swift | 4 +- Scribe/Colors/ScribeColor.swift | 56 +- Scribe/Colors/UIColor+ScribeColors.swift | 42 +- Scribe/ConfirmDialog/ConfirmDialogView.swift | 210 +- Scribe/Extensions/UIDeviceExtensions.swift | 24 +- .../Extensions/UIEdgeInsetsExtensions.swift | 11 +- .../InstallationTab/DownloadDataScreen.swift | 747 +-- .../DownloadStateManager.swift | 183 +- .../InstallationDownload.swift | 97 +- Scribe/InstallationTab/InstallationVC.swift | 988 ++-- Scribe/ParentTableCellModel.swift | 112 +- Scribe/SettingsTab/SettingsTableData.swift | 328 +- .../SettingsTab/SettingsViewController.swift | 579 +-- Scribe/TipCard/TipCardView.swift | 159 +- Scribe/Toast/ToastView.swift | 2 +- Scribe/Views/BaseTableViewController.swift | 211 +- .../InfoChildTableViewCell.swift | 385 +- .../InfoChildTableViewCell/WrapperCell.swift | 217 +- .../RadioTableViewCell.swift | 89 +- .../UITableViewCells/AboutTableViewCell.swift | 147 +- .../SelectionViewTemplateViewController.swift | 317 +- .../TableViewTemplateViewController.swift | 268 +- Services/LanguageDataService.swift | 183 +- .../KeyboardsBase/TestExtensions.swift | 255 +- .../KeyboardsBase/TestKeyboardStyling.swift | 512 +-- .../Views/BaseTableViewControllerTest.swift | 28 +- 99 files changed, 13303 insertions(+), 11841 deletions(-) delete mode 100644 .swiftformat diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f370fcf2..db61bfea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: hooks: - id: swiftlint name: run swiftlint linting check - entry: swiftlint --strict + entry: swiftlint --fix --strict - repo: https://github.com/to-sta/spdx-checker-pre-commit rev: 0.1.7 diff --git a/.swiftformat b/.swiftformat deleted file mode 100644 index fef1feb7..00000000 --- a/.swiftformat +++ /dev/null @@ -1,2 +0,0 @@ -disable_formatting_rules: - - trailingCommas diff --git a/.swiftlint.yml b/.swiftlint.yml index cc96a49c..30d87000 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,16 +1,65 @@ disabled_rules: # Currently raise errors: + - closure_parameter_position - cyclomatic_complexity - - identifier_name - - function_body_length + - file_length + - for_where - force_cast - force_try + - function_body_length + - identifier_name - line_length - - file_length - type_body_length -identifier_name: - min_length: 1 - max_length: - warning: 40 - error: 41 +opt_in_rules: + # - array_init + # - closure_spacing + - collection_alignment + - comma_inheritance + - contains_over_filter_count + - contains_over_filter_is_empty + # - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type + - discouraged_assert + - empty_collection_literal + # - empty_count + # - empty_string + - explicit_init + - fatal_error_message + - first_where + - flatmap_over_map_reduce + - identical_operands + - joined_default_parameter + - last_where + - legacy_multiple + - legacy_random + - local_doc_comment + - lower_acl_than_parent + - modifier_order + - no_extension_access_modifier + - optional_enum_case_matching + - overridden_super_call + - pattern_matching_keywords + - prohibited_super_call + - reduce_into + # - redundant_self_in_closure + - redundant_type_annotation + # - shorthand_optional_binding + - sorted_first_last + - toggle_bool + - unhandled_throwing_task + - vertical_parameter_alignment_on_call + - yoda_condition + +analyzer_rules: + - capture_variable + - typesafe_array_init + - unused_declaration + - unused_import + +vertical_whitespace: + max_empty_lines: 2 + +line_length: + ignores_urls: true diff --git a/Conjugate/ContentView.swift b/Conjugate/ContentView.swift index ed5bb33b..f8b455a2 100644 --- a/Conjugate/ContentView.swift +++ b/Conjugate/ContentView.swift @@ -4,7 +4,7 @@ import SwiftUI struct ContentView: View { var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + Text( /*@START_MENU_TOKEN@*/"Hello, World!" /*@END_MENU_TOKEN@*/) } } diff --git a/Keyboards/DataManager/ConjugationManager.swift b/Keyboards/DataManager/ConjugationManager.swift index eb60e2cc..3981aa69 100644 --- a/Keyboards/DataManager/ConjugationManager.swift +++ b/Keyboards/DataManager/ConjugationManager.swift @@ -1,62 +1,62 @@ // SPDX-License-Identifier: GPL-3.0-or-later import Foundation -/// Manages retrieval of verb conjugations for different languages. +// Manages retrieval of verb conjugations for different languages. class ConjugationManager { - static let shared = ConjugationManager() + static let shared = ConjugationManager() - private init() {} + private init() {} - /// Retrieves conjugation data for a given verb and language. - /// - /// - Parameters: - /// - verb: The infinitive form of the verb to conjugate. - /// - language: The language code (e.g., "de", "en"). - /// - Returns nested structure: [TenseTitle: [TypeTitle: [(pronoun, conjugatedForm)]]] - func getConjugations( - verb: String, - language: String - ) -> [(String, [(String, [(String, String)])])]? { - - let contract = ContractManager.shared.loadContract(language: language) - - guard let conjugations = contract.conjugations else { - return nil - } - - var result: [(String, [(String, [(String, String)])])] = [] - - for (_, conjugationSection) in conjugations.sorted(by: { - $0.key < $1.key - }) { - var conjugationTenses: [(String, [(String, String)])] = [] + /// Retrieves conjugation data for a given verb and language. + /// + /// - Parameters: + /// - verb: The infinitive form of the verb to conjugate. + /// - language: The language code (e.g., "de", "en"). + /// - Returns nested structure: [TenseTitle: [TypeTitle: [(pronoun, conjugatedForm)]]] + func getConjugations( + verb: String, + language: String + ) -> [(String, [(String, [(String, String)])])]? { + let contract = ContractManager.shared.loadContract(language: language) + + guard let conjugations = contract.conjugations + else { + return nil + } - for (_, conjugationTense) in conjugationSection.tenses.sorted(by: { - $0.key < $1.key - }) { - var forms: [(String, String)] = [] + var result: [(String, [(String, [(String, String)])])] = [] - for (_, tenseForm) in conjugationTense.tenseForms.sorted(by: { + for (_, conjugationSection) in conjugations.sorted(by: { $0.key < $1.key }) { - let conjugatedForm = queryConjugatedForm( - verb: verb, - columnName: tenseForm.value, - language: language - ) - forms.append((tenseForm.label, conjugatedForm)) + var conjugationTenses: [(String, [(String, String)])] = [] + + for (_, conjugationTense) in conjugationSection.tenses.sorted(by: { + $0.key < $1.key + }) { + var forms: [(String, String)] = [] + + for (_, tenseForm) in conjugationTense.tenseForms.sorted(by: { + $0.key < $1.key + }) { + let conjugatedForm = queryConjugatedForm( + verb: verb, + columnName: tenseForm.value, + language: language + ) + forms.append((tenseForm.label, conjugatedForm)) + } + + conjugationTenses.append((conjugationTense.tenseTitle, forms)) + } + + result.append((conjugationSection.sectionTitle, conjugationTenses)) } - conjugationTenses.append((conjugationTense.tenseTitle, forms)) - } - - result.append((conjugationSection.sectionTitle, conjugationTenses)) + return result.isEmpty ? nil : result } - return result.isEmpty ? nil : result - } - /// Queries the conjugated form for a given verb and column name. /// /// - Parameters: @@ -64,21 +64,19 @@ class ConjugationManager { /// - columnName: The column name to query (may include complex forms). /// - language: The language code. /// - Returns: The conjugated form as a string. - private func queryConjugatedForm( - verb: String, - columnName: String, - language: String - ) -> String { - if columnName.contains("[") { - return parseComplexForm(verb: verb, columnName: columnName, language: language) - } else { - // Simple column query. - let results = LanguageDBManager.shared.queryVerb(of: verb, with: [columnName]) - let result = results.first ?? "" - - return result + private func queryConjugatedForm( + verb: String, + columnName: String, + language: String + ) -> String { + if columnName.contains("[") { + return parseComplexForm(verb: verb, columnName: columnName, language: language) + } else { + // Simple column query. + let results = LanguageDBManager.shared.queryVerb(of: verb, with: [columnName]) + return results.first ?? "" + } } - } /// Parses and retrieves complex verb forms that involve auxiliary verbs. /// @@ -86,60 +84,67 @@ class ConjugationManager { /// - verb: The infinitive form of the verb. /// - columnName: The complex column name containing auxiliary information. /// - language: The language code. - private func parseComplexForm( - verb: String, - columnName: String, - language: String - ) -> String { - // Extract "[auxiliaryPart]" and "mainColumn". - let pattern = "\\[(.*?)\\]" - guard let regex = try? NSRegularExpression(pattern: pattern), - let match = regex.firstMatch(in: columnName, range: NSRange(columnName.startIndex..., in: columnName)) else { - return "" - } - - let auxiliaryPart = (columnName as NSString).substring(with: match.range(at: 1)) - let mainColumn = columnName.replacingOccurrences(of: pattern, with: "", options: .regularExpression) - .trimmingCharacters(in: .whitespaces) - - // Get the main verb form (e.g., "gegangen"). - guard let mainForm = LanguageDBManager.shared.queryVerb(of: verb, with: [mainColumn]).first, - !mainForm.isEmpty else { - return "" - } + private func parseComplexForm( + verb: String, + columnName: String, + language _: String + ) -> String { + // Extract "[auxiliaryPart]" and "mainColumn". + let pattern = "\\[(.*?)\\]" + guard let regex = try? NSRegularExpression(pattern: pattern), + let match = regex.firstMatch( + in: columnName, range: NSRange(columnName.startIndex..., in: columnName) + ) + else { + return "" + } - // Check if it's dynamic lookup (German style: "indicativePresentFirstPersonSingular auxiliaryVerb"). - let auxWords = auxiliaryPart.split(separator: " ") - if auxWords.count > 1 { - let targetForm = String(auxWords.first!) // e.g., "indicativePresentFirstPersonSingular" - let auxColumn = String(auxWords.last!) // e.g., "auxiliaryVerb" - - // Get the auxiliary verb identifier. - if let auxVerbId = LanguageDBManager.shared.queryVerb(of: verb, with: [auxColumn]).first, - !auxVerbId.isEmpty { - - // Try querying by wdLexemeId first. - var auxConjugated = LanguageDBManager.shared.queryVerb( - of: auxVerbId, - identifierColumn: "wdLexemeId", - with: [targetForm] - ).first - - // Fallback: try by infinitive. - if auxConjugated?.isEmpty ?? true { - auxConjugated = LanguageDBManager.shared.queryVerb( - of: auxVerbId, - with: [targetForm] - ).first + let auxiliaryPart = (columnName as NSString).substring(with: match.range(at: 1)) + let mainColumn = columnName.replacingOccurrences( + of: pattern, with: "", options: .regularExpression + ) + .trimmingCharacters(in: .whitespaces) + + // Get the main verb form (e.g., "gegangen"). + guard let mainForm = LanguageDBManager.shared.queryVerb(of: verb, with: [mainColumn]).first, + !mainForm.isEmpty + else { + return "" } - if let auxConjugated = auxConjugated, !auxConjugated.isEmpty { - return "\(auxConjugated) \(mainForm)" + // Check if it's dynamic lookup (German style: "indicativePresentFirstPersonSingular auxiliaryVerb"). + let auxWords = auxiliaryPart.split(separator: " ") + if auxWords.count > 1 { + let targetForm = String(auxWords.first!) // e.g., "indicativePresentFirstPersonSingular" + let auxColumn = String(auxWords.last!) // e.g., "auxiliaryVerb" + + // Get the auxiliary verb identifier. + if let auxVerbId = LanguageDBManager.shared.queryVerb(of: verb, with: [auxColumn]) + .first, + !auxVerbId.isEmpty { + // Try querying by wdLexemeId first. + var auxConjugated = LanguageDBManager.shared.queryVerb( + of: auxVerbId, + identifierColumn: "wdLexemeId", + with: [targetForm] + ).first + + // Fallback: try by infinitive. + if auxConjugated?.isEmpty ?? true { + auxConjugated = + LanguageDBManager.shared.queryVerb( + of: auxVerbId, + with: [targetForm] + ).first + } + + if let auxConjugated = auxConjugated, !auxConjugated.isEmpty { + return "\(auxConjugated) \(mainForm)" + } + } } - } - } - // Fallback: use auxiliary as-is. - return "\(auxiliaryPart) \(mainForm)" - } + // Fallback: use auxiliary as-is. + return "\(auxiliaryPart) \(mainForm)" + } } diff --git a/Keyboards/DataManager/ContractManager.swift b/Keyboards/DataManager/ContractManager.swift index a884a2e4..38810938 100644 --- a/Keyboards/DataManager/ContractManager.swift +++ b/Keyboards/DataManager/ContractManager.swift @@ -2,9 +2,7 @@ import Foundation import Yams -/** - * ContractManager is responsible for loading and caching DataContract instances based on language codes. - */ +/// ContractManager is responsible for loading and caching DataContract instances based on language codes. class ContractManager { static let shared = ContractManager() private var contractCache: [String: DataContract] = [:] @@ -20,10 +18,12 @@ class ContractManager { } // Load YAML file (e.g., "de.yaml", "en.yaml", "es.yaml"). - guard let yamlResourcePath = Bundle.main.path( - forResource: languageCode, - ofType: "yaml" - ) else { + guard + let yamlResourcePath = Bundle.main.path( + forResource: languageCode, + ofType: "yaml" + ) + else { NSLog("Contract not found: \(languageCode).yaml") return createDefaultContract() } @@ -45,7 +45,7 @@ class ContractManager { numbers: nil, genders: nil, conjugations: nil, - declensions: nil, + declensions: nil ) } } diff --git a/Keyboards/DataManager/DataContract.swift b/Keyboards/DataManager/DataContract.swift index aa44fbc0..0eb96a77 100644 --- a/Keyboards/DataManager/DataContract.swift +++ b/Keyboards/DataManager/DataContract.swift @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later import Foundation -/** - * DataContract represents the structure of the data used in the application. - */ +/// DataContract represents the structure of the data used in the application. struct DataContract: Codable { let numbers: [String: String]? let genders: GenderContract? diff --git a/Keyboards/DataManager/DeclensionManager.swift b/Keyboards/DataManager/DeclensionManager.swift index 716334bd..f05b8288 100644 --- a/Keyboards/DataManager/DeclensionManager.swift +++ b/Keyboards/DataManager/DeclensionManager.swift @@ -4,73 +4,80 @@ import Foundation /// Manages loading and parsing of declension contracts. class DeclensionManager { - static let shared = DeclensionManager() + static let shared = DeclensionManager() - private init() {} + private init() {} - /// Loads declension cases for a language. - /// - Parameters: - /// - language: The language code (e.g., "de", "en"). - /// - Returns: Array of navigation levels representing declension cases. - func loadDeclensions(language: String) -> [NavigationLevel]? { - let languageCode = language.lowercased() - let contract = ContractManager.shared.loadContract(language: languageCode) + /// Loads declension cases for a language. + /// - Parameters: + /// - language: The language code (e.g., "de", "en"). + /// - Returns: Array of navigation levels representing declension cases. + func loadDeclensions(language: String) -> [NavigationLevel]? { + let languageCode = language.lowercased() + let contract = ContractManager.shared.loadContract(language: languageCode) - guard let declensionsDict = contract.declensions else { - return nil - } + guard let declensionsDict = contract.declensions + else { + return nil + } + + let sortedKeys = declensionsDict.keys.sorted() - let sortedKeys = declensionsDict.keys.sorted() + var cases: [NavigationLevel] = [] - var cases: [NavigationLevel] = [] + for key in sortedKeys { + guard let declension = declensionsDict[key], + let title = declension.title ?? declension.sectionTitle, + let forms = declension.declensionForms + else { + continue + } - for key in sortedKeys { - guard let declension = declensionsDict[key], - let title = declension.title ?? declension.sectionTitle, - let forms = declension.declensionForms else { - continue - } + let options = parseForms(forms, parentTitle: title) + cases.append(NavigationLevel(title: title, options: options)) + } - let options = parseForms(forms, parentTitle: title) - cases.append(NavigationLevel(title: title, options: options)) + return cases } - return cases - } + /// Recursively parses forms with optional displayValue. + /// - Parameters: + /// - forms: The forms dictionary to parse. + /// - parentTitle: The title of the parent level. + /// - Returns: Array of label-node pairs for navigation. + private func parseForms( + _ forms: [Int: DeclensionNode], + parentTitle _: String + ) -> [(label: String, node: NavigationNode)] { + var options: [(label: String, node: NavigationNode)] = [] - /// Recursively parses forms with optional displayValue. - /// - Parameters: - /// - forms: The forms dictionary to parse. - /// - parentTitle: The title of the parent level. - /// - Returns: Array of label-node pairs for navigation. - private func parseForms( - _ forms: [Int: DeclensionNode], - parentTitle: String - ) -> [(label: String, node: NavigationNode)] { - var options: [(label: String, node: NavigationNode)] = [] + for key in forms.keys.sorted() { + guard let form = forms[key] else { continue } - for key in forms.keys.sorted() { - guard let form = forms[key] else { continue } + if let value = form.value, let label = form.label { + options.append((label: label, node: .finalValue(value))) + } else if let nestedForms = form.declensionForms, let title = form.title { + let displayValue = form.displayValue ?? autoGenerateDisplayValue(from: nestedForms) + let suboptions = parseForms(nestedForms, parentTitle: title) + let nextLevel = NavigationLevel(title: title, options: suboptions) + options.append( + ( + label: form.label ?? title, + node: .nextLevel(nextLevel, displayValue: displayValue) + ) + ) + } + } - if let value = form.value, let label = form.label { - options.append((label: label, node: .finalValue(value))) - } else if let nestedForms = form.declensionForms, let title = form.title { - let displayValue = form.displayValue ?? autoGenerateDisplayValue(from: nestedForms) - let suboptions = parseForms(nestedForms, parentTitle: title) - let nextLevel = NavigationLevel(title: title, options: suboptions) - options.append((label: form.label ?? title, node: .nextLevel(nextLevel, displayValue: displayValue))) - } + return options } - return options - } - - /// Auto-generates display value by joining direct child values. - /// - Parameter forms: The forms dictionary. - /// - Returns: Generated display value string. - private func autoGenerateDisplayValue(from forms: [Int: DeclensionNode]) -> String { - forms.keys.sorted() - .compactMap { forms[$0]?.value } - .joined(separator: "/") - } + /// Auto-generates display value by joining direct child values. + /// - Parameter forms: The forms dictionary. + /// - Returns: Generated display value string. + private func autoGenerateDisplayValue(from forms: [Int: DeclensionNode]) -> String { + forms.keys.sorted() + .compactMap { forms[$0]?.value } + .joined(separator: "/") + } } diff --git a/Keyboards/DataManager/GenderManager.swift b/Keyboards/DataManager/GenderManager.swift index c53feb4a..c85e32a6 100644 --- a/Keyboards/DataManager/GenderManager.swift +++ b/Keyboards/DataManager/GenderManager.swift @@ -3,106 +3,111 @@ import Foundation /// Manages gender query logic based on data contracts. class GenderManager { - static let shared = GenderManager() + static let shared = GenderManager() - private init() {} + private init() {} - struct GenderQueryInfo { - let query: String - let outputCols: [String] - let args: [String] - let fallbackGender: String? - } - - /** - * Builds the appropriate gender queries based on the contract structure - * - Parameters: - * - word: The word to query - * - contract: The data contract defining gender structure - * - Returns: Array of query info objects to execute - */ - func buildGenderQueries(word: String, contract: DataContract) -> [GenderQueryInfo] { - - if hasCanonicalGender(contract) { - if let queryInfo = buildCanonicalQuery(word: word, contract: contract) { - return [queryInfo] - } - } else if hasMasculineFeminine(contract) { - return buildMasculineFeminineQueries(word: word, contract: contract) - } else { - NSLog("GenderManager: No valid gender structure found in contract") + struct GenderQueryInfo { + let query: String + let outputCols: [String] + let args: [String] + let fallbackGender: String? } - return [] - } - - // MARK: Private Helper Methods - - /// Checks if the data contract defines a single, canonical gender column. - private func hasCanonicalGender(_ contract: DataContract) -> Bool { - return contract.genders?.canonical?.first?.isEmpty == false - } - - /// Checks if the data contract defines separate columns for masculine and feminine genders. - private func hasMasculineFeminine(_ contract: DataContract) -> Bool { - let hasMasculine = !(contract.genders?.masculines?.isEmpty ?? true) - let hasFeminine = !(contract.genders?.feminines?.isEmpty ?? true) - return hasMasculine && hasFeminine - } - - /// Builds query for canonical gender structure. - private func buildCanonicalQuery(word: String, contract: DataContract) -> GenderQueryInfo? { - guard let nounCol = contract.numbers?.keys.first, - let genderCol = contract.genders?.canonical?.first else { - NSLog("GenderManager: Missing columns for canonical gender query") - return nil + /** + * Builds the appropriate gender queries based on the contract structure + * - Parameters: + * - word: The word to query + * - contract: The data contract defining gender structure + * - Returns: Array of query info objects to execute + */ + func buildGenderQueries(word: String, contract: DataContract) -> [GenderQueryInfo] { + if hasCanonicalGender(contract) { + if let queryInfo = buildCanonicalQuery(word: word, contract: contract) { + return [queryInfo] + } + } else if hasMasculineFeminine(contract) { + return buildMasculineFeminineQueries(word: word, contract: contract) + } else { + NSLog("GenderManager: No valid gender structure found in contract") + } + + return [] } - let query = """ - SELECT `\(genderCol)` FROM nouns - WHERE `\(nounCol)` = ? OR `\(nounCol)` = ? - """ + // MARK: Private Helper Methods - return GenderQueryInfo( - query: query, - outputCols: [genderCol], - args: [word, word.lowercased()], - fallbackGender: nil - ) - } - - /// Builds queries for masculine/feminine gender structure. - private func buildMasculineFeminineQueries(word: String, contract: DataContract) -> [GenderQueryInfo] { - var queries: [GenderQueryInfo] = [] + /// Checks if the data contract defines a single, canonical gender column. + private func hasCanonicalGender(_ contract: DataContract) -> Bool { + return contract.genders?.canonical?.first?.isEmpty == false + } - // Masculine column query. - if let masculineCol = contract.genders?.masculines?.first { - let query = """ - SELECT `\(masculineCol)` FROM nouns - WHERE `\(masculineCol)` = ? OR `\(masculineCol)` = ? - """ - queries.append(GenderQueryInfo( - query: query, - outputCols: [masculineCol], - args: [word, word.lowercased()], - fallbackGender: "masculine" - )) + /// Checks if the data contract defines separate columns for masculine and feminine genders. + private func hasMasculineFeminine(_ contract: DataContract) -> Bool { + let hasMasculine = !(contract.genders?.masculines?.isEmpty ?? true) + let hasFeminine = !(contract.genders?.feminines?.isEmpty ?? true) + return hasMasculine && hasFeminine } - // Feminine column query. - if let feminineCol = contract.genders?.feminines?.first { - let query = """ - SELECT `\(feminineCol)` FROM nouns - WHERE `\(feminineCol)` = ? OR `\(feminineCol)` = ? - """ - queries.append(GenderQueryInfo( - query: query, - outputCols: [feminineCol], - args: [word, word.lowercased()], - fallbackGender: "feminine" - )) + /// Builds query for canonical gender structure. + private func buildCanonicalQuery(word: String, contract: DataContract) -> GenderQueryInfo? { + guard let nounCol = contract.numbers?.keys.first, + let genderCol = contract.genders?.canonical?.first + else { + NSLog("GenderManager: Missing columns for canonical gender query") + return nil + } + + let query = """ + SELECT `\(genderCol)` FROM nouns + WHERE `\(nounCol)` = ? OR `\(nounCol)` = ? + """ + + return GenderQueryInfo( + query: query, + outputCols: [genderCol], + args: [word, word.lowercased()], + fallbackGender: nil + ) } - return queries - } + /// Builds queries for masculine/feminine gender structure. + private func buildMasculineFeminineQueries(word: String, contract: DataContract) + -> [GenderQueryInfo] { + var queries: [GenderQueryInfo] = [] + + // Masculine column query. + if let masculineCol = contract.genders?.masculines?.first { + let query = """ + SELECT `\(masculineCol)` FROM nouns + WHERE `\(masculineCol)` = ? OR `\(masculineCol)` = ? + """ + queries.append( + GenderQueryInfo( + query: query, + outputCols: [masculineCol], + args: [word, word.lowercased()], + fallbackGender: "masculine" + ) + ) + } + + // Feminine column query. + if let feminineCol = contract.genders?.feminines?.first { + let query = """ + SELECT `\(feminineCol)` FROM nouns + WHERE `\(feminineCol)` = ? OR `\(feminineCol)` = ? + """ + queries.append( + GenderQueryInfo( + query: query, + outputCols: [feminineCol], + args: [word, word.lowercased()], + fallbackGender: "feminine" + ) + ) + } + + return queries + } } diff --git a/Keyboards/DataManager/LanguageDBManager.swift b/Keyboards/DataManager/LanguageDBManager.swift index a85bb1da..9816438f 100644 --- a/Keyboards/DataManager/LanguageDBManager.swift +++ b/Keyboards/DataManager/LanguageDBManager.swift @@ -1,477 +1,496 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/// Functions for loading in data to the keyboards. +// Functions for loading in data to the keyboards. import Foundation import GRDB import SwiftyJSON class LanguageDBManager { - static let shared = LanguageDBManager(translate: false) - static let translations = LanguageDBManager(translate: true) - private var database: DatabaseQueue? - private var cachedHasGrammaticalCase: Bool? - - private init(translate: Bool) { - if translate { - database = openDBQueue("TranslationData") - } else { - database = openDownloadedDBQueue("\(getControllerLanguageAbbr().uppercased())LanguageData") + static let shared = LanguageDBManager(translate: false) + static let translations = LanguageDBManager(translate: true) + private var database: DatabaseQueue? + private var cachedHasGrammaticalCase: Bool? + + private init(translate: Bool) { + if translate { + database = openDBQueue("TranslationData") + } else { + database = openDownloadedDBQueue( + "\(getControllerLanguageAbbr().uppercased())LanguageData" + ) + } } - } - - /// Makes a connection to the language database given the value for controllerLanguage. - private func openDBQueue(_ dbName: String) -> DatabaseQueue { - let dbResourcePath = Bundle.main.path(forResource: dbName, ofType: "sqlite")! - let fileManager = FileManager.default - do { - let dbPath = try fileManager - .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) - .appendingPathComponent("\(dbName).sqlite") - .path - if fileManager.fileExists(atPath: dbPath) { - try fileManager.removeItem(atPath: dbPath) - } - try fileManager.copyItem(atPath: dbResourcePath, toPath: dbPath) - let dbQueue = try DatabaseQueue(path: dbPath) - return dbQueue - } catch { - print("An error occurred: UILexicon not available") - let dbQueue = try! DatabaseQueue(path: dbResourcePath) - return dbQueue + + /// Makes a connection to the language database given the value for controllerLanguage. + private func openDBQueue(_ dbName: String) -> DatabaseQueue { + let dbResourcePath = Bundle.main.path(forResource: dbName, ofType: "sqlite")! + let fileManager = FileManager.default + do { + let dbPath = + try fileManager + .url( + for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, + create: true + ) + .appendingPathComponent("\(dbName).sqlite") + .path + if fileManager.fileExists(atPath: dbPath) { + try fileManager.removeItem(atPath: dbPath) + } + try fileManager.copyItem(atPath: dbResourcePath, toPath: dbPath) + return try DatabaseQueue(path: dbPath) + } catch { + print("An error occurred: UILexicon not available") + return try! DatabaseQueue(path: dbResourcePath) + } } - } - - /// Opens a connection to the downloaded language database for the current language. - private func openDownloadedDBQueue(_ dbName: String) -> DatabaseQueue? { - let fileManager = FileManager.default - guard let containerURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.be.scri.userDefaultsContainer") else { - print("App group container not found") - return nil + + /// Opens a connection to the downloaded language database for the current language. + private func openDownloadedDBQueue(_ dbName: String) -> DatabaseQueue? { + let fileManager = FileManager.default + guard + let containerURL = fileManager.containerURL( + forSecurityApplicationGroupIdentifier: "group.be.scri.userDefaultsContainer" + ) + else { + print("App group container not found") + return nil + } + let dbPath = containerURL.appendingPathComponent("\(dbName).sqlite").path + guard fileManager.fileExists(atPath: dbPath) + else { + print("\(dbName).sqlite not found in app group container") + return nil + } + return try? DatabaseQueue(path: dbPath) } - let dbPath = containerURL.appendingPathComponent("\(dbName).sqlite").path - guard fileManager.fileExists(atPath: dbPath) else { - print("\(dbName).sqlite not found in app group container") - return nil + + /// Loads a JSON file that contains grammatical information into a dictionary. + /// + /// - Parameters + /// - filename: the name of the JSON file to be loaded. + func loadJSON(filename fileName: String) -> JSON { + let url = Bundle.main.url(forResource: fileName, withExtension: "json")! + let data = NSData(contentsOf: url) + return try! JSON(data: data! as Data) } - return try? DatabaseQueue(path: dbPath) -} - /// Loads a JSON file that contains grammatical information into a dictionary. - /// - /// - Parameters - /// - filename: the name of the JSON file to be loaded. - func loadJSON(filename fileName: String) -> JSON { - let url = Bundle.main.url(forResource: fileName, withExtension: "json")! - let data = NSData(contentsOf: url) - let jsonData = try! JSON(data: data! as Data) - return jsonData - } - - /// Returns a row from the language database given a query and arguments. - /// - /// - Parameters - /// - query: the query to run against the language database. - /// - outputCols: the columns from which the values should come. - /// - args: arguments to pass to `query`. - private func queryDBRow(query: String, outputCols: [String], args: StatementArguments) -> [String] { - var outputValues = [String]() - do { - try database?.read { db in - if let row = try Row.fetchOne(db, sql: query, arguments: args) { - for col in outputCols { - if let stringValue = row[col] as? String { - outputValues.append(stringValue) - } else { - outputValues.append("") // default to empty string if NULL or wrong type + /// Returns a row from the language database given a query and arguments. + /// + /// - Parameters + /// - query: the query to run against the language database. + /// - outputCols: the columns from which the values should come. + /// - args: arguments to pass to `query`. + private func queryDBRow(query: String, outputCols: [String], args: StatementArguments) + -> [String] { + var outputValues = [String]() + do { + try database?.read { db in + if let row = try Row.fetchOne(db, sql: query, arguments: args) { + for col in outputCols { + if let stringValue = row[col] as? String { + outputValues.append(stringValue) + } else { + outputValues.append("") // default to empty string if NULL or wrong type + } + } + } } - } + } catch let error as DatabaseError { + let errorMessage = error.message + let errorSQL = error.sql + let errorArguments = error.arguments + print( + "An error '\(String(describing: errorMessage))' occurred in the query: \(String(describing: errorSQL)) (\(String(describing: errorArguments)))" + ) + } catch {} + + if outputValues.isEmpty { + // Append an empty string so that we can check for it and trigger commandState = .invalid. + outputValues.append("") } - } - } catch let error as DatabaseError { - let errorMessage = error.message - let errorSQL = error.sql - let errorArguments = error.arguments - print( - "An error '\(String(describing: errorMessage))' occurred in the query: \(String(describing: errorSQL)) (\(String(describing: errorArguments)))" - ) - } catch {} - - if outputValues.isEmpty { - // Append an empty string so that we can check for it and trigger commandState = .invalid. - outputValues.append("") + + return outputValues } - return outputValues - } - - /// Returns rows from the language database given a query and arguments. - /// - /// - Parameters: - /// - query: the query to run against the language database. - /// - outputCols: the columns from which the values should come. - /// - args: arguments to pass to `query`. - private func queryDBRows(query: String, outputCols: [String], args: StatementArguments) -> [String] { - var outputValues = [String]() - do { - guard let languageDB = database else { return [] } - let rows = try languageDB.read { db in - try Row.fetchAll(db, sql: query, arguments: args) - } - for r in rows { - // Loop through all columns. - for col in outputCols { - if let value = r[col] as? String, - !value.trimmingCharacters(in: .whitespaces).isEmpty { - outputValues.append(value) + /// Returns rows from the language database given a query and arguments. + /// + /// - Parameters: + /// - query: the query to run against the language database. + /// - outputCols: the columns from which the values should come. + /// - args: arguments to pass to `query`. + private func queryDBRows(query: String, outputCols: [String], args: StatementArguments) + -> [String] { + var outputValues = [String]() + do { + guard let languageDB = database else { return [] } + let rows = try languageDB.read { db in + try Row.fetchAll(db, sql: query, arguments: args) + } + for r in rows { + // Loop through all columns. + for col in outputCols { + if let value = r[col] as? String, + !value.trimmingCharacters(in: .whitespaces).isEmpty { + outputValues.append(value) + } } } + } catch let error as DatabaseError { + let errorMessage = error.message + let errorSQL = error.sql + let errorArguments = error.arguments + print( + "An error '\(String(describing: errorMessage))' occurred in the query: \(String(describing: errorSQL)) (\(String(describing: errorArguments)))" + ) + } catch {} + + if outputValues == [String]() { + // Append an empty string so that we can check for it and trigger commandState = .invalid. + outputValues.append("") } - } catch let error as DatabaseError { - let errorMessage = error.message - let errorSQL = error.sql - let errorArguments = error.arguments - print( - "An error '\(String(describing: errorMessage))' occurred in the query: \(String(describing: errorSQL)) (\(String(describing: errorArguments)))" - ) - } catch {} - - if outputValues == [String]() { - // Append an empty string so that we can check for it and trigger commandState = .invalid. - outputValues.append("") + + return outputValues } - return outputValues - } - - /// Writes a row of a language database table given a query and arguments. - /// - /// - Parameters - /// - query: the query to run against the language database. - /// - args: arguments to pass to `query`. - private func writeDBRow(query: String, args: StatementArguments) { - do { - try database?.write { db in - try db.execute(sql: query, arguments: args) - } - } catch let error as DatabaseError { - let errorMessage = error.message - let errorSQL = error.sql - let errorArguments = error.arguments - print( - "An error '\(String(describing: errorMessage))' occurred in the query: \(String(describing: errorSQL)) (\(String(describing: errorArguments)))" - ) - } catch {} - } - - /// Deletes rows from the language database given a query and arguments. - /// - /// - Parameters: - /// - query: the query to run against the language database. - /// - args: arguments to pass to `query`. - private func deleteDBRow(query: String, args: StatementArguments? = nil) { - do { - try database?.write { db in - guard let args = args else { - try db.execute(sql: query) - return - } - try db.execute(sql: query, arguments: args) - } - } catch let error as DatabaseError { - let errorMessage = error.message - let errorSQL = error.sql - print( - "An error '\(String(describing: errorMessage))' occurred in the query: \(String(describing: errorSQL))" - ) - } catch {} - } + /// Writes a row of a language database table given a query and arguments. + /// + /// - Parameters + /// - query: the query to run against the language database. + /// - args: arguments to pass to `query`. + private func writeDBRow(query: String, args: StatementArguments) { + do { + try database?.write { db in + try db.execute(sql: query, arguments: args) + } + } catch let error as DatabaseError { + let errorMessage = error.message + let errorSQL = error.sql + let errorArguments = error.arguments + print( + "An error '\(String(describing: errorMessage))' occurred in the query: \(String(describing: errorSQL)) (\(String(describing: errorArguments)))" + ) + } catch {} + } + + /// Deletes rows from the language database given a query and arguments. + /// + /// - Parameters: + /// - query: the query to run against the language database. + /// - args: arguments to pass to `query`. + private func deleteDBRow(query: String, args: StatementArguments? = nil) { + do { + try database?.write { db in + guard let args = args + else { + try db.execute(sql: query) + return + } + try db.execute(sql: query, arguments: args) + } + } catch let error as DatabaseError { + let errorMessage = error.message + let errorSQL = error.sql + print( + "An error '\(String(describing: errorMessage))' occurred in the query: \(String(describing: errorSQL))" + ) + } catch {} + } } // MARK: Database operations extension LanguageDBManager { - /// Delete non-unique values in case the lexicon has added words that were already present. - func deleteNonUniqueAutocompletions() { - let query = """ - DELETE FROM - autocomplete_lexicon - - WHERE rowid NOT IN ( - SELECT - MIN(rowid) - - FROM - autocomplete_lexicon - - GROUP BY - word - ) - """ - - deleteDBRow(query: query) - } - - /// Add words to autocompletions. - func insertAutocompleteLexicon(of word: String) { - let query = """ - INSERT OR IGNORE INTO - autocomplete_lexicon (word) - - VALUES (?) - """ - let args = [word] - - writeDBRow(query: query, args: StatementArguments(args)) - } - - /// Returns the next three words in the `autocomplete_lexicon` that follow a given word. - /// - /// - Parameters - /// - word: the word that autosuggestions should be returned for. - func queryAutocompletions(word: String) -> [String] { - let autocompletionsQuery = """ - SELECT - word - - FROM - autocomplete_lexicon - - WHERE - LOWER(word) LIKE ? - - ORDER BY - word COLLATE NOCASE ASC - - LIMIT - 3 - """ - let outputCols = ["word"] - let args = ["\(word.lowercased())%"] - - return queryDBRows(query: autocompletionsQuery, outputCols: outputCols, args: StatementArguments(args)) - } - - /// Query the suggestion of word in `autosuggestions`. - func queryAutosuggestions(of word: String) -> [String] { - let query = """ - SELECT - * - - FROM - autosuggestions - - WHERE - word = ? - """ - let args = [word] - let outputCols = ["autosuggestion_0", "autosuggestion_1", "autosuggestion_2"] - - return queryDBRow(query: query, outputCols: outputCols, args: StatementArguments(args)) - } - - /// Query emojis of word in `emoji_keywords`. - func queryEmojis(of word: String) -> [String] { - let query = """ - SELECT - * - - FROM - emoji_keywords - - WHERE - word = ? - """ - let outputCols = ["emoji_keyword_0", "emoji_keyword_1", "emoji_keyword_2"] - let args = [word] - - return queryDBRow(query: query, outputCols: outputCols, args: StatementArguments(args)) - } - - /// Query the noun form of word in `nonuns`. - func queryNounForm(of word: String) -> [String] { - let language = getControllerLanguageAbbr() - let contract = ContractManager.shared.loadContract(language: language) - - var allGenders: [String] = [] - - let queries = GenderManager.shared.buildGenderQueries(word: word, contract: contract) - - for queryInfo in queries { - let results = queryDBRows( - query: queryInfo.query, - outputCols: queryInfo.outputCols, - args: StatementArguments(queryInfo.args) - ) - - // For canonical gender: results are the actual genders. - if queryInfo.fallbackGender == nil { - for gender in results where !gender.isEmpty { - if !allGenders.contains(gender) { - allGenders.append(gender) - } - } - } - // For masculine/feminine: if any result, use fallback. - else { - if !results.isEmpty && !results[0].isEmpty, - let fallback = queryInfo.fallbackGender, - !allGenders.contains(fallback) { - allGenders.append(fallback) - } - } + /// Delete non-unique values in case the lexicon has added words that were already present. + func deleteNonUniqueAutocompletions() { + let query = """ + DELETE FROM + autocomplete_lexicon + + WHERE rowid NOT IN ( + SELECT + MIN(rowid) + + FROM + autocomplete_lexicon + + GROUP BY + word + ) + """ + + deleteDBRow(query: query) + } + + /// Add words to autocompletions. + func insertAutocompleteLexicon(of word: String) { + let query = """ + INSERT OR IGNORE INTO + autocomplete_lexicon (word) + + VALUES (?) + """ + let args = [word] + + writeDBRow(query: query, args: StatementArguments(args)) + } + + /// Returns the next three words in the `autocomplete_lexicon` that follow a given word. + /// + /// - Parameters + /// - word: the word that autosuggestions should be returned for. + func queryAutocompletions(word: String) -> [String] { + let autocompletionsQuery = """ + SELECT + word + + FROM + autocomplete_lexicon + + WHERE + LOWER(word) LIKE ? + + ORDER BY + word COLLATE NOCASE ASC + + LIMIT + 3 + """ + let outputCols = ["word"] + let args = ["\(word.lowercased())%"] + + return queryDBRows( + query: autocompletionsQuery, outputCols: outputCols, args: StatementArguments(args) + ) + } + + /// Query the suggestion of word in `autosuggestions`. + func queryAutosuggestions(of word: String) -> [String] { + let query = """ + SELECT + * + + FROM + autosuggestions + + WHERE + word = ? + """ + let args = [word] + let outputCols = ["autosuggestion_0", "autosuggestion_1", "autosuggestion_2"] + + return queryDBRow(query: query, outputCols: outputCols, args: StatementArguments(args)) } - return allGenders.isEmpty ? [""] : [allGenders.joined(separator: "/")] - } - - /// Query the plural form of word in `nouns`. - func queryNounPlural(of word: String) -> [String] { - let language = getControllerLanguageAbbr() - let contract = ContractManager.shared.loadContract(language: language) - - let queryInfos = PluralManager.shared.buildPluralQuery( - word: word, - contract: contract - ) - - // Try each query until we find a result. - for queryInfo in queryInfos { - let result = queryDBRow( - query: queryInfo.query, - outputCols: queryInfo.outputCols, - args: StatementArguments(queryInfo.args) - ) - - // If we found a result, return it. - if result.count >= 2 && !result[1].isEmpty { - return [result[1]] - } + /// Query emojis of word in `emoji_keywords`. + func queryEmojis(of word: String) -> [String] { + let query = """ + SELECT + * + + FROM + emoji_keywords + + WHERE + word = ? + """ + let outputCols = ["emoji_keyword_0", "emoji_keyword_1", "emoji_keyword_2"] + let args = [word] + + return queryDBRow(query: query, outputCols: outputCols, args: StatementArguments(args)) } - return [] - } + /// Query the noun form of word in `nonuns`. + func queryNounForm(of word: String) -> [String] { + let language = getControllerLanguageAbbr() + let contract = ContractManager.shared.loadContract(language: language) + + var allGenders: [String] = [] + + let queries = GenderManager.shared.buildGenderQueries(word: word, contract: contract) + + for queryInfo in queries { + let results = queryDBRows( + query: queryInfo.query, + outputCols: queryInfo.outputCols, + args: StatementArguments(queryInfo.args) + ) + + // For canonical gender: results are the actual genders. + if queryInfo.fallbackGender == nil { + for gender in results where !gender.isEmpty { + if !allGenders.contains(gender) { + allGenders.append(gender) + } + } + } + // For masculine/feminine: if any result, use fallback. + else { + if !results.isEmpty, !results[0].isEmpty, + let fallback = queryInfo.fallbackGender, + !allGenders.contains(fallback) { + allGenders.append(fallback) + } + } + } + + return allGenders.isEmpty ? [""] : [allGenders.joined(separator: "/")] + } - /// Query all plural forms for the current language. - func queryAllPluralForms() -> [String]? { - let language = getControllerLanguageAbbr() - let contract = ContractManager.shared.loadContract(language: language) + /// Query the plural form of word in `nouns`. + func queryNounPlural(of word: String) -> [String] { + let language = getControllerLanguageAbbr() + let contract = ContractManager.shared.loadContract(language: language) + + let queryInfos = PluralManager.shared.buildPluralQuery( + word: word, + contract: contract + ) + + // Try each query until we find a result. + for queryInfo in queryInfos { + let result = queryDBRow( + query: queryInfo.query, + outputCols: queryInfo.outputCols, + args: StatementArguments(queryInfo.args) + ) + + // If we found a result, return it. + if result.count >= 2, !result[1].isEmpty { + return [result[1]] + } + } - guard let queryInfo = PluralManager.shared.buildAllPluralsQuery(contract: contract) else { - return nil + return [] } - let result = queryDBRows( - query: queryInfo.query, - outputCols: queryInfo.outputCols, - args: StatementArguments() - ) + /// Query all plural forms for the current language. + func queryAllPluralForms() -> [String]? { + let language = getControllerLanguageAbbr() + let contract = ContractManager.shared.loadContract(language: language) - return result == [""] ? nil : result - } + guard let queryInfo = PluralManager.shared.buildAllPluralsQuery(contract: contract) + else { + return nil + } - /// Query preposition form of word in `prepositions`. - func queryPrepForm(of word: String) -> [String] { - // Check if this language's database has grammaticalCase column. - guard hasGrammaticalCaseColumn() else { - return [""] + let result = queryDBRows( + query: queryInfo.query, + outputCols: queryInfo.outputCols, + args: StatementArguments() + ) + + return result == [""] ? nil : result } - let query = """ - SELECT grammaticalCase - FROM prepositions - WHERE preposition = ? - """ - let outputCols = ["grammaticalCase"] - let args = [word] - - return queryDBRow(query: query, outputCols: outputCols, args: StatementArguments(args)) - } - - /// Check if the prepositions table has a grammaticalCase column. - /// Result is cached to avoid repeated PRAGMA queries. - private func hasGrammaticalCaseColumn() -> Bool { - if let cached = cachedHasGrammaticalCase { - return cached + /// Query preposition form of word in `prepositions`. + func queryPrepForm(of word: String) -> [String] { + // Check if this language's database has grammaticalCase column. + guard hasGrammaticalCaseColumn() + else { + return [""] + } + + let query = """ + SELECT grammaticalCase + FROM prepositions + WHERE preposition = ? + """ + let outputCols = ["grammaticalCase"] + let args = [word] + + return queryDBRow(query: query, outputCols: outputCols, args: StatementArguments(args)) } - var hasColumn = false + /// Check if the prepositions table has a grammaticalCase column. + /// Result is cached to avoid repeated PRAGMA queries. + private func hasGrammaticalCaseColumn() -> Bool { + if let cached = cachedHasGrammaticalCase { + return cached + } + + var hasColumn = false - do { - try database?.read { db in - let columns = try Row.fetchAll(db, sql: "PRAGMA table_info(prepositions)") - hasColumn = columns.contains { row in - (row["name"] as? String) == "grammaticalCase" + do { + try database?.read { db in + let columns = try Row.fetchAll(db, sql: "PRAGMA table_info(prepositions)") + hasColumn = columns.contains { row in + (row["name"] as? String) == "grammaticalCase" + } + } + } catch { + hasColumn = false } - } - } catch { - hasColumn = false + + cachedHasGrammaticalCase = hasColumn + return hasColumn + } + + /// Query the translation of word in the current language. Only works with the `translations` manager. + func queryTranslation(of word: String) -> [String] { + let translateLanguage = getKeyInDict( + givenValue: getControllerTranslateLangCode(), dict: languagesAbbrDict + ) + let query = """ + SELECT + * + + FROM + \(translateLanguage) + + WHERE + word = ? + """ + let outputCols = [getControllerLanguageAbbr()] + let args = [word] + + return queryDBRow(query: query, outputCols: outputCols, args: StatementArguments(args)) + } + + /// Query the verb form of word in `verbs`. + /// - Parameters: + /// - word: The value to search for. + func queryVerb(of word: String) -> [String] { + let columnName = (controllerLanguage == "Swedish") ? "verb" : "infinitive" + let query = """ + SELECT + * + + FROM + verbs + + WHERE + \(columnName) = ? + """ + let outputCols = [columnName] + let args = [word] + + return queryDBRow(query: query, outputCols: outputCols, args: StatementArguments(args)) } - cachedHasGrammaticalCase = hasColumn - return hasColumn - } - - /// Query the translation of word in the current language. Only works with the `translations` manager. - func queryTranslation(of word: String) -> [String] { - let translateLanguage = getKeyInDict(givenValue: getControllerTranslateLangCode(), dict: languagesAbbrDict) - let query = """ - SELECT - * - - FROM - \(translateLanguage) - - WHERE - word = ? - """ - let outputCols = [getControllerLanguageAbbr()] - let args = [word] - - return queryDBRow(query: query, outputCols: outputCols, args: StatementArguments(args)) - } - - /// Query the verb form of word in `verbs`. - /// - Parameters: - /// - word: The value to search for. - func queryVerb(of word: String) -> [String] { - let columnName = (controllerLanguage == "Swedish") ? "verb" : "infinitive" - let query = """ - SELECT - * - - FROM - verbs - - WHERE - \(columnName) = ? - """ - let outputCols = [columnName] - let args = [word] - - return queryDBRow(query: query, outputCols: outputCols, args: StatementArguments(args)) - } - - /// Query specific form of word in `verbs`. - /// - /// - Parameters: - /// - word: The value to search for - /// - identifierColumn: The column to search in (default: "infinitive" or "verb" for Swedish) - /// - outputCols: Specific form want to output - func queryVerb(of word: String, identifierColumn: String? = nil, with outputCols: [String]) -> [String] { - let columnName = identifierColumn ?? ((controllerLanguage == "Swedish") ? "verb" : "infinitive") - let query = """ - SELECT - * - - FROM - verbs - - WHERE - \(columnName) = ? - """ - let args = [word] - - return queryDBRow(query: query, outputCols: outputCols, args: StatementArguments(args)) - } + /// Query specific form of word in `verbs`. + /// + /// - Parameters: + /// - word: The value to search for + /// - identifierColumn: The column to search in (default: "infinitive" or "verb" for Swedish) + /// - outputCols: Specific form want to output + func queryVerb(of word: String, identifierColumn: String? = nil, with outputCols: [String]) + -> [String] { + let columnName = + identifierColumn ?? ((controllerLanguage == "Swedish") ? "verb" : "infinitive") + let query = """ + SELECT + * + + FROM + verbs + + WHERE + \(columnName) = ? + """ + let args = [word] + + return queryDBRow(query: query, outputCols: outputCols, args: StatementArguments(args)) + } } diff --git a/Keyboards/DataManager/PluralManager.swift b/Keyboards/DataManager/PluralManager.swift index 4ac03d12..5082105f 100644 --- a/Keyboards/DataManager/PluralManager.swift +++ b/Keyboards/DataManager/PluralManager.swift @@ -3,72 +3,76 @@ import Foundation /// Manages plural query logic based on data contracts class PluralManager { - static let shared = PluralManager() + static let shared = PluralManager() - private init() {} + private init() {} - struct PluralQueryInfo { - let query: String - let outputCols: [String] - let args: [String] - } - - /** - * Builds a query to find the plural form of a word based on the contract structure - * - Parameters: - * - word: The singular word to find the plural for - * - contract: The data contract defining the database structure - * - Returns: Query info to execute, or nil if contract is invalid - */ - func buildPluralQuery(word: String, contract: DataContract?) -> [PluralQueryInfo] { // ← Return array! - guard let contract = contract, - let numbers = contract.numbers, - !numbers.isEmpty else { - NSLog("PluralManager: No valid plural columns in contract") - return [] + struct PluralQueryInfo { + let query: String + let outputCols: [String] + let args: [String] } - var queries: [PluralQueryInfo] = [] + /** + * Builds a query to find the plural form of a word based on the contract structure + * - Parameters: + * - word: The singular word to find the plural for + * - contract: The data contract defining the database structure + * - Returns: Query info to execute, or nil if contract is invalid + */ + func buildPluralQuery(word: String, contract: DataContract?) -> [PluralQueryInfo] { // ← Return array! + guard let contract = contract, + let numbers = contract.numbers, + !numbers.isEmpty + else { + NSLog("PluralManager: No valid plural columns in contract") + return [] + } + + var queries: [PluralQueryInfo] = [] - // Build a query for EACH singular/plural pair. - for (singularCol, pluralCol) in numbers { - let query = """ + // Build a query for EACH singular/plural pair. + for (singularCol, pluralCol) in numbers { + let query = """ SELECT `\(singularCol)`, `\(pluralCol)` FROM nouns WHERE `\(singularCol)` = ? COLLATE NOCASE """ - queries.append(PluralQueryInfo( - query: query, - outputCols: [singularCol, pluralCol], - args: [word.lowercased()] - )) - } - - return queries - } + queries.append( + PluralQueryInfo( + query: query, + outputCols: [singularCol, pluralCol], + args: [word.lowercased()] + ) + ) + } - /** - * Builds a query to get all plural forms for a language - * - Parameter contract: The data contract defining plural columns. - * - Returns: Query info to execute, or nil if contract is invalid. - */ - func buildAllPluralsQuery(contract: DataContract?) -> PluralQueryInfo? { - guard let contract = contract, - let numbers = contract.numbers, - !numbers.isEmpty else { - NSLog("PluralManager: No valid plural columns in contract") - return nil + return queries } - let pluralColumns = Array(numbers.values) - let columns = pluralColumns.map { "`\($0)`" }.joined(separator: ", ") - let query = "SELECT \(columns) FROM nouns" + /** + * Builds a query to get all plural forms for a language + * - Parameter contract: The data contract defining plural columns. + * - Returns: Query info to execute, or nil if contract is invalid. + */ + func buildAllPluralsQuery(contract: DataContract?) -> PluralQueryInfo? { + guard let contract = contract, + let numbers = contract.numbers, + !numbers.isEmpty + else { + NSLog("PluralManager: No valid plural columns in contract") + return nil + } + + let pluralColumns = Array(numbers.values) + let columns = pluralColumns.map { "`\($0)`" }.joined(separator: ", ") + let query = "SELECT \(columns) FROM nouns" - return PluralQueryInfo( - query: query, - outputCols: pluralColumns, - args: [] - ) - } + return PluralQueryInfo( + query: query, + outputCols: pluralColumns, + args: [] + ) + } } diff --git a/Keyboards/InterfaceConstants.swift b/Keyboards/InterfaceConstants.swift index 7dabcfa4..9b7f0534 100644 --- a/Keyboards/InterfaceConstants.swift +++ b/Keyboards/InterfaceConstants.swift @@ -1,10 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Constants for the Scribe keyboard interfaces. */ enum SpecialKeys { - static let indent = "indent" - static let capsLock = "capslock" + static let indent = "indent" + static let capsLock = "capslock" } diff --git a/Keyboards/KeyboardBuilder.swift b/Keyboards/KeyboardBuilder.swift index 4b0e3331..b81e94f9 100644 --- a/Keyboards/KeyboardBuilder.swift +++ b/Keyboards/KeyboardBuilder.swift @@ -1,43 +1,43 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Building a keyboard layout using a builder pattern. */ import Foundation protocol KeyboardBuilderProtocol { - func addRow(_ row: [String]) -> KeyboardBuilder - func build() -> [[String]] - func replaceKey(row: Int, column: Int, to newKey: String) -> KeyboardBuilder - func replaceKey(form oldKey: String, to newKey: String) -> KeyboardBuilder + func addRow(_ row: [String]) -> KeyboardBuilder + func build() -> [[String]] + func replaceKey(row: Int, column: Int, to newKey: String) -> KeyboardBuilder + func replaceKey(form oldKey: String, to newKey: String) -> KeyboardBuilder } class KeyboardBuilder: KeyboardBuilderProtocol { - private var rows: [[String]] = [] + private var rows: [[String]] = [] - func addRow(_ row: [String]) -> KeyboardBuilder { - rows.append(row) - return self - } + func addRow(_ row: [String]) -> KeyboardBuilder { + rows.append(row) + return self + } - func build() -> [[String]] { - return rows - } + func build() -> [[String]] { + return rows + } - func replaceKey(row: Int, column: Int, to newKey: String) -> KeyboardBuilder { - guard row < rows.count && column < rows[row].count else { return self } - rows[row][column] = newKey + func replaceKey(row: Int, column: Int, to newKey: String) -> KeyboardBuilder { + guard row < rows.count && column < rows[row].count else { return self } + rows[row][column] = newKey - return self - } + return self + } - func replaceKey(form oldKey: String, to newKey: String) -> KeyboardBuilder { - for (rowIndex, row) in rows.enumerated() { - if let symbolIndex = row.firstIndex(of: oldKey) { - rows[rowIndex][symbolIndex] = newKey + func replaceKey(form oldKey: String, to newKey: String) -> KeyboardBuilder { + for (rowIndex, row) in rows.enumerated() { + if let symbolIndex = row.firstIndex(of: oldKey) { + rows[rowIndex][symbolIndex] = newKey + } } + return self } - return self - } } diff --git a/Keyboards/KeyboardProvider.swift b/Keyboards/KeyboardProvider.swift index c82a3952..4537c40e 100644 --- a/Keyboards/KeyboardProvider.swift +++ b/Keyboards/KeyboardProvider.swift @@ -1,26 +1,26 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Define protocol for keyboard provider. */ import Foundation protocol KeyboardProviderProtocol { - static func genPhoneLetterKeys() -> [[String]] - static func genPhoneNumberKeys(currencyKey: String) -> [[String]] - static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] + static func genPhoneLetterKeys() -> [[String]] + static func genPhoneNumberKeys(currencyKey: String) -> [[String]] + static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] - static func genPadLetterKeys() -> [[String]] - static func genPadNumberKeys(currencyKey: String) -> [[String]] - static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] + static func genPadLetterKeys() -> [[String]] + static func genPadNumberKeys(currencyKey: String) -> [[String]] + static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] - static func genPadExpandedLetterKeys() -> [[String]] - static func genPadExpandedSymbolKeys() -> [[String]] + static func genPadExpandedLetterKeys() -> [[String]] + static func genPadExpandedSymbolKeys() -> [[String]] } protocol KeyboardProviderDisableAccentsProtocol { - static func genPhoneDisableAccentsLetterKeys() -> [[String]] - static func genPadDisableAccentsLetterKeys() -> [[String]] - static func genPadExpandedDisableAccentsLetterKeys() -> [[String]] + static func genPhoneDisableAccentsLetterKeys() -> [[String]] + static func genPadDisableAccentsLetterKeys() -> [[String]] + static func genPadExpandedDisableAccentsLetterKeys() -> [[String]] } diff --git a/Keyboards/KeyboardsBase/DynamicConjugationViewController.swift b/Keyboards/KeyboardsBase/DynamicConjugationViewController.swift index a3f7cf66..95b2b4ff 100644 --- a/Keyboards/KeyboardsBase/DynamicConjugationViewController.swift +++ b/Keyboards/KeyboardsBase/DynamicConjugationViewController.swift @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/// Dynamic view controller for arbitrary-depth navigation. -/// Handles conjugations and declensions. +// Dynamic view controller for arbitrary-depth navigation. +// Handles conjugations and declensions. import UIKit @@ -15,43 +15,44 @@ class DynamicConjugationViewController: UIViewController { private var buttonContainerView: UIView! private var pageControl: UIPageControl? - // MARK: Navigation Data + // MARK: Navigation Data - // For tree navigation (conjugations and variant declensions). - private var navigationStack: [NavigationLevel] = [] + /// For tree navigation (conjugations and variant declensions). + private var navigationStack: [NavigationLevel] = [] - // For linear navigation (declension cases). - private var linearCases: [NavigationLevel]? - private var currentCaseIndex: Int = 0 + // For linear navigation (declension cases). + private var linearCases: [NavigationLevel]? + private var currentCaseIndex: Int = 0 - private weak var commandBar: CommandBar? + private weak var commandBar: CommandBar? - // MARK: Initialization + // MARK: Initialization - // Tree navigation (conjugations). - init(navigationTree: NavigationLevel, commandBar: CommandBar) { - self.commandBar = commandBar - super.init(nibName: nil, bundle: nil) - navigationStack = [navigationTree] - } + /// Tree navigation (conjugations). + init(navigationTree: NavigationLevel, commandBar: CommandBar) { + self.commandBar = commandBar + super.init(nibName: nil, bundle: nil) + navigationStack = [navigationTree] + } - // Linear navigation (declensions). - init(linearCases: [NavigationLevel], commandBar: CommandBar, startingIndex: Int = 0) { - self.commandBar = commandBar - self.linearCases = linearCases - self.currentCaseIndex = startingIndex - super.init(nibName: nil, bundle: nil) + /// Linear navigation (declensions). + init(linearCases: [NavigationLevel], commandBar: CommandBar, startingIndex: Int = 0) { + self.commandBar = commandBar + self.linearCases = linearCases + currentCaseIndex = startingIndex + super.init(nibName: nil, bundle: nil) - if !linearCases.isEmpty && startingIndex < linearCases.count { - navigationStack = [linearCases[startingIndex]] + if !linearCases.isEmpty, startingIndex < linearCases.count { + navigationStack = [linearCases[startingIndex]] + } } - } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - // MARK: Lifecycle + // MARK: Lifecycle override func viewDidLoad() { super.viewDidLoad() @@ -60,66 +61,70 @@ class DynamicConjugationViewController: UIViewController { setupPageControl() } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() - if buttonContainerView.subviews.isEmpty { - displayCurrentLevel() + if buttonContainerView.subviews.isEmpty { + displayCurrentLevel() + } } - } - - // MARK: Setup - - /// Sets up the UI components. - private func setupUI() { - buttonContainerView = UIView() - buttonContainerView.backgroundColor = .clear - buttonContainerView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(buttonContainerView) - - leftArrowButton = UIButton(type: .system) - leftArrowButton.setImage(UIImage(systemName: "chevron.left"), for: .normal) - leftArrowButton.tintColor = keyCharColor - leftArrowButton.backgroundColor = keyColor - leftArrowButton.layer.cornerRadius = keyCornerRadius - leftArrowButton.layer.shadowColor = keyShadowColor - leftArrowButton.layer.shadowOffset = CGSize(width: 0, height: 1) - leftArrowButton.layer.shadowOpacity = 1.0 - leftArrowButton.layer.shadowRadius = 0 - leftArrowButton.addTarget(self, action: #selector(leftArrowTapped), for: .touchUpInside) - leftArrowButton.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(leftArrowButton) - - rightArrowButton = UIButton(type: .system) - rightArrowButton.setImage(UIImage(systemName: "chevron.right"), for: .normal) - rightArrowButton.tintColor = keyCharColor - rightArrowButton.backgroundColor = keyColor - rightArrowButton.layer.cornerRadius = keyCornerRadius - rightArrowButton.layer.shadowColor = keyShadowColor - rightArrowButton.layer.shadowOffset = CGSize(width: 0, height: 1) - rightArrowButton.layer.shadowOpacity = 1.0 - rightArrowButton.layer.shadowRadius = 0 - rightArrowButton.addTarget(self, action: #selector(rightArrowTapped), for: .touchUpInside) - rightArrowButton.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(rightArrowButton) - NSLayoutConstraint.activate([ - leftArrowButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8), - leftArrowButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 8), - leftArrowButton.widthAnchor.constraint(equalToConstant: 40), - leftArrowButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8), - - rightArrowButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), - rightArrowButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 8), - rightArrowButton.widthAnchor.constraint(equalToConstant: 40), - rightArrowButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8), - - buttonContainerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8), - buttonContainerView.leadingAnchor.constraint(equalTo: leftArrowButton.trailingAnchor, constant: 4), - buttonContainerView.trailingAnchor.constraint(equalTo: rightArrowButton.leadingAnchor, constant: -4), - buttonContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8) - ]) - } + // MARK: Setup + + /// Sets up the UI components. + private func setupUI() { + buttonContainerView = UIView() + buttonContainerView.backgroundColor = .clear + buttonContainerView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(buttonContainerView) + + leftArrowButton = UIButton(type: .system) + leftArrowButton.setImage(UIImage(systemName: "chevron.left"), for: .normal) + leftArrowButton.tintColor = keyCharColor + leftArrowButton.backgroundColor = keyColor + leftArrowButton.layer.cornerRadius = keyCornerRadius + leftArrowButton.layer.shadowColor = keyShadowColor + leftArrowButton.layer.shadowOffset = CGSize(width: 0, height: 1) + leftArrowButton.layer.shadowOpacity = 1.0 + leftArrowButton.layer.shadowRadius = 0 + leftArrowButton.addTarget(self, action: #selector(leftArrowTapped), for: .touchUpInside) + leftArrowButton.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(leftArrowButton) + + rightArrowButton = UIButton(type: .system) + rightArrowButton.setImage(UIImage(systemName: "chevron.right"), for: .normal) + rightArrowButton.tintColor = keyCharColor + rightArrowButton.backgroundColor = keyColor + rightArrowButton.layer.cornerRadius = keyCornerRadius + rightArrowButton.layer.shadowColor = keyShadowColor + rightArrowButton.layer.shadowOffset = CGSize(width: 0, height: 1) + rightArrowButton.layer.shadowOpacity = 1.0 + rightArrowButton.layer.shadowRadius = 0 + rightArrowButton.addTarget(self, action: #selector(rightArrowTapped), for: .touchUpInside) + rightArrowButton.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(rightArrowButton) + + NSLayoutConstraint.activate([ + leftArrowButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8), + leftArrowButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 8), + leftArrowButton.widthAnchor.constraint(equalToConstant: 40), + leftArrowButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8), + + rightArrowButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), + rightArrowButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 8), + rightArrowButton.widthAnchor.constraint(equalToConstant: 40), + rightArrowButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8), + + buttonContainerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8), + buttonContainerView.leadingAnchor.constraint( + equalTo: leftArrowButton.trailingAnchor, constant: 4 + ), + buttonContainerView.trailingAnchor.constraint( + equalTo: rightArrowButton.leadingAnchor, constant: -4 + ), + buttonContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8) + ]) + } /// Builds page control for linear navigation mode. private func setupPageControl() { @@ -141,221 +146,223 @@ class DynamicConjugationViewController: UIViewController { self.pageControl = pc } - // MARK: Display + // MARK: Display - /// Displays the current navigation level. - private func displayCurrentLevel() { - buttonContainerView.subviews.forEach { $0.removeFromSuperview() } + /// Displays the current navigation level. + private func displayCurrentLevel() { + buttonContainerView.subviews.forEach { $0.removeFromSuperview() } - guard let currentLevel = navigationStack.last else { - commandBar?.text = commandPromptSpacing + "No data" - return - } + guard let currentLevel = navigationStack.last + else { + commandBar?.text = commandPromptSpacing + "No data" + return + } - commandBar?.text = commandPromptSpacing + currentLevel.title - commandBar?.isShowingInfoButton = false + commandBar?.text = commandPromptSpacing + currentLevel.title + commandBar?.isShowingInfoButton = false let options = currentLevel.options guard !options.isEmpty else { return } - // Create button grid. - let count = options.count - let (rows, cols) = getGridLayout(forCount: count) - let spacing: CGFloat = 4 - - let containerWidth = buttonContainerView.bounds.width - let containerHeight = buttonContainerView.bounds.height - let buttonWidth = (containerWidth - CGFloat(cols + 1) * spacing) / CGFloat(cols) - let buttonHeight = (containerHeight - CGFloat(rows + 1) * spacing) / CGFloat(rows) - - for (index, option) in options.enumerated() { - let row: Int - let col: Int - if cols == 1 { - row = index - col = 0 - } else { - col = index / rows - row = index % rows - } - - let button = UIButton(type: .custom) - button.frame = CGRect( - x: CGFloat(col) * (buttonWidth + spacing) + spacing, - y: CGFloat(row) * (buttonHeight + spacing) + spacing, - width: buttonWidth, - height: buttonHeight - ) - - button.setTitleColor(keyCharColor, for: .normal) - button.backgroundColor = keyColor - button.titleLabel?.font = .systemFont(ofSize: 16) - button.titleLabel?.numberOfLines = 0 - button.titleLabel?.adjustsFontSizeToFitWidth = true - button.titleLabel?.minimumScaleFactor = 0.6 - button.titleLabel?.textAlignment = .center - button.contentVerticalAlignment = .center - button.layer.cornerRadius = keyCornerRadius - button.layer.shadowColor = keyShadowColor - button.layer.shadowOffset = CGSize(width: 0, height: 1) - button.layer.shadowOpacity = 1.0 - button.layer.shadowRadius = 0 - button.tag = index - button.addTarget(self, action: #selector(optionButtonTapped(_:)), for: .touchUpInside) - if isInfoState { - button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 12) - } - - // Determine the display value. - let displayValue: String? - switch option.node { - case .finalValue(let value): - displayValue = value.isEmpty ? nil : value - case .nextLevel(_, let value): - displayValue = value - } - - // Add label at top-left. - let label = UILabel() - label.text = " " + option.label - label.font = .systemFont(ofSize: 11) - label.textColor = commandBarPlaceholderColor - label.translatesAutoresizingMaskIntoConstraints = false - button.addSubview(label) - - // Set value in center (if exists). - if let value = displayValue { - button.setTitle(value, for: .normal) - } - - NSLayoutConstraint.activate([ - label.topAnchor.constraint(equalTo: button.topAnchor, constant: 2), - label.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: 2) - ]) - - buttonContainerView.addSubview(button) - } - - updateArrowButtons() - } - - /// Handles option button taps. - @objc private func optionButtonTapped(_ sender: UIButton) { - guard let currentLevel = navigationStack.last, - sender.tag < currentLevel.options.count else { - return - } - - let selectedOption = currentLevel.options[sender.tag] - - switch selectedOption.node { - case .nextLevel(let nextLevel, _): - // Navigate deeper. - navigationStack.append(nextLevel) - displayCurrentLevel() - - case .finalValue(let value): - // Skip empty values. - guard !value.isEmpty, !isInfoState else { return } - - // Insert text and close. - proxy.insertText(value + " ") - closeTapped() + // Create button grid. + let count = options.count + let (rows, cols) = getGridLayout(forCount: count) + let spacing: CGFloat = 4 + + let containerWidth = buttonContainerView.bounds.width + let containerHeight = buttonContainerView.bounds.height + let buttonWidth = (containerWidth - CGFloat(cols + 1) * spacing) / CGFloat(cols) + let buttonHeight = (containerHeight - CGFloat(rows + 1) * spacing) / CGFloat(rows) + + for (index, option) in options.enumerated() { + let row: Int + let col: Int + if cols == 1 { + row = index + col = 0 + } else { + col = index / rows + row = index % rows + } + + let button = UIButton(type: .custom) + button.frame = CGRect( + x: CGFloat(col) * (buttonWidth + spacing) + spacing, + y: CGFloat(row) * (buttonHeight + spacing) + spacing, + width: buttonWidth, + height: buttonHeight + ) + + button.setTitleColor(keyCharColor, for: .normal) + button.backgroundColor = keyColor + button.titleLabel?.font = .systemFont(ofSize: 16) + button.titleLabel?.numberOfLines = 0 + button.titleLabel?.adjustsFontSizeToFitWidth = true + button.titleLabel?.minimumScaleFactor = 0.6 + button.titleLabel?.textAlignment = .center + button.contentVerticalAlignment = .center + button.layer.cornerRadius = keyCornerRadius + button.layer.shadowColor = keyShadowColor + button.layer.shadowOffset = CGSize(width: 0, height: 1) + button.layer.shadowOpacity = 1.0 + button.layer.shadowRadius = 0 + button.tag = index + button.addTarget(self, action: #selector(optionButtonTapped(_:)), for: .touchUpInside) + if isInfoState { + button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 12) + } + + // Determine the display value. + let displayValue: String? + switch option.node { + case let .finalValue(value): + displayValue = value.isEmpty ? nil : value + case let .nextLevel(_, value): + displayValue = value + } + + // Add label at top-left. + let label = UILabel() + label.text = " " + option.label + label.font = .systemFont(ofSize: 11) + label.textColor = commandBarPlaceholderColor + label.translatesAutoresizingMaskIntoConstraints = false + button.addSubview(label) + + // Set value in center (if exists). + if let value = displayValue { + button.setTitle(value, for: .normal) + } + + NSLayoutConstraint.activate([ + label.topAnchor.constraint(equalTo: button.topAnchor, constant: 2), + label.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: 2) + ]) + + buttonContainerView.addSubview(button) + } + + updateArrowButtons() } - } - /// Handles left arrow button tap. - @objc private func leftArrowTapped() { - if let cases = linearCases { - // Linear mode: navigate between cases or go back in tree. - if navigationStack.count > 1 { - // In a variant - go back. - navigationStack.removeLast() - displayCurrentLevel() - } else if currentCaseIndex > 0 { - // At root level - go to previous case. - currentCaseIndex -= 1 - navigationStack = [cases[currentCaseIndex]] - displayCurrentLevel() - } - } else { - // Tree mode: just go back. - if navigationStack.count > 1 { - navigationStack.removeLast() - displayCurrentLevel() - } + /// Handles option button taps. + @objc private func optionButtonTapped(_ sender: UIButton) { + guard let currentLevel = navigationStack.last, + sender.tag < currentLevel.options.count + else { + return + } + + let selectedOption = currentLevel.options[sender.tag] + + switch selectedOption.node { + case let .nextLevel(nextLevel, _): + // Navigate deeper. + navigationStack.append(nextLevel) + displayCurrentLevel() + + case let .finalValue(value): + // Skip empty values. + guard !value.isEmpty, !isInfoState else { return } + + // Insert text and close. + proxy.insertText(value + " ") + closeTapped() + } } - pageControl?.currentPage = currentCaseIndex + /// Handles left arrow button tap. + @objc private func leftArrowTapped() { + if let cases = linearCases { + // Linear mode: navigate between cases or go back in tree. + if navigationStack.count > 1 { + // In a variant - go back. + navigationStack.removeLast() + displayCurrentLevel() + } else if currentCaseIndex > 0 { + // At root level - go to previous case. + currentCaseIndex -= 1 + navigationStack = [cases[currentCaseIndex]] + displayCurrentLevel() + } + } else { + // Tree mode: just go back. + if navigationStack.count > 1 { + navigationStack.removeLast() + displayCurrentLevel() + } + } + + pageControl?.currentPage = currentCaseIndex } - /// Handles right arrow button tap. - @objc private func rightArrowTapped() { - if let cases = linearCases { - // Linear mode: navigate to next case. - if navigationStack.count > 1 { - // In a variant - can't navigate cases. - return - } else if currentCaseIndex < cases.count - 1 { - // At root level - go to next case. - currentCaseIndex += 1 - navigationStack = [cases[currentCaseIndex]] - displayCurrentLevel() - } - } - // Tree mode: right arrow does nothing. - - pageControl?.currentPage = currentCaseIndex + /// Handles right arrow button tap. + @objc private func rightArrowTapped() { + if let cases = linearCases { + // Linear mode: navigate to next case. + if navigationStack.count > 1 { + // In a variant - can't navigate cases. + return + } else if currentCaseIndex < cases.count - 1 { + // At root level - go to next case. + currentCaseIndex += 1 + navigationStack = [cases[currentCaseIndex]] + displayCurrentLevel() + } + } + // Tree mode: right arrow does nothing. + + pageControl?.currentPage = currentCaseIndex } - /// Updates the enabled state of arrow buttons. - private func updateArrowButtons() { - if let cases = linearCases { - // Linear mode. - if navigationStack.count > 1 { - // In a variant - left goes back, right disabled. - leftArrowButton.isEnabled = true - leftArrowButton.alpha = 1.0 - rightArrowButton.isEnabled = false - rightArrowButton.alpha = 1.0 - } else { - // At root case level - arrows navigate cases. - leftArrowButton.isEnabled = currentCaseIndex > 0 - rightArrowButton.isEnabled = currentCaseIndex < cases.count - 1 - } - } else { - // Tree mode - left goes back, right disabled. - leftArrowButton.isEnabled = navigationStack.count > 1 - - rightArrowButton.isEnabled = false + /// Updates the enabled state of arrow buttons. + private func updateArrowButtons() { + if let cases = linearCases { + // Linear mode. + if navigationStack.count > 1 { + // In a variant - left goes back, right disabled. + leftArrowButton.isEnabled = true + leftArrowButton.alpha = 1.0 + rightArrowButton.isEnabled = false + rightArrowButton.alpha = 1.0 + } else { + // At root case level - arrows navigate cases. + leftArrowButton.isEnabled = currentCaseIndex > 0 + rightArrowButton.isEnabled = currentCaseIndex < cases.count - 1 + } + } else { + // Tree mode - left goes back, right disabled. + leftArrowButton.isEnabled = navigationStack.count > 1 + + rightArrowButton.isEnabled = false + } } - } - /// Closes the dynamic conjugation view. - @objc private func closeTapped() { - commandState = .idle - autoActionState = .suggest + /// Closes the dynamic conjugation view. + @objc private func closeTapped() { + commandState = .idle + autoActionState = .suggest - let kvc = parent as? KeyboardViewController + let kvc = parent as? KeyboardViewController - removeFromParent() - view.removeFromSuperview() + removeFromParent() + view.removeFromSuperview() - kvc?.loadKeys() - kvc?.conditionallySetAutoActionBtns() - } + kvc?.loadKeys() + kvc?.conditionallySetAutoActionBtns() + } - /// Determines grid layout based on button count. - /// - Parameters: - /// - count: The number of buttons to display. - private func getGridLayout(forCount count: Int) -> (rows: Int, cols: Int) { - switch count { - case 1: return (1, 1) - case 2: return (2, 1) - case 3: return (3, 1) - case 4: return (2, 2) - case 6: return (3, 2) - default: return (count, 1) + /// Determines grid layout based on button count. + /// - Parameters: + /// - count: The number of buttons to display. + private func getGridLayout(forCount count: Int) -> (rows: Int, cols: Int) { + switch count { + case 1: return (1, 1) + case 2: return (2, 1) + case 3: return (3, 1) + case 4: return (2, 2) + case 6: return (3, 2) + default: return (count, 1) + } } - } } diff --git a/Keyboards/KeyboardsBase/Extensions.swift b/Keyboards/KeyboardsBase/Extensions.swift index 5a05fb0c..ef0612c0 100644 --- a/Keyboards/KeyboardsBase/Extensions.swift +++ b/Keyboards/KeyboardsBase/Extensions.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Extensions with helper functions for Scribe keyboards. */ @@ -8,96 +8,96 @@ import UIKit /// Extension to access the second to last element of an array. extension Array { - func secondToLast() -> Element? { - if count < 2 { - return nil + func secondToLast() -> Element? { + if count < 2 { + return nil + } + let index = count - 2 + return self[index] } - let index = count - 2 - return self[index] - } } extension Sequence where Iterator.Element: Hashable { - func unique() -> [Iterator.Element] { - var seen: [Iterator.Element: Bool] = [:] - return filter { seen.updateValue(true, forKey: $0) == nil } - } + func unique() -> [Iterator.Element] { + var seen: [Iterator.Element: Bool] = [:] + return filter { seen.updateValue(true, forKey: $0) == nil } + } } /// Extensions to String to allow for easier indexing, substring extraction and checking for certain characteristics. extension String { - func index(fromIdx: Int) -> Index { - return index(startIndex, offsetBy: fromIdx) - } - - func substring(fromIdx: Int) -> String { - let fromIndex = index(fromIdx: fromIdx) - return String(self[fromIndex...]) - } - - func substring(toIdx: Int) -> String { - let toIndex = index(fromIdx: toIdx) - return String(self[..) -> String { - let startIndex = index(fromIdx: range.lowerBound) - let endIndex = index(fromIdx: range.upperBound) - return String(self[startIndex ..< endIndex]) - } - - func insertPriorToCursor(char: String) -> String { - return substring(toIdx: count - 1) + char + commandCursor - } - - func deletePriorToCursor() -> String { - return substring(toIdx: count - 2) + commandCursor - } - - var isLowercase: Bool { - return self == lowercased() - } - - var isUppercase: Bool { - return self == uppercased() - } - - var isCapitalized: Bool { - return self == prefix(1).uppercased() + lowercased().dropFirst() - } - - func count(of char: Character) -> Int { - return reduce(0) { - $1 == char ? $0 + 1 : $0 - } - } - - func capitalize() -> String { - return prefix(1).uppercased() + lowercased().dropFirst() - } - - var isNumeric: Bool { - return Double(self) != nil - } + func index(fromIdx: Int) -> Index { + return index(startIndex, offsetBy: fromIdx) + } + + func substring(fromIdx: Int) -> String { + let fromIndex = index(fromIdx: fromIdx) + return String(self[fromIndex...]) + } + + func substring(toIdx: Int) -> String { + let toIndex = index(fromIdx: toIdx) + return String(self[..) -> String { + let startIndex = index(fromIdx: range.lowerBound) + let endIndex = index(fromIdx: range.upperBound) + return String(self[startIndex ..< endIndex]) + } + + func insertPriorToCursor(char: String) -> String { + return substring(toIdx: count - 1) + char + commandCursor + } + + func deletePriorToCursor() -> String { + return substring(toIdx: count - 2) + commandCursor + } + + var isLowercase: Bool { + return self == lowercased() + } + + var isUppercase: Bool { + return self == uppercased() + } + + var isCapitalized: Bool { + return self == prefix(1).uppercased() + lowercased().dropFirst() + } + + func count(of char: Character) -> Int { + return reduce(0) { + $1 == char ? $0 + 1 : $0 + } + } + + func capitalize() -> String { + return prefix(1).uppercased() + lowercased().dropFirst() + } + + var isNumeric: Bool { + return Double(self) != nil + } } /// Adds the ability to efficiently trim whitespace at the end of a string. extension StringProtocol { - @inline(__always) - var trailingSpacesTrimmed: Self.SubSequence { - var view = self[...] - - // Needs to be an explicit boolean comparison. - while view.last?.isWhitespace == true { - view = view.dropLast() + @inline(__always) + var trailingSpacesTrimmed: Self.SubSequence { + var view = self[...] + + // Needs to be an explicit boolean comparison. + while view.last?.isWhitespace == true { + view = view.dropLast() + } + return view } - return view - } } extension NSMutableAttributedString { - func setColorForText(textForAttribute: String, withColor color: UIColor) { - let range = mutableString.range(of: textForAttribute, options: .caseInsensitive) - addAttribute(NSAttributedString.Key.foregroundColor, value: color, range: range) - } + func setColorForText(textForAttribute: String, withColor color: UIColor) { + let range = mutableString.range(of: textForAttribute, options: .caseInsensitive) + addAttribute(NSAttributedString.Key.foregroundColor, value: color, range: range) + } } diff --git a/Keyboards/KeyboardsBase/InterfaceVariables.swift b/Keyboards/KeyboardsBase/InterfaceVariables.swift index 0d68c76a..ad4c8a64 100644 --- a/Keyboards/KeyboardsBase/InterfaceVariables.swift +++ b/Keyboards/KeyboardsBase/InterfaceVariables.swift @@ -1,10 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/// Variables associated with the base keyboard interface. +// Variables associated with the base keyboard interface. import UIKit -// A proxy into which text is typed. +/// A proxy into which text is typed. var proxy: UITextDocumentProxy! // MARK: Display Variables @@ -14,7 +14,8 @@ var keyboard = [[String]]() var usingExpandedKeyboard = false var allKeys = [String]() let specialKeys = [ - SpecialKeys.indent, SpecialKeys.capsLock, "shift", "delete", "ABC", "АБВ", "123", "#+=", "selectKeyboard", "space", "return", ".?123", "hideKeyboard" + SpecialKeys.indent, SpecialKeys.capsLock, "shift", "delete", "ABC", "АБВ", "123", "#+=", + "selectKeyboard", "space", "return", ".?123", "hideKeyboard" ] var allNonSpecialKeys = [String]() var keyboardHeight: CGFloat! @@ -56,7 +57,7 @@ var scalarCapsLockKeyWidth = 1.8 var spaceBar = String() var language = String() var languageTextForSpaceBar: String { - "\(language) (Scribe)" + "\(language) (Scribe)" } var showKeyboardLanguage = false @@ -68,46 +69,46 @@ var symbolKeys = [[String]]() /// States of the keyboard corresponding to layouts found in KeyboardConstants.swift. enum KeyboardState { - case letters - case numbers - case symbols + case letters + case numbers + case symbols } /// What the keyboard state is in regards to the shift key. /// - normal: not capitalized /// - shift: capitalized enum ShiftButtonState { - case normal - case shift - case capsLocked + case normal + case shift + case capsLocked } /// States of the keyboard corresponding to which commands the user is executing. enum CommandState { - case idle - case selectCommand - case translate - case conjugate - case selectCaseDeclension - case plural - case alreadyPlural - case invalid - case displayInformation - case dynamicConjugation + case idle + case selectCommand + case translate + case conjugate + case selectCaseDeclension + case plural + case alreadyPlural + case invalid + case displayInformation + case dynamicConjugation } /// States of the keyboard corresponding to which auto actions should be presented. enum AutoActionState { - case complete - case suggest + case complete + case suggest } /// States for which conjugation table view shift button should be active. enum ConjViewShiftButtonsState { - case bothActive - case bothInactive - case leftInactive - case rightInactive + case bothActive + case bothInactive + case leftInactive + case rightInactive } // Baseline state variables. @@ -119,121 +120,132 @@ var autoActionState: AutoActionState = .suggest var conjViewShiftButtonsState: ConjViewShiftButtonsState = .bothInactive var pluralWords: Set? -// Variables and functions to determine display parameters. +/// Variables and functions to determine display parameters. enum DeviceType { - static let isPhone = UIDevice.current.userInterfaceIdiom == .phone - static let isPad = UIDevice.current.userInterfaceIdiom == .pad + static let isPhone = UIDevice.current.userInterfaceIdiom == .phone + static let isPad = UIDevice.current.userInterfaceIdiom == .pad } var isLandscapeView = false /// Checks if the device is in landscape mode. func checkLandscapeMode() { - if UIScreen.main.bounds.height < UIScreen.main.bounds.width { - isLandscapeView = true - } else { - isLandscapeView = false - } + if UIScreen.main.bounds.height < UIScreen.main.bounds.width { + isLandscapeView = true + } else { + isLandscapeView = false + } } -// Keyboard language variables. +/// Keyboard language variables. var controllerLanguage = String() -// Dictionary for accessing language abbreviations. +/// Dictionary for accessing language abbreviations. let languagesAbbrDict = [ - // "Danish": "da", - "English": "en", - "French": "fr", - "German": "de", - // "Indonesian": "id", - "Italian": "it", - // "Norwegian": "nb", - "Portuguese": "pt", - "Russian": "ru", - "Spanish": "es", - "Swedish": "sv" + // "Danish": "da", + "English": "en", + "French": "fr", + "German": "de", + // "Indonesian": "id", + "Italian": "it", + // "Norwegian": "nb", + "Portuguese": "pt", + "Russian": "ru", + "Spanish": "es", + "Swedish": "sv" ] let languagesStringDict = [ - "English": NSLocalizedString("i18n.app._global.english", value: "English", comment: ""), - "French": NSLocalizedString("i18n.app._global.french", value: "French", comment: ""), - "German": NSLocalizedString("i18n.app._global.german", value: "German", comment: ""), - // "Indonesian": NSLocalizedString("i18n.app._global.indonesian", value: "Indonesian", comment: ""), - "Italian": NSLocalizedString("i18n.app._global.italian", value: "Italian", comment: ""), - // "Norwegian": NSLocalizedString("i18n.app._global.norwegian", value: "Norwegian", comment: ""), - "Portuguese": NSLocalizedString("i18n.app._global.portuguese", value: "Portuguese", comment: ""), - "Russian": NSLocalizedString("i18n.app._global.russian", value: "Russian", comment: ""), - "Spanish": NSLocalizedString("i18n.app._global.spanish", value: "Spanish", comment: ""), - "Swedish": NSLocalizedString("i18n.app._global.swedish", value: "Swedish", comment: "") + "English": NSLocalizedString("i18n.app._global.english", value: "English", comment: ""), + "French": NSLocalizedString("i18n.app._global.french", value: "French", comment: ""), + "German": NSLocalizedString("i18n.app._global.german", value: "German", comment: ""), + // "Indonesian": NSLocalizedString("i18n.app._global.indonesian", value: "Indonesian", comment: ""), + "Italian": NSLocalizedString("i18n.app._global.italian", value: "Italian", comment: ""), + // "Norwegian": NSLocalizedString("i18n.app._global.norwegian", value: "Norwegian", comment: ""), + "Portuguese": NSLocalizedString( + "i18n.app._global.portuguese", value: "Portuguese", comment: "" + ), + "Russian": NSLocalizedString("i18n.app._global.russian", value: "Russian", comment: ""), + "Spanish": NSLocalizedString("i18n.app._global.spanish", value: "Spanish", comment: ""), + "Swedish": NSLocalizedString("i18n.app._global.swedish", value: "Swedish", comment: "") ] /// Returns the key in a dictionary for a given value. func getKeyInDict(givenValue: String, dict: [String: String]) -> String { - for (key, value) in dict where value == givenValue { - return key - } - return "" + for (key, value) in dict where value == givenValue { + return key + } + return "" } /// Returns the abbreviation of the language for use in commands. func getControllerLanguageAbbr() -> String { - guard let abbreviation = languagesAbbrDict[controllerLanguage] else { - return "" - } - return abbreviation + guard let abbreviation = languagesAbbrDict[controllerLanguage] + else { + return "" + } + return abbreviation } /// Returns the translation language code for the current controller language. func getControllerTranslateLangCode() -> String { - let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! - let key = getControllerLanguageAbbr() + "TranslateLanguage" - if let translateLang = userDefaults.string(forKey: key) { - return translateLang - } else { - userDefaults.set("en", forKey: key) - return "en" - } + let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! + let key = getControllerLanguageAbbr() + "TranslateLanguage" + if let translateLang = userDefaults.string(forKey: key) { + return translateLang + } else { + userDefaults.set("en", forKey: key) + return "en" + } } -// Dictionary for accessing keyboard abbreviations and layouts. +/// Dictionary for accessing keyboard abbreviations and layouts. let keyboardLayoutDict: [String: () -> Void] = [ - // Layouts for French checked within setFRKeyboardLayout. - "English": setENKeyboardLayout, - "French": setFRKeyboardLayout, - "German": setDEKeyboardLayout, - "Indonesian": setIDKeyboardLayout, - "Italian": setITKeyboardLayout, - "Norwegian": setNBKeyboardLayout, - "Portuguese": setPTKeyboardLayout, - "Russian": setRUKeyboardLayout, - "Spanish": setESKeyboardLayout, - "Swedish": setSVKeyboardLayout + // Layouts for French checked within setFRKeyboardLayout. + "English": setENKeyboardLayout, + "French": setFRKeyboardLayout, + "German": setDEKeyboardLayout, + "Indonesian": setIDKeyboardLayout, + "Italian": setITKeyboardLayout, + "Norwegian": setNBKeyboardLayout, + "Portuguese": setPTKeyboardLayout, + "Russian": setRUKeyboardLayout, + "Spanish": setESKeyboardLayout, + "Swedish": setSVKeyboardLayout ] /// Sets the keyboard layout and its alternate keys. func setKeyboard() { - setKeyboardLayout() - setKeyboardAlternateKeys() + setKeyboardLayout() + setKeyboardAlternateKeys() } /// Sets the keyboard layouts given the chosen keyboard and device type. func setKeyboardLayout() { - if commandState == .translate { - let translateLanguage = getKeyInDict(givenValue: getControllerTranslateLangCode(), dict: languagesAbbrDict) - if let setLayoutFxn = keyboardLayoutDict[translateLanguage] { - setLayoutFxn() - } else { - setENKeyboardLayout() + if commandState == .translate { + let translateLanguage = getKeyInDict( + givenValue: getControllerTranslateLangCode(), dict: languagesAbbrDict + ) + if let setLayoutFxn = keyboardLayoutDict[translateLanguage] { + setLayoutFxn() + } else { + setENKeyboardLayout() + } + } else if let setLayoutFxn = keyboardLayoutDict[controllerLanguage] { + setLayoutFxn() } - } else if let setLayoutFxn = keyboardLayoutDict[controllerLanguage] { - setLayoutFxn() - } - // Variable type is String. - allPrompts = [translatePromptAndCursor, conjugatePromptAndCursor, pluralPromptAndCursor, translatePromptAndPlaceholder, conjugatePromptAndPlaceholder, pluralPromptAndPlaceholder] + // Variable type is String. + allPrompts = [ + translatePromptAndCursor, conjugatePromptAndCursor, pluralPromptAndCursor, + translatePromptAndPlaceholder, conjugatePromptAndPlaceholder, pluralPromptAndPlaceholder + ] - // Variable type is NSAttributedString. - allColoredPrompts = [translatePromptAndColorPlaceholder, conjugatePromptAndColorPlaceholder, pluralPromptAndColorPlaceholder] + // Variable type is NSAttributedString. + allColoredPrompts = [ + translatePromptAndColorPlaceholder, conjugatePromptAndColorPlaceholder, + pluralPromptAndColorPlaceholder + ] } // Variables that define which keys are positioned on the very left, right or in the center of the keyboard. diff --git a/Keyboards/KeyboardsBase/KeyAltChars.swift b/Keyboards/KeyboardsBase/KeyAltChars.swift index 0eea0500..ec13fd5a 100644 --- a/Keyboards/KeyboardsBase/KeyAltChars.swift +++ b/Keyboards/KeyboardsBase/KeyAltChars.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Functions and variables to create alternate key views. */ @@ -8,45 +8,45 @@ import UIKit /// Sets the alternates for certain keys given the chosen keyboard. func setKeyboardAlternateKeys() { - if DeviceType.isPhone { - keysWithAlternates += symbolKeysWithAlternatesLeft - keysWithAlternates += symbolKeysWithAlternatesRight - keysWithAlternates.append(currencySymbol) - keysWithAlternatesLeft += symbolKeysWithAlternatesLeft - keysWithAlternatesRight += symbolKeysWithAlternatesRight - keysWithAlternatesRight.append(currencySymbol) - } - - keyAlternatesDict = [ - "a": aAlternateKeys, - "e": eAlternateKeys, - "е": еAlternateKeys, // Russian е - "i": iAlternateKeys, - "o": oAlternateKeys, - "u": uAlternateKeys, - "ä": äAlternateKeys, - "ö": öAlternateKeys, - "å": åAlternateKeys, - "æ": æAlternateKeys, - "ø": øAlternateKeys, - "y": yAlternateKeys, - "s": sAlternateKeys, - "l": lAlternateKeys, - "z": zAlternateKeys, - "d": dAlternateKeys, - "c": cAlternateKeys, - "n": nAlternateKeys, - "ь": ьAlternateKeys, - "/": backslashAlternateKeys, - "?": questionMarkAlternateKeys, - "!": exclamationAlternateKeys, - "%": percentAlternateKeys, - "&": ampersandAlternateKeys, - "'": apostropheAlternateKeys, - "\"": quotationAlternateKeys, - "=": equalSignAlternateKeys, - currencySymbol: currencySymbolAlternates - ] + if DeviceType.isPhone { + keysWithAlternates += symbolKeysWithAlternatesLeft + keysWithAlternates += symbolKeysWithAlternatesRight + keysWithAlternates.append(currencySymbol) + keysWithAlternatesLeft += symbolKeysWithAlternatesLeft + keysWithAlternatesRight += symbolKeysWithAlternatesRight + keysWithAlternatesRight.append(currencySymbol) + } + + keyAlternatesDict = [ + "a": aAlternateKeys, + "e": eAlternateKeys, + "е": еAlternateKeys, // Russian е + "i": iAlternateKeys, + "o": oAlternateKeys, + "u": uAlternateKeys, + "ä": äAlternateKeys, + "ö": öAlternateKeys, + "å": åAlternateKeys, + "æ": æAlternateKeys, + "ø": øAlternateKeys, + "y": yAlternateKeys, + "s": sAlternateKeys, + "l": lAlternateKeys, + "z": zAlternateKeys, + "d": dAlternateKeys, + "c": cAlternateKeys, + "n": nAlternateKeys, + "ь": ьAlternateKeys, + "/": backslashAlternateKeys, + "?": questionMarkAlternateKeys, + "!": exclamationAlternateKeys, + "%": percentAlternateKeys, + "&": ampersandAlternateKeys, + "'": apostropheAlternateKeys, + "\"": quotationAlternateKeys, + "=": equalSignAlternateKeys, + currencySymbol: currencySymbolAlternates + ] } var alternatesKeyView: UIView! @@ -114,46 +114,47 @@ var øAlternateKeys = [String]() /// - numAlternates: the number of alternate characters to display. /// - side: the side of the keyboard that the key is found. func setAlternatesPathState( - startY _: CGFloat, - keyWidth: CGFloat, - keyHeight: CGFloat, - numAlternates: CGFloat, - side: String + startY _: CGFloat, + keyWidth: CGFloat, + keyHeight: CGFloat, + numAlternates: CGFloat, + side: String ) { - if DeviceType.isPad { - widthMultiplier = 0.2 - maxHeightMultiplier = 2.05 - if isLandscapeView { - maxHeightMultiplier = 1.95 + if DeviceType.isPad { + widthMultiplier = 0.2 + maxHeightMultiplier = 2.05 + if isLandscapeView { + maxHeightMultiplier = 1.95 + } + } else if DeviceType.isPhone { + widthMultiplier = 0.4 + maxHeightMultiplier = 2.125 + if isLandscapeView { + widthMultiplier = 0.2 + } + } + + maxHeight = vertStart - (keyHeight * maxHeightMultiplier) + maxHeightCurveControl = vertStart - (keyHeight * (maxHeightMultiplier - 0.125)) + minHeightCurveControl = vertStart - (keyHeight * 0.005) + + if DeviceType.isPhone { + heightBeforeTopCurves = vertStart - (keyHeight * 1.8) + maxWidthCurveControl = keyWidth * 0.5 + } else if DeviceType.isPad || (DeviceType.isPhone && isLandscapeView) { + heightBeforeTopCurves = vertStart - (keyHeight * 1.6) + maxWidthCurveControl = keyWidth * 0.25 } - } else if DeviceType.isPhone { - widthMultiplier = 0.4 - maxHeightMultiplier = 2.125 - if isLandscapeView { - widthMultiplier = 0.2 + if side == "left" { + alternatesLongWidth = horizStart + (keyWidth * numAlternates + (3.0 * numAlternates) + 8.0) + } else if side == "right" { + alternatesLongWidth = + horizStart + keyWidth - CGFloat(keyWidth * numAlternates + (3.0 * numAlternates) + 8.0) } - } - - maxHeight = vertStart - (keyHeight * maxHeightMultiplier) - maxHeightCurveControl = vertStart - (keyHeight * (maxHeightMultiplier - 0.125)) - minHeightCurveControl = vertStart - (keyHeight * 0.005) - - if DeviceType.isPhone { - heightBeforeTopCurves = vertStart - (keyHeight * 1.8) - maxWidthCurveControl = keyWidth * 0.5 - } else if DeviceType.isPad || (DeviceType.isPhone && isLandscapeView) { - heightBeforeTopCurves = vertStart - (keyHeight * 1.6) - maxWidthCurveControl = keyWidth * 0.25 - } - if side == "left" { - alternatesLongWidth = horizStart + (keyWidth * numAlternates + (3.0 * numAlternates) + 8.0) - } else if side == "right" { - alternatesLongWidth = horizStart + keyWidth - CGFloat(keyWidth * numAlternates + (3.0 * numAlternates) + 8.0) - } - - alternatesShapeLayer.strokeColor = keyShadowColor - alternatesShapeLayer.fillColor = keyColor.cgColor - alternatesShapeLayer.lineWidth = 1.0 + + alternatesShapeLayer.strokeColor = keyShadowColor + alternatesShapeLayer.fillColor = keyColor.cgColor + alternatesShapeLayer.lineWidth = 1.0 } /// Creates the shape that allows alternate keys to be displayed to the user for keys on the left side of the keyboard. @@ -165,75 +166,90 @@ func setAlternatesPathState( /// - keyHeight: the height of the key. /// - numAlternates: the number of alternate characters to display. func alternateKeysPathLeft( - startX: CGFloat, - startY: CGFloat, - keyWidth: CGFloat, - keyHeight: CGFloat, - numAlternates: CGFloat + startX: CGFloat, + startY: CGFloat, + keyWidth: CGFloat, + keyHeight: CGFloat, + numAlternates: CGFloat ) -> UIBezierPath { - // Starting positions need to be updated. - horizStart = startX; vertStart = startY + keyHeight - - setAlternatesPathState( - startY: startY, keyWidth: keyWidth, keyHeight: keyHeight, numAlternates: numAlternates, side: "left" - ) - - // Path is clockwise from bottom left. - let path = UIBezierPath(); path.move(to: CGPoint(x: horizStart + (keyWidth * 0.075), y: vertStart)) - - // Curve up past bottom left, path up, and curve out to the left. - path.addCurve( - to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.075)), - controlPoint1: CGPoint(x: horizStart + (keyWidth * 0.075), y: minHeightCurveControl), - controlPoint2: CGPoint(x: horizStart, y: minHeightCurveControl) - ) - path.addLine(to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.85))) - path.addCurve( - to: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: vertStart - (keyHeight * 1.2)), - controlPoint1: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.9)), - controlPoint2: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: vertStart - (keyHeight * 1.05)) - ) - - // Path up and curve right past the top left. - path.addLine(to: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: heightBeforeTopCurves)) - path.addCurve( - to: CGPoint(x: horizStart + (keyWidth * 0.075), y: maxHeight), - controlPoint1: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: maxHeightCurveControl), - controlPoint2: CGPoint(x: horizStart - (keyWidth * 0.25), y: maxHeight) - ) - - // Path right, curve down past the top right, and path down. - path.addLine(to: CGPoint(x: alternatesLongWidth - maxWidthCurveControl, y: maxHeight)) - path.addCurve( - to: CGPoint(x: alternatesLongWidth, y: heightBeforeTopCurves), - controlPoint1: CGPoint(x: alternatesLongWidth - (keyWidth * 0.2), y: maxHeight), - controlPoint2: CGPoint(x: alternatesLongWidth, y: maxHeightCurveControl) - ) - path.addLine(to: CGPoint(x: alternatesLongWidth, y: vertStart - (keyHeight * 1.15))) - - // Curve down past the left and path left. - path.addCurve( - to: CGPoint(x: alternatesLongWidth - maxWidthCurveControl, y: vertStart - (keyHeight * 0.95)), - controlPoint1: CGPoint(x: alternatesLongWidth, y: vertStart - (keyHeight * 1.05)), - controlPoint2: CGPoint(x: alternatesLongWidth - (keyWidth * 0.2), y: vertStart - (keyHeight * 0.95)) - ) - path.addLine(to: CGPoint(x: horizStart + (keyWidth * 1.15), y: vertStart - (keyHeight * 0.95))) - - // Curve in to the left, go down, and curve down past bottom left. - path.addCurve( - to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.85)), - controlPoint1: CGPoint(x: horizStart + (keyWidth * 1.05), y: vertStart - (keyHeight * 0.95)), - controlPoint2: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.875)) - ) - path.addLine(to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.075))) - path.addCurve( - to: CGPoint(x: horizStart + (keyWidth * 0.925), y: vertStart), - controlPoint1: CGPoint(x: horizStart + keyWidth, y: minHeightCurveControl), - controlPoint2: CGPoint(x: horizStart + (keyWidth * 0.925), y: minHeightCurveControl) - ) - - path.close() - return path + // Starting positions need to be updated. + horizStart = startX + vertStart = startY + keyHeight + + setAlternatesPathState( + startY: startY, keyWidth: keyWidth, keyHeight: keyHeight, numAlternates: numAlternates, + side: "left" + ) + + // Path is clockwise from bottom left. + let path = UIBezierPath() + path.move(to: CGPoint(x: horizStart + (keyWidth * 0.075), y: vertStart)) + + // Curve up past bottom left, path up, and curve out to the left. + path.addCurve( + to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.075)), + controlPoint1: CGPoint(x: horizStart + (keyWidth * 0.075), y: minHeightCurveControl), + controlPoint2: CGPoint(x: horizStart, y: minHeightCurveControl) + ) + path.addLine(to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.85))) + path.addCurve( + to: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: vertStart - (keyHeight * 1.2)), + controlPoint1: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.9)), + controlPoint2: CGPoint( + x: horizStart - (keyWidth * widthMultiplier), y: vertStart - (keyHeight * 1.05) + ) + ) + + // Path up and curve right past the top left. + path.addLine( + to: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: heightBeforeTopCurves) + ) + path.addCurve( + to: CGPoint(x: horizStart + (keyWidth * 0.075), y: maxHeight), + controlPoint1: CGPoint( + x: horizStart - (keyWidth * widthMultiplier), y: maxHeightCurveControl + ), + controlPoint2: CGPoint(x: horizStart - (keyWidth * 0.25), y: maxHeight) + ) + + // Path right, curve down past the top right, and path down. + path.addLine(to: CGPoint(x: alternatesLongWidth - maxWidthCurveControl, y: maxHeight)) + path.addCurve( + to: CGPoint(x: alternatesLongWidth, y: heightBeforeTopCurves), + controlPoint1: CGPoint(x: alternatesLongWidth - (keyWidth * 0.2), y: maxHeight), + controlPoint2: CGPoint(x: alternatesLongWidth, y: maxHeightCurveControl) + ) + path.addLine(to: CGPoint(x: alternatesLongWidth, y: vertStart - (keyHeight * 1.15))) + + // Curve down past the left and path left. + path.addCurve( + to: CGPoint( + x: alternatesLongWidth - maxWidthCurveControl, y: vertStart - (keyHeight * 0.95) + ), + controlPoint1: CGPoint(x: alternatesLongWidth, y: vertStart - (keyHeight * 1.05)), + controlPoint2: CGPoint( + x: alternatesLongWidth - (keyWidth * 0.2), y: vertStart - (keyHeight * 0.95) + ) + ) + path.addLine(to: CGPoint(x: horizStart + (keyWidth * 1.15), y: vertStart - (keyHeight * 0.95))) + + // Curve in to the left, go down, and curve down past bottom left. + path.addCurve( + to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.85)), + controlPoint1: CGPoint( + x: horizStart + (keyWidth * 1.05), y: vertStart - (keyHeight * 0.95) + ), + controlPoint2: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.875)) + ) + path.addLine(to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.075))) + path.addCurve( + to: CGPoint(x: horizStart + (keyWidth * 0.925), y: vertStart), + controlPoint1: CGPoint(x: horizStart + keyWidth, y: minHeightCurveControl), + controlPoint2: CGPoint(x: horizStart + (keyWidth * 0.925), y: minHeightCurveControl) + ) + + path.close() + return path } /// Creates the shape that allows alternate keys to be displayed to the user for keys on the right side of the keyboard. @@ -245,75 +261,92 @@ func alternateKeysPathLeft( /// - keyHeight: the height of the key. /// - numAlternates: the number of alternate characters to display. func alternateKeysPathRight( - startX: CGFloat, - startY: CGFloat, - keyWidth: CGFloat, - keyHeight: CGFloat, - numAlternates: CGFloat + startX: CGFloat, + startY: CGFloat, + keyWidth: CGFloat, + keyHeight: CGFloat, + numAlternates: CGFloat ) -> UIBezierPath { - // Starting positions need to be updated. - horizStart = startX; vertStart = startY + keyHeight - - setAlternatesPathState( - startY: startY, keyWidth: keyWidth, keyHeight: keyHeight, numAlternates: numAlternates, side: "right" - ) - - // Path is clockwise from bottom left. - let path = UIBezierPath(); path.move(to: CGPoint(x: horizStart + (keyWidth * 0.075), y: vertStart)) - - // Curve up past bottom left, path up, and curve out to the left. - path.addCurve( - to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.075)), - controlPoint1: CGPoint(x: horizStart + (keyWidth * 0.075), y: minHeightCurveControl), - controlPoint2: CGPoint(x: horizStart, y: minHeightCurveControl) - ) - path.addLine(to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.85))) - path.addCurve( - to: CGPoint(x: horizStart - (keyWidth * 0.15), y: vertStart - (keyHeight * 0.95)), - controlPoint1: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.875)), - controlPoint2: CGPoint(x: horizStart - (keyWidth * 0.05), y: vertStart - (keyHeight * 0.95)) - ) - - // Path left and path up past the left. - path.addLine(to: CGPoint(x: alternatesLongWidth + maxWidthCurveControl, y: vertStart - (keyHeight * 0.95))) - path.addCurve( - to: CGPoint(x: alternatesLongWidth, y: vertStart - (keyHeight * 1.15)), - controlPoint1: CGPoint(x: alternatesLongWidth + (keyWidth * 0.2), y: vertStart - (keyHeight * 0.95)), - controlPoint2: CGPoint(x: alternatesLongWidth, y: vertStart - (keyHeight * 1.05)) - ) - - // Path up and curve up past the top left. - path.addLine(to: CGPoint(x: alternatesLongWidth, y: heightBeforeTopCurves)) - path.addCurve( - to: CGPoint(x: alternatesLongWidth + maxWidthCurveControl, y: maxHeight), - controlPoint1: CGPoint(x: alternatesLongWidth, y: maxHeightCurveControl), - controlPoint2: CGPoint(x: alternatesLongWidth + (keyWidth * 0.2), y: maxHeight) - ) - - // Path right, curve down past the top right, and path down. - path.addLine(to: CGPoint(x: horizStart + (keyWidth * 0.925), y: maxHeight)) - path.addCurve( - to: CGPoint(x: horizStart + (keyWidth * (1 + widthMultiplier)), y: heightBeforeTopCurves), - controlPoint1: CGPoint(x: horizStart + (keyWidth * 1.25), y: maxHeight), - controlPoint2: CGPoint(x: horizStart + (keyWidth * (1 + widthMultiplier)), y: maxHeightCurveControl) - ) - path.addLine(to: CGPoint(x: horizStart + (keyWidth * (1 + widthMultiplier)), y: vertStart - (keyHeight * 1.2))) - - // Curve in to the left, go down, and curve down past bottom left. - path.addCurve( - to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.85)), - controlPoint1: CGPoint(x: horizStart + (keyWidth * (1 + widthMultiplier)), y: vertStart - (keyHeight * 1.05)), - controlPoint2: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.9)) - ) - path.addLine(to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.075))) - path.addCurve( - to: CGPoint(x: horizStart + (keyWidth * 0.925), y: vertStart), - controlPoint1: CGPoint(x: horizStart + keyWidth, y: minHeightCurveControl), - controlPoint2: CGPoint(x: horizStart + (keyWidth * 0.925), y: minHeightCurveControl) - ) - - path.close() - return path + // Starting positions need to be updated. + horizStart = startX + vertStart = startY + keyHeight + + setAlternatesPathState( + startY: startY, keyWidth: keyWidth, keyHeight: keyHeight, numAlternates: numAlternates, + side: "right" + ) + + // Path is clockwise from bottom left. + let path = UIBezierPath() + path.move(to: CGPoint(x: horizStart + (keyWidth * 0.075), y: vertStart)) + + // Curve up past bottom left, path up, and curve out to the left. + path.addCurve( + to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.075)), + controlPoint1: CGPoint(x: horizStart + (keyWidth * 0.075), y: minHeightCurveControl), + controlPoint2: CGPoint(x: horizStart, y: minHeightCurveControl) + ) + path.addLine(to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.85))) + path.addCurve( + to: CGPoint(x: horizStart - (keyWidth * 0.15), y: vertStart - (keyHeight * 0.95)), + controlPoint1: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.875)), + controlPoint2: CGPoint(x: horizStart - (keyWidth * 0.05), y: vertStart - (keyHeight * 0.95)) + ) + + // Path left and path up past the left. + path.addLine( + to: CGPoint( + x: alternatesLongWidth + maxWidthCurveControl, y: vertStart - (keyHeight * 0.95) + ) + ) + path.addCurve( + to: CGPoint(x: alternatesLongWidth, y: vertStart - (keyHeight * 1.15)), + controlPoint1: CGPoint( + x: alternatesLongWidth + (keyWidth * 0.2), y: vertStart - (keyHeight * 0.95) + ), + controlPoint2: CGPoint(x: alternatesLongWidth, y: vertStart - (keyHeight * 1.05)) + ) + + // Path up and curve up past the top left. + path.addLine(to: CGPoint(x: alternatesLongWidth, y: heightBeforeTopCurves)) + path.addCurve( + to: CGPoint(x: alternatesLongWidth + maxWidthCurveControl, y: maxHeight), + controlPoint1: CGPoint(x: alternatesLongWidth, y: maxHeightCurveControl), + controlPoint2: CGPoint(x: alternatesLongWidth + (keyWidth * 0.2), y: maxHeight) + ) + + // Path right, curve down past the top right, and path down. + path.addLine(to: CGPoint(x: horizStart + (keyWidth * 0.925), y: maxHeight)) + path.addCurve( + to: CGPoint(x: horizStart + (keyWidth * (1 + widthMultiplier)), y: heightBeforeTopCurves), + controlPoint1: CGPoint(x: horizStart + (keyWidth * 1.25), y: maxHeight), + controlPoint2: CGPoint( + x: horizStart + (keyWidth * (1 + widthMultiplier)), y: maxHeightCurveControl + ) + ) + path.addLine( + to: CGPoint( + x: horizStart + (keyWidth * (1 + widthMultiplier)), y: vertStart - (keyHeight * 1.2) + ) + ) + + // Curve in to the left, go down, and curve down past bottom left. + path.addCurve( + to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.85)), + controlPoint1: CGPoint( + x: horizStart + (keyWidth * (1 + widthMultiplier)), y: vertStart - (keyHeight * 1.05) + ), + controlPoint2: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.9)) + ) + path.addLine(to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.075))) + path.addCurve( + to: CGPoint(x: horizStart + (keyWidth * 0.925), y: vertStart), + controlPoint1: CGPoint(x: horizStart + keyWidth, y: minHeightCurveControl), + controlPoint2: CGPoint(x: horizStart + (keyWidth * 0.925), y: minHeightCurveControl) + ) + + path.close() + return path } /// Generates an alternates view to select other characters related to a long held key. @@ -321,52 +354,56 @@ func alternateKeysPathRight( /// - Parameters /// - sender: the long press of the given key. func genAlternatesView(key: UIButton) { - // Get the frame in respect to the superview. - if let frame = key.superview?.convert(key.frame, to: nil) { - let width = key.frame.width - - // Derive which button was pressed and get its alternates. - let char = key.layer.value(forKey: "original") as? String ?? "" - alternateKeys = keyAlternatesDict[char] ?? [""] - - // Add the original key given its location on the keyboard. - if keysWithAlternatesLeft.contains(char) { - alternateKeys.insert(char, at: 0) - } else if keysWithAlternatesRight.contains(char) { - alternateKeys.append(char) - } - let numAlternates = CGFloat(alternateKeys.count) - - if keysWithAlternatesLeft.contains(char) { - alternatesViewX = frame.origin.x - 4.0 - alternatesShapeLayer.path = alternateKeysPathLeft( - startX: frame.origin.x, startY: frame.origin.y, - keyWidth: width, keyHeight: key.frame.height, numAlternates: numAlternates - ).cgPath - } else if keysWithAlternatesRight.contains(char) { - alternatesViewX = frame.origin.x + width - CGFloat(width * numAlternates + (3.0 * numAlternates) + 2.0) - alternatesShapeLayer.path = alternateKeysPathRight( - startX: frame.origin.x, startY: frame.origin.y, - keyWidth: width, keyHeight: key.frame.height, numAlternates: numAlternates - ).cgPath + // Get the frame in respect to the superview. + if let frame = key.superview?.convert(key.frame, to: nil) { + let width = key.frame.width + + // Derive which button was pressed and get its alternates. + let char = key.layer.value(forKey: "original") as? String ?? "" + alternateKeys = keyAlternatesDict[char] ?? [""] + + // Add the original key given its location on the keyboard. + if keysWithAlternatesLeft.contains(char) { + alternateKeys.insert(char, at: 0) + } else if keysWithAlternatesRight.contains(char) { + alternateKeys.append(char) + } + let numAlternates = CGFloat(alternateKeys.count) + + if keysWithAlternatesLeft.contains(char) { + alternatesViewX = frame.origin.x - 4.0 + alternatesShapeLayer.path = + alternateKeysPathLeft( + startX: frame.origin.x, startY: frame.origin.y, + keyWidth: width, keyHeight: key.frame.height, numAlternates: numAlternates + ).cgPath + } else if keysWithAlternatesRight.contains(char) { + alternatesViewX = + frame.origin.x + width + - CGFloat(width * numAlternates + (3.0 * numAlternates) + 2.0) + alternatesShapeLayer.path = + alternateKeysPathRight( + startX: frame.origin.x, startY: frame.origin.y, + keyWidth: width, keyHeight: key.frame.height, numAlternates: numAlternates + ).cgPath + } + + if numAlternates > 0 { + alternatesViewWidth = CGFloat(width * numAlternates + (3.0 * numAlternates) + 8.0) + } + + alternatesViewY = frame.origin.y - key.frame.height * 1.135 + alternatesBtnHeight = key.frame.height * 0.9 + alternatesKeyView = UIView( + frame: CGRect( + x: alternatesViewX, + y: alternatesViewY, + width: alternatesViewWidth, + height: key.frame.height * 1.2 + ) + ) + + alternatesKeyView.tag = 1001 + key.backgroundColor = keyColor } - - if numAlternates > 0 { - alternatesViewWidth = CGFloat(width * numAlternates + (3.0 * numAlternates) + 8.0) - } - - alternatesViewY = frame.origin.y - key.frame.height * 1.135 - alternatesBtnHeight = key.frame.height * 0.9 - alternatesKeyView = UIView( - frame: CGRect( - x: alternatesViewX, - y: alternatesViewY, - width: alternatesViewWidth, - height: key.frame.height * 1.2 - ) - ) - - alternatesKeyView.tag = 1001 - key.backgroundColor = keyColor - } } diff --git a/Keyboards/KeyboardsBase/KeyAnimation.swift b/Keyboards/KeyboardsBase/KeyAnimation.swift index 46a8eaa1..b61f1c87 100644 --- a/Keyboards/KeyboardsBase/KeyAnimation.swift +++ b/Keyboards/KeyboardsBase/KeyAnimation.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Functions to animate key presses with pop up characters. */ @@ -15,43 +15,44 @@ import UIKit /// - char: the character of the key. /// - position: the position of the key. func setPopPathState( - startX: CGFloat, - startY: CGFloat, - keyHeight: CGFloat, - char: String, - position: String + startX: CGFloat, + startY: CGFloat, + keyHeight: CGFloat, + char: String, + position: String ) { - // Starting positions need to be updated. - horizStart = startX; vertStart = startY + keyHeight - if DeviceType.isPad { - widthMultiplier = 0.2 - maxHeightMultiplier = 2.05 - if isLandscapeView { - maxHeightMultiplier = 1.95 + // Starting positions need to be updated. + horizStart = startX + vertStart = startY + keyHeight + if DeviceType.isPad { + widthMultiplier = 0.2 + maxHeightMultiplier = 2.05 + if isLandscapeView { + maxHeightMultiplier = 1.95 + } + } else if DeviceType.isPhone && isLandscapeView { + widthMultiplier = 0.2 + maxHeightMultiplier = 2.125 + } else if DeviceType.isPhone && [".", ",", "?", "!", "'"].contains(char) { + widthMultiplier = 0.2 + maxHeightMultiplier = 2.125 + } else { + widthMultiplier = 0.4 + maxHeightMultiplier = 2.125 + } + // Non central characters have a call out that's twice the width in one direction. + if position != "center" { + widthMultiplier *= 2 + } + maxHeight = vertStart - (keyHeight * maxHeightMultiplier) + maxHeightCurveControl = vertStart - (keyHeight * (maxHeightMultiplier - 0.125)) + minHeightCurveControl = vertStart - (keyHeight * 0.005) + + if DeviceType.isPhone { + heightBeforeTopCurves = vertStart - (keyHeight * 1.8) + } else if DeviceType.isPad || (DeviceType.isPhone && isLandscapeView) { + heightBeforeTopCurves = vertStart - (keyHeight * 1.6) } - } else if DeviceType.isPhone && isLandscapeView { - widthMultiplier = 0.2 - maxHeightMultiplier = 2.125 - } else if DeviceType.isPhone && [".", ",", "?", "!", "'"].contains(char) { - widthMultiplier = 0.2 - maxHeightMultiplier = 2.125 - } else { - widthMultiplier = 0.4 - maxHeightMultiplier = 2.125 - } - // Non central characters have a call out that's twice the width in one direction. - if position != "center" { - widthMultiplier *= 2 - } - maxHeight = vertStart - (keyHeight * maxHeightMultiplier) - maxHeightCurveControl = vertStart - (keyHeight * (maxHeightMultiplier - 0.125)) - minHeightCurveControl = vertStart - (keyHeight * 0.005) - - if DeviceType.isPhone { - heightBeforeTopCurves = vertStart - (keyHeight * 1.8) - } else if DeviceType.isPad || (DeviceType.isPhone && isLandscapeView) { - heightBeforeTopCurves = vertStart - (keyHeight * 1.6) - } } /// Creates the shape that allows left most buttons to pop up after being pressed. @@ -63,57 +64,71 @@ func setPopPathState( /// - keyHeight: the height of the key. /// - char: the character of the key. func leftKeyPopPath( - startX: CGFloat, - startY: CGFloat, - keyWidth: CGFloat, - keyHeight: CGFloat, - char: String + startX: CGFloat, + startY: CGFloat, + keyWidth: CGFloat, + keyHeight: CGFloat, + char: String ) -> UIBezierPath { - setPopPathState( - startX: startX, startY: startY, keyHeight: keyHeight, char: char, position: "left" - ) - - // Path is clockwise from bottom left. - let path = UIBezierPath() - path.move(to: CGPoint(x: horizStart + (keyWidth * 0.075), y: vertStart)) - - // Curve up past bottom left, path up, and curve right past the top left. - path.addCurve( - to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.075)), - controlPoint1: CGPoint(x: horizStart + (keyWidth * 0.075), y: minHeightCurveControl), - controlPoint2: CGPoint(x: horizStart, y: minHeightCurveControl) - ) - path.addLine(to: CGPoint(x: horizStart, y: heightBeforeTopCurves)) - path.addCurve( - to: CGPoint(x: horizStart + (keyWidth * 0.35), y: maxHeight), - controlPoint1: CGPoint(x: horizStart, y: maxHeightCurveControl), - controlPoint2: CGPoint(x: horizStart + (keyWidth * 0.2), y: maxHeight) - ) - - // Path right, curve down past the top right, and path down. - path.addLine(to: CGPoint(x: horizStart + (keyWidth * (1 + widthMultiplier * 0.35)), y: maxHeight)) - path.addCurve( - to: CGPoint(x: horizStart + (keyWidth * (1 + widthMultiplier)), y: heightBeforeTopCurves * 1.15), - controlPoint1: CGPoint(x: horizStart + (keyWidth * (1 + widthMultiplier * 0.75)), y: maxHeight), - controlPoint2: CGPoint(x: horizStart + (keyWidth * (1 + widthMultiplier)), y: maxHeightCurveControl) - ) - path.addLine(to: CGPoint(x: horizStart + (keyWidth * (1 + widthMultiplier)), y: vertStart - (keyHeight * 1.3))) - - // Curve in to the left, go down, and curve down past bottom left. - path.addCurve( - to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.5)), - controlPoint1: CGPoint(x: horizStart + (keyWidth * (1 + widthMultiplier)), y: vertStart - (keyHeight * 1.05)), - controlPoint2: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.9)) - ) - path.addLine(to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.075))) - path.addCurve( - to: CGPoint(x: horizStart + (keyWidth * 0.925), y: vertStart), - controlPoint1: CGPoint(x: horizStart + keyWidth, y: minHeightCurveControl), - controlPoint2: CGPoint(x: horizStart + (keyWidth * 0.925), y: minHeightCurveControl) - ) - - path.close() - return path + setPopPathState( + startX: startX, startY: startY, keyHeight: keyHeight, char: char, position: "left" + ) + + // Path is clockwise from bottom left. + let path = UIBezierPath() + path.move(to: CGPoint(x: horizStart + (keyWidth * 0.075), y: vertStart)) + + // Curve up past bottom left, path up, and curve right past the top left. + path.addCurve( + to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.075)), + controlPoint1: CGPoint(x: horizStart + (keyWidth * 0.075), y: minHeightCurveControl), + controlPoint2: CGPoint(x: horizStart, y: minHeightCurveControl) + ) + path.addLine(to: CGPoint(x: horizStart, y: heightBeforeTopCurves)) + path.addCurve( + to: CGPoint(x: horizStart + (keyWidth * 0.35), y: maxHeight), + controlPoint1: CGPoint(x: horizStart, y: maxHeightCurveControl), + controlPoint2: CGPoint(x: horizStart + (keyWidth * 0.2), y: maxHeight) + ) + + // Path right, curve down past the top right, and path down. + path.addLine( + to: CGPoint(x: horizStart + (keyWidth * (1 + widthMultiplier * 0.35)), y: maxHeight) + ) + path.addCurve( + to: CGPoint( + x: horizStart + (keyWidth * (1 + widthMultiplier)), y: heightBeforeTopCurves * 1.15 + ), + controlPoint1: CGPoint( + x: horizStart + (keyWidth * (1 + widthMultiplier * 0.75)), y: maxHeight + ), + controlPoint2: CGPoint( + x: horizStart + (keyWidth * (1 + widthMultiplier)), y: maxHeightCurveControl + ) + ) + path.addLine( + to: CGPoint( + x: horizStart + (keyWidth * (1 + widthMultiplier)), y: vertStart - (keyHeight * 1.3) + ) + ) + + // Curve in to the left, go down, and curve down past bottom left. + path.addCurve( + to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.5)), + controlPoint1: CGPoint( + x: horizStart + (keyWidth * (1 + widthMultiplier)), y: vertStart - (keyHeight * 1.05) + ), + controlPoint2: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.9)) + ) + path.addLine(to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.075))) + path.addCurve( + to: CGPoint(x: horizStart + (keyWidth * 0.925), y: vertStart), + controlPoint1: CGPoint(x: horizStart + keyWidth, y: minHeightCurveControl), + controlPoint2: CGPoint(x: horizStart + (keyWidth * 0.925), y: minHeightCurveControl) + ) + + path.close() + return path } /// Creates the shape that allows right most buttons to pop up after being pressed. @@ -125,58 +140,65 @@ func leftKeyPopPath( /// - keyHeight: the height of the key. /// - char: the character of the key. func rightKeyPopPath( - startX: CGFloat, - startY: CGFloat, - keyWidth: CGFloat, - keyHeight: CGFloat, - char: String + startX: CGFloat, + startY: CGFloat, + keyWidth: CGFloat, + keyHeight: CGFloat, + char: String ) -> UIBezierPath { - setPopPathState( - startX: startX, startY: startY, keyHeight: keyHeight, char: char, position: "right" - ) - - // Path is clockwise from bottom left. - let path = UIBezierPath(); path.move(to: CGPoint(x: horizStart + (keyWidth * 0.075), y: vertStart)) - - // Curve up past bottom left, path up, and curve out to the left. - path.addCurve( - to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.075)), - controlPoint1: CGPoint(x: horizStart + (keyWidth * 0.075), y: minHeightCurveControl), - controlPoint2: CGPoint(x: horizStart, y: minHeightCurveControl) - ) - path.addLine(to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.5))) - path.addCurve( - to: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: vertStart - (keyHeight * 1.3)), - controlPoint1: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.9)), - controlPoint2: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: vertStart - (keyHeight * 1.05)) - ) - - // Path up and curve right past the top left. - path.addLine(to: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: heightBeforeTopCurves)) - path.addCurve( - to: CGPoint(x: horizStart - (keyWidth * widthMultiplier * 0.35), y: maxHeight), - controlPoint1: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: maxHeightCurveControl), - controlPoint2: CGPoint(x: horizStart - (keyWidth * widthMultiplier * 0.75), y: maxHeight) - ) - - // Path right, curve down past the top right, and path down. - path.addLine(to: CGPoint(x: horizStart + (keyWidth * 0.5), y: maxHeight)) - path.addCurve( - to: CGPoint(x: horizStart + keyWidth, y: heightBeforeTopCurves), - controlPoint1: CGPoint(x: horizStart + (keyWidth * 0.95), y: maxHeight), - controlPoint2: CGPoint(x: horizStart + keyWidth, y: maxHeightCurveControl) - ) - path.addLine(to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.075))) - - // Curve down past bottom left. - path.addCurve( - to: CGPoint(x: horizStart + (keyWidth * 0.925), y: vertStart), - controlPoint1: CGPoint(x: horizStart + keyWidth, y: minHeightCurveControl), - controlPoint2: CGPoint(x: horizStart + (keyWidth * 0.925), y: minHeightCurveControl) - ) - - path.close() - return path + setPopPathState( + startX: startX, startY: startY, keyHeight: keyHeight, char: char, position: "right" + ) + + // Path is clockwise from bottom left. + let path = UIBezierPath() + path.move(to: CGPoint(x: horizStart + (keyWidth * 0.075), y: vertStart)) + + // Curve up past bottom left, path up, and curve out to the left. + path.addCurve( + to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.075)), + controlPoint1: CGPoint(x: horizStart + (keyWidth * 0.075), y: minHeightCurveControl), + controlPoint2: CGPoint(x: horizStart, y: minHeightCurveControl) + ) + path.addLine(to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.5))) + path.addCurve( + to: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: vertStart - (keyHeight * 1.3)), + controlPoint1: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.9)), + controlPoint2: CGPoint( + x: horizStart - (keyWidth * widthMultiplier), y: vertStart - (keyHeight * 1.05) + ) + ) + + // Path up and curve right past the top left. + path.addLine( + to: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: heightBeforeTopCurves) + ) + path.addCurve( + to: CGPoint(x: horizStart - (keyWidth * widthMultiplier * 0.35), y: maxHeight), + controlPoint1: CGPoint( + x: horizStart - (keyWidth * widthMultiplier), y: maxHeightCurveControl + ), + controlPoint2: CGPoint(x: horizStart - (keyWidth * widthMultiplier * 0.75), y: maxHeight) + ) + + // Path right, curve down past the top right, and path down. + path.addLine(to: CGPoint(x: horizStart + (keyWidth * 0.5), y: maxHeight)) + path.addCurve( + to: CGPoint(x: horizStart + keyWidth, y: heightBeforeTopCurves), + controlPoint1: CGPoint(x: horizStart + (keyWidth * 0.95), y: maxHeight), + controlPoint2: CGPoint(x: horizStart + keyWidth, y: maxHeightCurveControl) + ) + path.addLine(to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.075))) + + // Curve down past bottom left. + path.addCurve( + to: CGPoint(x: horizStart + (keyWidth * 0.925), y: vertStart), + controlPoint1: CGPoint(x: horizStart + keyWidth, y: minHeightCurveControl), + controlPoint2: CGPoint(x: horizStart + (keyWidth * 0.925), y: minHeightCurveControl) + ) + + path.close() + return path } /// Creates the shape that allows central buttons to pop up after being pressed. @@ -188,64 +210,79 @@ func rightKeyPopPath( /// - keyHeight: the height of the key. /// - char: the character of the key. func centerKeyPopPath( - startX: CGFloat, - startY: CGFloat, - keyWidth: CGFloat, - keyHeight: CGFloat, - char: String + startX: CGFloat, + startY: CGFloat, + keyWidth: CGFloat, + keyHeight: CGFloat, + char: String ) -> UIBezierPath { - setPopPathState( - startX: startX, startY: startY, keyHeight: keyHeight, char: char, position: "center" - ) - - // Path is clockwise from bottom left. - let path = UIBezierPath(); path.move(to: CGPoint(x: horizStart + (keyWidth * 0.075), y: vertStart)) - - // Curve up past bottom left, path up, and curve out to the left. - path.addCurve( - to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.075)), - controlPoint1: CGPoint(x: horizStart + (keyWidth * 0.075), y: minHeightCurveControl), - controlPoint2: CGPoint(x: horizStart, y: minHeightCurveControl) - ) - path.addLine(to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.85))) - path.addCurve( - to: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: vertStart - (keyHeight * 1.2)), - controlPoint1: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.9)), - controlPoint2: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: vertStart - (keyHeight * 1.05)) - ) - - // Path up and curve right past the top left. - path.addLine(to: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: heightBeforeTopCurves)) - path.addCurve( - to: CGPoint(x: horizStart + (keyWidth * 0.075), y: maxHeight), - controlPoint1: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: maxHeightCurveControl), - controlPoint2: CGPoint(x: horizStart - (keyWidth * 0.25), y: maxHeight) - ) - - // Path right, curve down past the top right, and path down. - path.addLine(to: CGPoint(x: horizStart + (keyWidth * 0.925), y: maxHeight)) - path.addCurve( - to: CGPoint(x: horizStart + (keyWidth * (1 + widthMultiplier)), y: heightBeforeTopCurves), - controlPoint1: CGPoint(x: horizStart + (keyWidth * 1.25), y: maxHeight), - controlPoint2: CGPoint(x: horizStart + (keyWidth * (1 + widthMultiplier)), y: maxHeightCurveControl) - ) - path.addLine(to: CGPoint(x: horizStart + (keyWidth * (1 + widthMultiplier)), y: vertStart - (keyHeight * 1.2))) - - // Curve in to the left, go down, and curve down past bottom left. - path.addCurve( - to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.85)), - controlPoint1: CGPoint(x: horizStart + (keyWidth * (1 + widthMultiplier)), y: vertStart - (keyHeight * 1.05)), - controlPoint2: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.9)) - ) - path.addLine(to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.075))) - path.addCurve( - to: CGPoint(x: horizStart + (keyWidth * 0.925), y: vertStart), - controlPoint1: CGPoint(x: horizStart + keyWidth, y: minHeightCurveControl), - controlPoint2: CGPoint(x: horizStart + (keyWidth * 0.925), y: minHeightCurveControl) - ) - - path.close() - return path + setPopPathState( + startX: startX, startY: startY, keyHeight: keyHeight, char: char, position: "center" + ) + + // Path is clockwise from bottom left. + let path = UIBezierPath() + path.move(to: CGPoint(x: horizStart + (keyWidth * 0.075), y: vertStart)) + + // Curve up past bottom left, path up, and curve out to the left. + path.addCurve( + to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.075)), + controlPoint1: CGPoint(x: horizStart + (keyWidth * 0.075), y: minHeightCurveControl), + controlPoint2: CGPoint(x: horizStart, y: minHeightCurveControl) + ) + path.addLine(to: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.85))) + path.addCurve( + to: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: vertStart - (keyHeight * 1.2)), + controlPoint1: CGPoint(x: horizStart, y: vertStart - (keyHeight * 0.9)), + controlPoint2: CGPoint( + x: horizStart - (keyWidth * widthMultiplier), y: vertStart - (keyHeight * 1.05) + ) + ) + + // Path up and curve right past the top left. + path.addLine( + to: CGPoint(x: horizStart - (keyWidth * widthMultiplier), y: heightBeforeTopCurves) + ) + path.addCurve( + to: CGPoint(x: horizStart + (keyWidth * 0.075), y: maxHeight), + controlPoint1: CGPoint( + x: horizStart - (keyWidth * widthMultiplier), y: maxHeightCurveControl + ), + controlPoint2: CGPoint(x: horizStart - (keyWidth * 0.25), y: maxHeight) + ) + + // Path right, curve down past the top right, and path down. + path.addLine(to: CGPoint(x: horizStart + (keyWidth * 0.925), y: maxHeight)) + path.addCurve( + to: CGPoint(x: horizStart + (keyWidth * (1 + widthMultiplier)), y: heightBeforeTopCurves), + controlPoint1: CGPoint(x: horizStart + (keyWidth * 1.25), y: maxHeight), + controlPoint2: CGPoint( + x: horizStart + (keyWidth * (1 + widthMultiplier)), y: maxHeightCurveControl + ) + ) + path.addLine( + to: CGPoint( + x: horizStart + (keyWidth * (1 + widthMultiplier)), y: vertStart - (keyHeight * 1.2) + ) + ) + + // Curve in to the left, go down, and curve down past bottom left. + path.addCurve( + to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.85)), + controlPoint1: CGPoint( + x: horizStart + (keyWidth * (1 + widthMultiplier)), y: vertStart - (keyHeight * 1.05) + ), + controlPoint2: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.9)) + ) + path.addLine(to: CGPoint(x: horizStart + keyWidth, y: vertStart - (keyHeight * 0.075))) + path.addCurve( + to: CGPoint(x: horizStart + (keyWidth * 0.925), y: vertStart), + controlPoint1: CGPoint(x: horizStart + keyWidth, y: minHeightCurveControl), + controlPoint2: CGPoint(x: horizStart + (keyWidth * 0.925), y: minHeightCurveControl) + ) + + path.close() + return path } /// Creates and styles the pop up animation of a key. @@ -256,53 +293,76 @@ func centerKeyPopPath( /// - char: the character of the key. /// - displayChar: the character to display on the pop up. func getKeyPopPath(key: UIButton, layer: CAShapeLayer, char: String, displayChar: String) { - // Get the frame in respect to the superview. - if let frame = key.superview?.convert(key.frame, to: nil) { - var labelVertPosition = frame.origin.y - key.frame.height / 1.75 - // non-capital characters should be higher for portrait phone views. - if displayChar == char, DeviceType.isPhone, !isLandscapeView, - !["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"].contains(char) { - labelVertPosition = frame.origin.y - key.frame.height / 1.6 - } else if DeviceType.isPad, - isLandscapeView { - labelVertPosition = frame.origin.y - key.frame.height / 2 - } - - if centralKeyChars.contains(char) { - layer.path = centerKeyPopPath( - startX: frame.origin.x, startY: frame.origin.y, - keyWidth: key.frame.width, keyHeight: key.frame.height, char: char - ).cgPath - keyPopChar.center = CGPoint(x: frame.origin.x + key.frame.width * 0.5, y: labelVertPosition) - keyHoldPopChar.center = CGPoint(x: frame.origin.x + key.frame.width * 0.5, y: labelVertPosition) - } else if leftKeyChars.contains(char) { - layer.path = leftKeyPopPath( - startX: frame.origin.x, startY: frame.origin.y, - keyWidth: key.frame.width, keyHeight: key.frame.height, char: char - ).cgPath - keyPopChar.center = CGPoint(x: frame.origin.x + key.frame.width * 0.85, y: labelVertPosition) - keyHoldPopChar.center = CGPoint(x: frame.origin.x + key.frame.width * 0.85, y: labelVertPosition) - if DeviceType.isPad || (DeviceType.isPhone && isLandscapeView) { - keyPopChar.center = CGPoint(x: frame.origin.x + key.frame.width * 0.65, y: labelVertPosition) - keyHoldPopChar.center = CGPoint(x: frame.origin.x + key.frame.width * 0.65, y: labelVertPosition) - } - } else if rightKeyChars.contains(char) { - layer.path = rightKeyPopPath( - startX: frame.origin.x, startY: frame.origin.y, - keyWidth: key.frame.width, keyHeight: key.frame.height, char: char - ).cgPath - keyPopChar.center = CGPoint(x: frame.origin.x + key.frame.width * 0.15, y: labelVertPosition) - keyHoldPopChar.center = CGPoint(x: frame.origin.x + key.frame.width * 0.15, y: labelVertPosition) - if DeviceType.isPad || (DeviceType.isPhone && isLandscapeView) { - keyPopChar.center = CGPoint(x: frame.origin.x + key.frame.width * 0.35, y: labelVertPosition) - keyHoldPopChar.center = CGPoint(x: frame.origin.x + key.frame.width * 0.35, y: labelVertPosition) - } + // Get the frame in respect to the superview. + if let frame = key.superview?.convert(key.frame, to: nil) { + var labelVertPosition = frame.origin.y - key.frame.height / 1.75 + // non-capital characters should be higher for portrait phone views. + if displayChar == char, DeviceType.isPhone, !isLandscapeView, + !["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"].contains(char) { + labelVertPosition = frame.origin.y - key.frame.height / 1.6 + } else if DeviceType.isPad, + isLandscapeView { + labelVertPosition = frame.origin.y - key.frame.height / 2 + } + + if centralKeyChars.contains(char) { + layer.path = + centerKeyPopPath( + startX: frame.origin.x, startY: frame.origin.y, + keyWidth: key.frame.width, keyHeight: key.frame.height, char: char + ).cgPath + keyPopChar.center = CGPoint( + x: frame.origin.x + key.frame.width * 0.5, y: labelVertPosition + ) + keyHoldPopChar.center = CGPoint( + x: frame.origin.x + key.frame.width * 0.5, y: labelVertPosition + ) + } else if leftKeyChars.contains(char) { + layer.path = + leftKeyPopPath( + startX: frame.origin.x, startY: frame.origin.y, + keyWidth: key.frame.width, keyHeight: key.frame.height, char: char + ).cgPath + keyPopChar.center = CGPoint( + x: frame.origin.x + key.frame.width * 0.85, y: labelVertPosition + ) + keyHoldPopChar.center = CGPoint( + x: frame.origin.x + key.frame.width * 0.85, y: labelVertPosition + ) + if DeviceType.isPad || (DeviceType.isPhone && isLandscapeView) { + keyPopChar.center = CGPoint( + x: frame.origin.x + key.frame.width * 0.65, y: labelVertPosition + ) + keyHoldPopChar.center = CGPoint( + x: frame.origin.x + key.frame.width * 0.65, y: labelVertPosition + ) + } + } else if rightKeyChars.contains(char) { + layer.path = + rightKeyPopPath( + startX: frame.origin.x, startY: frame.origin.y, + keyWidth: key.frame.width, keyHeight: key.frame.height, char: char + ).cgPath + keyPopChar.center = CGPoint( + x: frame.origin.x + key.frame.width * 0.15, y: labelVertPosition + ) + keyHoldPopChar.center = CGPoint( + x: frame.origin.x + key.frame.width * 0.15, y: labelVertPosition + ) + if DeviceType.isPad || (DeviceType.isPhone && isLandscapeView) { + keyPopChar.center = CGPoint( + x: frame.origin.x + key.frame.width * 0.35, y: labelVertPosition + ) + keyHoldPopChar.center = CGPoint( + x: frame.origin.x + key.frame.width * 0.35, y: labelVertPosition + ) + } + } + + layer.strokeColor = keyShadowColor + layer.fillColor = keyColor.cgColor + layer.lineWidth = 1.0 } - - layer.strokeColor = keyShadowColor - layer.fillColor = keyColor.cgColor - layer.lineWidth = 1.0 - } } /// Sizes the character displayed on a key pop for iPhones. @@ -310,31 +370,32 @@ func getKeyPopPath(key: UIButton, layer: CAShapeLayer, char: String, displayChar /// - Parameters /// - char: the character of the key. func setPhoneKeyPopCharSize(char: String) { - if keyboardState != .letters && !["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"].contains(char) { - if isLandscapeView { - keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.25) - keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.25) + if keyboardState != .letters + && !["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"].contains(char) { + if isLandscapeView { + keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.25) + keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.25) + } else { + keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 1.15) + keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 1.15) + } + } else if shiftButtonState == .shift || shiftButtonState == .capsLocked { + if isLandscapeView { + keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.15) + keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.15) + } else { + keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 1) + keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 1) + } } else { - keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 1.15) - keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 1.15) + if isLandscapeView { + keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 2) + keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 2) + } else { + keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 0.9) + keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 0.9) + } } - } else if shiftButtonState == .shift || shiftButtonState == .capsLocked { - if isLandscapeView { - keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.15) - keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.15) - } else { - keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 1) - keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 1) - } - } else { - if isLandscapeView { - keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 2) - keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 2) - } else { - keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 0.9) - keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 0.9) - } - } } /// Sizes the character displayed on a key pop for iPads. @@ -342,31 +403,33 @@ func setPhoneKeyPopCharSize(char: String) { /// - Parameters /// - char: the character of the key. func setPadKeyPopCharSize(char: String) { - if keyboardState != .letters, !["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"].contains(char) { - if isLandscapeView { - keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.75) - keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.75) - } else { - keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.5) - keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.5) - } - } else if keyboardState == .letters, shiftButtonState == .shift || shiftButtonState == .capsLocked { - if isLandscapeView { - keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.5) - keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.5) - } else { - keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 2) - keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 2) - } - } else { - if isLandscapeView { - keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.25) - keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.25) + if keyboardState != .letters, + !["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"].contains(char) { + if isLandscapeView { + keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.75) + keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.75) + } else { + keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.5) + keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.5) + } + } else if keyboardState == .letters, + shiftButtonState == .shift || shiftButtonState == .capsLocked { + if isLandscapeView { + keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.5) + keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.5) + } else { + keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 2) + keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 2) + } } else { - keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 1.75) - keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 1.75) + if isLandscapeView { + keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.25) + keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 2.25) + } else { + keyPopChar.font = .systemFont(ofSize: letterKeyWidth / 1.75) + keyHoldPopChar.font = .systemFont(ofSize: letterKeyWidth / 1.75) + } } - } } /// Sizes the character displayed on a key pop. @@ -374,11 +437,11 @@ func setPadKeyPopCharSize(char: String) { /// - Parameters /// - char: the character of the key. func setKeyPopCharSize(char: String) { - if DeviceType.isPhone { - setPhoneKeyPopCharSize(char: char) - } else if DeviceType.isPad { - setPadKeyPopCharSize(char: char) - } + if DeviceType.isPhone { + setPhoneKeyPopCharSize(char: char) + } else if DeviceType.isPad { + setPadKeyPopCharSize(char: char) + } } /// Creates and styles the pop up animation of a key. @@ -389,16 +452,16 @@ func setKeyPopCharSize(char: String) { /// - char: the character of the key. /// - displayChar: the character to display on the pop up. func genKeyPop(key: UIButton, layer: CAShapeLayer, char: String, displayChar: String) { - setKeyPopCharSize(char: char) - - let popLbls = [keyPopChar, keyHoldPopChar] - for lbl in popLbls { - lbl.text = displayChar - lbl.backgroundColor = .clear - lbl.textAlignment = .center - lbl.textColor = keyCharColor - lbl.sizeToFit() - } - - getKeyPopPath(key: key, layer: layer, char: char, displayChar: displayChar) + setKeyPopCharSize(char: char) + + let popLbls = [keyPopChar, keyHoldPopChar] + for lbl in popLbls { + lbl.text = displayChar + lbl.backgroundColor = .clear + lbl.textAlignment = .center + lbl.textColor = keyCharColor + lbl.sizeToFit() + } + + getKeyPopPath(key: key, layer: layer, char: char, displayChar: displayChar) } diff --git a/Keyboards/KeyboardsBase/KeyboardKeys.swift b/Keyboards/KeyboardsBase/KeyboardKeys.swift index 00ec70d7..2f83dcea 100644 --- a/Keyboards/KeyboardsBase/KeyboardKeys.swift +++ b/Keyboards/KeyboardsBase/KeyboardKeys.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Classes and variables that define keys for Scribe keyboards. */ @@ -12,334 +12,349 @@ var paddingViews: [UIButton] = [] /// Class of UIButton that allows the tap area to be increased so that edges between keys can still receive user input. class KeyboardKey: UIButton { - // Properties for the touch area - passing negative values will expand the touch area. - var topShift = CGFloat(0) - var leftShift = CGFloat(0) - var bottomShift = CGFloat(0) - var rightShift = CGFloat(0) - - /// Allows the bounds of the key to be expanded. - override func point(inside point: CGPoint, with _: UIEvent?) -> Bool { - return bounds.inset(by: UIEdgeInsets( - top: topShift, - left: leftShift, - bottom: bottomShift, - right: rightShift - ) - ).contains(point) - } - - var row: Int! - var idx: Int! - var key: String! - - /// Styles the key with a color, corner radius and shadow. - func style() { - backgroundColor = keyColor - layer.cornerRadius = keyCornerRadius - layer.shadowColor = keyShadowColor - layer.shadowOffset = CGSize(width: 0.0, height: 1.0) - layer.shadowOpacity = 1.0 - layer.shadowRadius = 0.0 - layer.masksToBounds = false - } - - /// Sets the character of the key and defines its capitalized state. - func setChar() { - key = keyboard[row][idx] - - if key == "space" { - key = showKeyboardLanguage ? languageTextForSpaceBar : spaceBar - layer.setValue(true, forKey: "isSpecial") + // Properties for the touch area - passing negative values will expand the touch area. + var topShift = CGFloat(0) + var leftShift = CGFloat(0) + var bottomShift = CGFloat(0) + var rightShift = CGFloat(0) + + /// Allows the bounds of the key to be expanded. + override func point(inside point: CGPoint, with _: UIEvent?) -> Bool { + return bounds.inset( + by: UIEdgeInsets( + top: topShift, + left: leftShift, + bottom: bottomShift, + right: rightShift + ) + ).contains(point) } - var capsKey = "" - if key != "ß" - && key != "´" - && key != spaceBar - && key != languageTextForSpaceBar - && key != "ABC" - && key != "АБВ" { - capsKey = keyboard[row][idx].capitalized - } else { - capsKey = key + var row: Int! + var idx: Int! + var key: String! + + /// Styles the key with a color, corner radius and shadow. + func style() { + backgroundColor = keyColor + layer.cornerRadius = keyCornerRadius + layer.shadowColor = keyShadowColor + layer.shadowOffset = CGSize(width: 0.0, height: 1.0) + layer.shadowOpacity = 1.0 + layer.shadowRadius = 0.0 + layer.masksToBounds = false } - let keyToDisplay = shiftButtonState == .shift || shiftButtonState == .capsLocked ? capsKey : key - setTitleColor(keyCharColor, for: .normal) - layer.setValue(key, forKey: "original") - layer.setValue(keyToDisplay, forKey: "keyToDisplay") - layer.setValue(false, forKey: "isSpecial") - setTitle(keyToDisplay, for: .normal) // set button character - - if showKeyboardLanguage && key == languageTextForSpaceBar { - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.layer.setValue(spaceBar, forKey: "original") - self.layer.setValue(spaceBar, forKey: "keyToDisplay") - self.setTitle(spaceBar, for: .normal) - - showKeyboardLanguage = false - } + + /// Sets the character of the key and defines its capitalized state. + func setChar() { + key = keyboard[row][idx] + + if key == "space" { + key = showKeyboardLanguage ? languageTextForSpaceBar : spaceBar + layer.setValue(true, forKey: "isSpecial") + } + var capsKey = "" + + if key != "ß" + && key != "´" + && key != spaceBar + && key != languageTextForSpaceBar + && key != "ABC" + && key != "АБВ" { + capsKey = keyboard[row][idx].capitalized + } else { + capsKey = key + } + let keyToDisplay = + shiftButtonState == .shift || shiftButtonState == .capsLocked ? capsKey : key + setTitleColor(keyCharColor, for: .normal) + layer.setValue(key, forKey: "original") + layer.setValue(keyToDisplay, forKey: "keyToDisplay") + layer.setValue(false, forKey: "isSpecial") + setTitle(keyToDisplay, for: .normal) // set button character + + if showKeyboardLanguage, key == languageTextForSpaceBar { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.layer.setValue(spaceBar, forKey: "original") + self.layer.setValue(spaceBar, forKey: "keyToDisplay") + self.setTitle(spaceBar, for: .normal) + + showKeyboardLanguage = false + } + } } - } - - /// Sets the character size of a capital key if the device is an iPhone given the orientation. - func setPhoneCapCharSize() { - if isLandscapeView { - if key == "#+=" - || key == "ABC" - || key == "АБВ" - || key == "123" { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 3.5) - } else if key == spaceBar || key == languageTextForSpaceBar { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 4) - } else { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 2.9) - } - } else { - if key == "#+=" - || key == "ABC" - || key == "АБВ" - || key == "123" { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 1.75) - } else if key == spaceBar || key == languageTextForSpaceBar { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 2) - } else { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 1.5) - } + + /// Sets the character size of a capital key if the device is an iPhone given the orientation. + func setPhoneCapCharSize() { + if isLandscapeView { + if key == "#+=" + || key == "ABC" + || key == "АБВ" + || key == "123" { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 3.5) + } else if key == spaceBar || key == languageTextForSpaceBar { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 4) + } else { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 2.9) + } + } else { + if key == "#+=" + || key == "ABC" + || key == "АБВ" + || key == "123" { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 1.75) + } else if key == spaceBar || key == languageTextForSpaceBar { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 2) + } else { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 1.5) + } + } } - } - - /// Checks if the character is a lower case letter and adjusts it if so. - func checkSetPhoneLowerCharSize() { - guard let isSpecial = layer.value(forKey: "isSpecial") as? Bool else { return } - - if keyboardState == .letters - && !isSpecial - && !["123", "´", spaceBar, languageTextForSpaceBar].contains(key) - && shiftButtonState == .normal { - - if isLandscapeView { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 2.4) - } else { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 1.35) - } + + /// Checks if the character is a lower case letter and adjusts it if so. + func checkSetPhoneLowerCharSize() { + guard let isSpecial = layer.value(forKey: "isSpecial") as? Bool else { return } + + if keyboardState == .letters, + !isSpecial, + !["123", "´", spaceBar, languageTextForSpaceBar].contains(key), + shiftButtonState == .normal { + if isLandscapeView { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 2.4) + } else { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 1.35) + } + } } - } - - /// Sets the character size of a key if the device is an iPhone. - func setPhoneCharSize() { - setPhoneCapCharSize() - checkSetPhoneLowerCharSize() - } - - /// Sets the character size of a key if the device is an iPad given the orientation. - func setPadCapCharSize() { - if isLandscapeView { - if key == "#+=" - || key == "ABC" - || key == "АБВ" - || key == "hideKeyboard" { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 3.75) - } else if key == spaceBar || key == languageTextForSpaceBar { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 4.25) - } else if key == ".?123" { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 4.5) - } else { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 3.75) - } - } else { - if key == "#+=" - || key == "ABC" - || key == "АБВ" - || key == "hideKeyboard" { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 3.25) - } else if key == spaceBar || key == languageTextForSpaceBar { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 3.5) - } else if key == ".?123" { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 4) - } else { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 3) - } + + /// Sets the character size of a key if the device is an iPhone. + func setPhoneCharSize() { + setPhoneCapCharSize() + checkSetPhoneLowerCharSize() + } + + /// Sets the character size of a key if the device is an iPad given the orientation. + func setPadCapCharSize() { + if isLandscapeView { + if key == "#+=" + || key == "ABC" + || key == "АБВ" + || key == "hideKeyboard" { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 3.75) + } else if key == spaceBar || key == languageTextForSpaceBar { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 4.25) + } else if key == ".?123" { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 4.5) + } else { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 3.75) + } + } else { + if key == "#+=" + || key == "ABC" + || key == "АБВ" + || key == "hideKeyboard" { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 3.25) + } else if key == spaceBar || key == languageTextForSpaceBar { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 3.5) + } else if key == ".?123" { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 4) + } else { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 3) + } + } } - } - - /// Sets the character size of a key if the device is an iPad given the orientation. - func checkSetPadLowerCharSize() { - guard let isSpecial = layer.value(forKey: "isSpecial") as? Bool else { return } - - if keyboardState == .letters - && !isSpecial - && ![".?123", spaceBar, languageTextForSpaceBar, "ß", "´", ",", ".", "'", "-"].contains(key) - && shiftButtonState == .normal { - - if isLandscapeView { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 3.35) - } else { - titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 2.75) - } + + /// Sets the character size of a key if the device is an iPad given the orientation. + func checkSetPadLowerCharSize() { + guard let isSpecial = layer.value(forKey: "isSpecial") as? Bool else { return } + + if keyboardState == .letters, + !isSpecial, + ![".?123", spaceBar, languageTextForSpaceBar, "ß", "´", ",", ".", "'", "-"].contains( + key + ), + shiftButtonState == .normal { + if isLandscapeView { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 3.35) + } else { + titleLabel?.font = .systemFont(ofSize: letterKeyWidth / 2.75) + } + } } - } - - /// Sets the character size of a key if the device is an iPad. - func setPadCharSize() { - setPadCapCharSize() - checkSetPadLowerCharSize() - } - - /// Sets the key character sizes depending on device type and orientation. - func setCharSize() { - if DeviceType.isPhone { - setPhoneCharSize() - } else if DeviceType.isPad { - setPadCharSize() + + /// Sets the character size of a key if the device is an iPad. + func setPadCharSize() { + setPadCapCharSize() + checkSetPadLowerCharSize() } - } - - /// Adjusts the width of a key if it's one of the special characters on the iPhone keyboard. - func adjustPhoneKeyWidth() { - if ["ABC", "АБВ"].contains(key) { - layer.setValue(true, forKey: "isSpecial") - widthAnchor.constraint(equalToConstant: numSymKeyWidth * 2).isActive = true - } else if ["delete", "#+=", "shift", "selectKeyboard"].contains(key) { - if keyboardState == .letters - && ( - ( - commandState != .translate - && controllerLanguage == "Russian" - ) || ( - commandState == .translate - && getControllerTranslateLangCode() == "ru" - )) { - layer.setValue(true, forKey: "isSpecial") - widthAnchor.constraint(equalToConstant: numSymKeyWidth * 1).isActive = true - } else { - layer.setValue(true, forKey: "isSpecial") - widthAnchor.constraint(equalToConstant: numSymKeyWidth * 1.5).isActive = true - } - } else if ["123", ".?123", "return", "hideKeyboard"].contains(key) { - if row == 2 { - layer.setValue(true, forKey: "isSpecial") - widthAnchor.constraint(equalToConstant: numSymKeyWidth * 1.5).isActive = true - } else if row != 2 { - layer.setValue(true, forKey: "isSpecial") - widthAnchor.constraint(equalToConstant: numSymKeyWidth * 2).isActive = true - } - } else if (keyboardState == .numbers || keyboardState == .symbols) - && row == 2 { - // Make second row number and symbol keys wider for iPhones. - widthAnchor.constraint(equalToConstant: numSymKeyWidth * 1.4).isActive = true - } else if key != spaceBar && key != languageTextForSpaceBar { - widthAnchor.constraint(equalToConstant: keyWidth).isActive = true + + /// Sets the key character sizes depending on device type and orientation. + func setCharSize() { + if DeviceType.isPhone { + setPhoneCharSize() + } else if DeviceType.isPad { + setPadCharSize() + } } - } - - /// Adjusts the width of a key if it's one of the special characters on the iPad keyboard. - func adjustPadKeyWidth() { - if usingExpandedKeyboard { - scalarCapsLockKeyWidth = 1.3 - scalarDeleteKeyWidth = 1.65 - scalarReturnKeyWidth = 1.3 - scalarShiftKeyWidth = 1.5 - scalarSpecialKeysWidth = 1.0 - switch key { - case "ABC", "АБВ": - layer.setValue(true, forKey: "isSpecial") - widthAnchor.constraint(equalToConstant: numSymKeyWidth * 1).isActive = true - case "#+=", "selectKeyboard": - layer.setValue(true, forKey: "isSpecial") - widthAnchor.constraint(equalToConstant: numSymKeyWidth * scalarSpecialKeysWidth).isActive = true - case "delete": - layer.setValue(true, forKey: "isSpecial") - widthAnchor.constraint(equalToConstant: numSymKeyWidth * scalarDeleteKeyWidth).isActive = true - case SpecialKeys.capsLock: - layer.setValue(true, forKey: "isSpecial") - widthAnchor.constraint(equalToConstant: numSymKeyWidth * scalarCapsLockKeyWidth).isActive = true - case SpecialKeys.indent: - layer.setValue(true, forKey: "isSpecial") - widthAnchor.constraint(equalToConstant: numSymKeyWidth * scalarIndentKeyWidth).isActive = true - case "shift" where idx == 0: - layer.setValue(true, forKey: "isSpecial") - widthAnchor.constraint(equalToConstant: numSymKeyWidth * scalarShiftKeyWidth).isActive = true - case "shift" where idx > 0: - layer.setValue(true, forKey: "isSpecial") - widthAnchor.constraint(equalToConstant: numSymKeyWidth * scalarRightShiftKeyWidth).isActive = true - case "return": - layer.setValue(true, forKey: "isSpecial") - if DeviceType.isPad - && ((commandState != .translate && ["English", "Portuguese", "Italian"].contains(controllerLanguage)) || - (commandState == .translate && ["en", "pt", "it"].contains(getControllerTranslateLangCode()))) - && row == 1 { - widthAnchor.constraint(equalToConstant: numSymKeyWidth * 1.5).isActive = true - } else { - widthAnchor.constraint(equalToConstant: numSymKeyWidth * scalarReturnKeyWidth).isActive = true + + /// Adjusts the width of a key if it's one of the special characters on the iPhone keyboard. + func adjustPhoneKeyWidth() { + if ["ABC", "АБВ"].contains(key) { + layer.setValue(true, forKey: "isSpecial") + widthAnchor.constraint(equalToConstant: numSymKeyWidth * 2).isActive = true + } else if ["delete", "#+=", "shift", "selectKeyboard"].contains(key) { + if keyboardState == .letters, + (commandState != .translate + && controllerLanguage == "Russian") + || (commandState == .translate + && getControllerTranslateLangCode() == "ru") { + layer.setValue(true, forKey: "isSpecial") + widthAnchor.constraint(equalToConstant: numSymKeyWidth * 1).isActive = true + } else { + layer.setValue(true, forKey: "isSpecial") + widthAnchor.constraint(equalToConstant: numSymKeyWidth * 1.5).isActive = true + } + } else if ["123", ".?123", "return", "hideKeyboard"].contains(key) { + if row == 2 { + layer.setValue(true, forKey: "isSpecial") + widthAnchor.constraint(equalToConstant: numSymKeyWidth * 1.5).isActive = true + } else if row != 2 { + layer.setValue(true, forKey: "isSpecial") + widthAnchor.constraint(equalToConstant: numSymKeyWidth * 2).isActive = true + } + } else if keyboardState == .numbers || keyboardState == .symbols, + row == 2 { + // Make second row number and symbol keys wider for iPhones. + widthAnchor.constraint(equalToConstant: numSymKeyWidth * 1.4).isActive = true + } else if key != spaceBar, key != languageTextForSpaceBar { + widthAnchor.constraint(equalToConstant: keyWidth).isActive = true } - case "123", ".?123", "hideKeyboard": - layer.setValue(true, forKey: "isSpecial") - widthAnchor.constraint(equalToConstant: numSymKeyWidth * scalarSpecialKeysWidth).isActive = true - default: - if key != spaceBar && key != languageTextForSpaceBar { - widthAnchor.constraint(equalToConstant: keyWidth).isActive = true + } + + /// Adjusts the width of a key if it's one of the special characters on the iPad keyboard. + func adjustPadKeyWidth() { + if usingExpandedKeyboard { + scalarCapsLockKeyWidth = 1.3 + scalarDeleteKeyWidth = 1.65 + scalarReturnKeyWidth = 1.3 + scalarShiftKeyWidth = 1.5 + scalarSpecialKeysWidth = 1.0 + switch key { + case "ABC", "АБВ": + layer.setValue(true, forKey: "isSpecial") + widthAnchor.constraint(equalToConstant: numSymKeyWidth * 1).isActive = true + case "#+=", "selectKeyboard": + layer.setValue(true, forKey: "isSpecial") + widthAnchor.constraint(equalToConstant: numSymKeyWidth * scalarSpecialKeysWidth) + .isActive = + true + case "delete": + layer.setValue(true, forKey: "isSpecial") + widthAnchor.constraint(equalToConstant: numSymKeyWidth * scalarDeleteKeyWidth) + .isActive = + true + case SpecialKeys.capsLock: + layer.setValue(true, forKey: "isSpecial") + widthAnchor.constraint(equalToConstant: numSymKeyWidth * scalarCapsLockKeyWidth) + .isActive = + true + case SpecialKeys.indent: + layer.setValue(true, forKey: "isSpecial") + widthAnchor.constraint(equalToConstant: numSymKeyWidth * scalarIndentKeyWidth) + .isActive = + true + case "shift" where idx == 0: + layer.setValue(true, forKey: "isSpecial") + widthAnchor.constraint(equalToConstant: numSymKeyWidth * scalarShiftKeyWidth) + .isActive = + true + case "shift" where idx > 0: + layer.setValue(true, forKey: "isSpecial") + widthAnchor.constraint(equalToConstant: numSymKeyWidth * scalarRightShiftKeyWidth) + .isActive = true + case "return": + layer.setValue(true, forKey: "isSpecial") + if DeviceType.isPad, + (commandState != .translate + && ["English", "Portuguese", "Italian"].contains(controllerLanguage)) + || (commandState == .translate + && ["en", "pt", "it"].contains(getControllerTranslateLangCode())), + row == 1 { + widthAnchor.constraint(equalToConstant: numSymKeyWidth * 1.5).isActive = true + } else { + widthAnchor.constraint(equalToConstant: numSymKeyWidth * scalarReturnKeyWidth) + .isActive = + true + } + case "123", ".?123", "hideKeyboard": + layer.setValue(true, forKey: "isSpecial") + widthAnchor.constraint(equalToConstant: numSymKeyWidth * scalarSpecialKeysWidth) + .isActive = + true + default: + if key != spaceBar, key != languageTextForSpaceBar { + widthAnchor.constraint(equalToConstant: keyWidth).isActive = true + } + } } - } } - } - - /// Adjusts the width of a key if it's one of the special characters on the keyboard. - func adjustKeyWidth() { - if DeviceType.isPhone { - adjustPhoneKeyWidth() - } else if DeviceType.isPad { - adjustPadKeyWidth() + + /// Adjusts the width of a key if it's one of the special characters on the keyboard. + func adjustKeyWidth() { + if DeviceType.isPhone { + adjustPhoneKeyWidth() + } else if DeviceType.isPad { + adjustPadKeyWidth() + } } - } - - /// Adjusts the style of the button based on different states. - func adjustButtonStyle() { - guard let isSpecial = layer.value(forKey: "isSpecial") as? Bool else { return } - - switch key { - case SpecialKeys.indent: - backgroundColor = specialKeyColor - - case SpecialKeys.capsLock: - switch shiftButtonState { - case .capsLocked: - backgroundColor = keyPressedColor - styleIconBtn(btn: self, color: UIColor.label, iconName: "capslock.fill") - - default: - backgroundColor = specialKeyColor - styleIconBtn(btn: self, color: UIColor.label, iconName: "capslock") - } - - case "shift": - if shiftButtonState == .shift { - backgroundColor = keyPressedColor - - styleIconBtn(btn: self, color: UIColor.label, iconName: "shift.fill") - } else if DeviceType.isPhone && shiftButtonState == .capsLocked { - // We need to style the SHIFT button instead of the CAPSLOCK since the keyboard is smaller. - backgroundColor = keyPressedColor - - styleIconBtn(btn: self, color: UIColor.label, iconName: "capslock.fill") - } else { - backgroundColor = specialKeyColor - } - - case "return": - if [.translate, .conjugate, .plural].contains(commandState) { - // Color the return key depending on if it's being used as enter for commands. - backgroundColor = commandKeyColor - } else { - backgroundColor = specialKeyColor - } - - default: - if isSpecial { - backgroundColor = specialKeyColor - } + + /// Adjusts the style of the button based on different states. + func adjustButtonStyle() { + guard let isSpecial = layer.value(forKey: "isSpecial") as? Bool else { return } + + switch key { + case SpecialKeys.indent: + backgroundColor = specialKeyColor + + case SpecialKeys.capsLock: + switch shiftButtonState { + case .capsLocked: + backgroundColor = keyPressedColor + styleIconBtn(btn: self, color: UIColor.label, iconName: "capslock.fill") + + default: + backgroundColor = specialKeyColor + styleIconBtn(btn: self, color: UIColor.label, iconName: "capslock") + } + + case "shift": + if shiftButtonState == .shift { + backgroundColor = keyPressedColor + + styleIconBtn(btn: self, color: UIColor.label, iconName: "shift.fill") + } else if DeviceType.isPhone, shiftButtonState == .capsLocked { + // We need to style the SHIFT button instead of the CAPSLOCK since the keyboard is smaller. + backgroundColor = keyPressedColor + + styleIconBtn(btn: self, color: UIColor.label, iconName: "capslock.fill") + } else { + backgroundColor = specialKeyColor + } + + case "return": + if [.translate, .conjugate, .plural].contains(commandState) { + // Color the return key depending on if it's being used as enter for commands. + backgroundColor = commandKeyColor + } else { + backgroundColor = specialKeyColor + } + + default: + if isSpecial { + backgroundColor = specialKeyColor + } + } } - } } /// Sets a button's values that are displayed and inserted into the proxy as well as assigning a color. @@ -351,22 +366,22 @@ class KeyboardKey: UIButton { /// - canBeCapitalized: whether the key receives a capitalized character for the shift state. /// - isSpecial: whether the btn should be marked as special to be colored accordingly. func setBtn(btn: UIButton, color: UIColor, name: String, canBeCapitalized: Bool, isSpecial: Bool) { - btn.backgroundColor = color - btn.layer.setValue(name, forKey: "original") + btn.backgroundColor = color + btn.layer.setValue(name, forKey: "original") - let charsWithoutShiftState = ["ß"] + let charsWithoutShiftState = ["ß"] - var capsKey = "" - if canBeCapitalized { - if !charsWithoutShiftState.contains(name) { - capsKey = name.capitalized + var capsKey = "" + if canBeCapitalized { + if !charsWithoutShiftState.contains(name) { + capsKey = name.capitalized + } else { + capsKey = name + } + let shiftChar = shiftButtonState == .normal ? name : capsKey + btn.layer.setValue(shiftChar, forKey: "keyToDisplay") } else { - capsKey = name + btn.layer.setValue(name, forKey: "keyToDisplay") } - let shiftChar = shiftButtonState == .normal ? name : capsKey - btn.layer.setValue(shiftChar, forKey: "keyToDisplay") - } else { - btn.layer.setValue(name, forKey: "keyToDisplay") - } - btn.layer.setValue(isSpecial, forKey: "isSpecial") + btn.layer.setValue(isSpecial, forKey: "isSpecial") } diff --git a/Keyboards/KeyboardsBase/KeyboardStyling.swift b/Keyboards/KeyboardsBase/KeyboardStyling.swift index 5c976d7c..5feec2e2 100644 --- a/Keyboards/KeyboardsBase/KeyboardStyling.swift +++ b/Keyboards/KeyboardsBase/KeyboardStyling.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Functions to style keyboard elements. */ @@ -13,46 +13,47 @@ import UIKit /// - title: the title to be assigned. /// - radius: the corner radius of the button. func styleBtn(btn: UIButton, title: String, radius: CGFloat) { - btn.clipsToBounds = true - btn.layer.masksToBounds = false - btn.layer.cornerRadius = radius - btn.setTitle(title, for: .normal) - if title == invalidCommandMsgWikidata || title == invalidCommandMsgWiktionary { - btn.configuration = UIButton.Configuration.plain() - btn.configuration?.baseForegroundColor = UITraitCollection.current.userInterfaceStyle == .light ? specialKeyColor : keyColor - btn.configuration?.image = UIImage(systemName: "info.circle.fill") - btn.configuration?.imagePlacement = .trailing - btn.configuration?.imagePadding = 3 - } else { - btn.configuration = nil - } - btn.contentHorizontalAlignment = UIControl.ContentHorizontalAlignment.center - btn.setTitleColor(keyCharColor, for: .normal) + btn.clipsToBounds = true + btn.layer.masksToBounds = false + btn.layer.cornerRadius = radius + btn.setTitle(title, for: .normal) + if title == invalidCommandMsgWikidata || title == invalidCommandMsgWiktionary { + btn.configuration = UIButton.Configuration.plain() + btn.configuration?.baseForegroundColor = + UITraitCollection.current.userInterfaceStyle == .light ? specialKeyColor : keyColor + btn.configuration?.image = UIImage(systemName: "info.circle.fill") + btn.configuration?.imagePlacement = .trailing + btn.configuration?.imagePadding = 3 + } else { + btn.configuration = nil + } + btn.contentHorizontalAlignment = UIControl.ContentHorizontalAlignment.center + btn.setTitleColor(keyCharColor, for: .normal) - if title != "Scribe" { - btn.layer.shadowColor = keyShadowColor - btn.layer.shadowOffset = CGSize(width: 0.0, height: 1.0) - btn.layer.shadowOpacity = 1.0 - btn.layer.shadowRadius = 0.0 - } + if title != "Scribe" { + btn.layer.shadowColor = keyShadowColor + btn.layer.shadowOffset = CGSize(width: 0.0, height: 1.0) + btn.layer.shadowOpacity = 1.0 + btn.layer.shadowRadius = 0.0 + } - // Needed to prevent an unnecessary shadow. - if commandState != .selectCommand, [.one, .two, .three].contains(emojisToShow) { - btn.layer.shadowOpacity = 0 - } + // Needed to prevent an unnecessary shadow. + if commandState != .selectCommand, [.one, .two, .three].contains(emojisToShow) { + btn.layer.shadowOpacity = 0 + } } -// The names of symbols whose keys should be slightly larger than the default size. +/// The names of symbols whose keys should be slightly larger than the default size. var keysThatAreSlightlyLarger = [ - "delete.left", - "chevron.left", - "chevron.right", - "shift", - "shift.fill", - "capslock", - "capslock.fill", - "arrow.forward.to.line", - "arrowtriangle.right.fill" + "delete.left", + "chevron.left", + "chevron.right", + "shift", + "shift.fill", + "capslock", + "capslock.fill", + "arrow.forward.to.line", + "arrowtriangle.right.fill" ] /// Get the icon configurations for keys if the device is an iPhone. @@ -60,33 +61,33 @@ var keysThatAreSlightlyLarger = [ /// - Parameters /// - iconName: the name of the UIImage systemName icon to be used. func getPhoneIconConfig(iconName: String) -> UIImage.SymbolConfiguration { - var iconConfig = UIImage.SymbolConfiguration( - pointSize: letterKeyWidth / 1.75, - weight: .light, - scale: .medium - ) - if keysThatAreSlightlyLarger.contains(iconName) { - iconConfig = UIImage.SymbolConfiguration( - pointSize: letterKeyWidth / 1.55, - weight: .light, - scale: .medium - ) - } - if isLandscapeView { - iconConfig = UIImage.SymbolConfiguration( - pointSize: letterKeyWidth / 3.5, - weight: .light, - scale: .medium - ) - if keysThatAreSlightlyLarger.contains(iconName) { - iconConfig = UIImage.SymbolConfiguration( - pointSize: letterKeyWidth / 3.2, + var iconConfig = UIImage.SymbolConfiguration( + pointSize: letterKeyWidth / 1.75, weight: .light, scale: .medium - ) + ) + if keysThatAreSlightlyLarger.contains(iconName) { + iconConfig = UIImage.SymbolConfiguration( + pointSize: letterKeyWidth / 1.55, + weight: .light, + scale: .medium + ) + } + if isLandscapeView { + iconConfig = UIImage.SymbolConfiguration( + pointSize: letterKeyWidth / 3.5, + weight: .light, + scale: .medium + ) + if keysThatAreSlightlyLarger.contains(iconName) { + iconConfig = UIImage.SymbolConfiguration( + pointSize: letterKeyWidth / 3.2, + weight: .light, + scale: .medium + ) + } } - } - return iconConfig + return iconConfig } /// Get the icon configurations for keys if the device is an iPad. @@ -94,35 +95,35 @@ func getPhoneIconConfig(iconName: String) -> UIImage.SymbolConfiguration { /// - Parameters /// - iconName: the name of the UIImage systemName icon to be used. func getPadIconConfig(iconName: String) -> UIImage.SymbolConfiguration { - keysThatAreSlightlyLarger.append("globe") - var iconConfig = UIImage.SymbolConfiguration( - pointSize: letterKeyWidth / 3, - weight: .light, - scale: .medium - ) - if keysThatAreSlightlyLarger.contains(iconName) { - iconConfig = UIImage.SymbolConfiguration( - pointSize: letterKeyWidth / 2.75, - weight: .light, - scale: .medium - ) - } - if isLandscapeView { - iconConfig = UIImage.SymbolConfiguration( - pointSize: letterKeyWidth / 3.75, - weight: .light, - scale: .medium - ) - if keysThatAreSlightlyLarger.contains(iconName) { - iconConfig = UIImage.SymbolConfiguration( - pointSize: letterKeyWidth / 3.4, + keysThatAreSlightlyLarger.append("globe") + var iconConfig = UIImage.SymbolConfiguration( + pointSize: letterKeyWidth / 3, weight: .light, scale: .medium - ) + ) + if keysThatAreSlightlyLarger.contains(iconName) { + iconConfig = UIImage.SymbolConfiguration( + pointSize: letterKeyWidth / 2.75, + weight: .light, + scale: .medium + ) + } + if isLandscapeView { + iconConfig = UIImage.SymbolConfiguration( + pointSize: letterKeyWidth / 3.75, + weight: .light, + scale: .medium + ) + if keysThatAreSlightlyLarger.contains(iconName) { + iconConfig = UIImage.SymbolConfiguration( + pointSize: letterKeyWidth / 3.4, + weight: .light, + scale: .medium + ) + } } - } - keysThatAreSlightlyLarger.removeAll { $0 == "globe" } - return iconConfig + keysThatAreSlightlyLarger.removeAll { $0 == "globe" } + return iconConfig } /// Styles buttons that have icon keys. @@ -132,14 +133,14 @@ func getPadIconConfig(iconName: String) -> UIImage.SymbolConfiguration { /// - color: the tint color for the icon on the key. /// - iconName: the name of the UIImage systemName icon to be used. func styleIconBtn(btn: UIButton, color: UIColor, iconName: String, btnTitle _: String = "") { - btn.setTitle("", for: .normal) - var iconConfig = getPhoneIconConfig(iconName: iconName) - if DeviceType.isPad { - iconConfig = getPadIconConfig(iconName: iconName) - } + btn.setTitle("", for: .normal) + var iconConfig = getPhoneIconConfig(iconName: iconName) + if DeviceType.isPad { + iconConfig = getPadIconConfig(iconName: iconName) + } - btn.setImage(UIImage(systemName: iconName, withConfiguration: iconConfig), for: .normal) - btn.tintColor = color + btn.setImage(UIImage(systemName: iconName, withConfiguration: iconConfig), for: .normal) + btn.tintColor = color } /// Sets icon of delete button for pressed or non-pressed state. @@ -147,8 +148,10 @@ func styleIconBtn(btn: UIButton, color: UIColor, iconName: String, btnTitle _: S /// - button: The delete button. /// - isPressed: Determines if icon for pressed or non-pressed state will be set. func styleDeleteButton(_ button: UIButton, isPressed: Bool) { - styleIconBtn(btn: button, color: keyCharColor, - iconName: isPressed ? "delete.left.fill" : "delete.left") + styleIconBtn( + btn: button, color: keyCharColor, + iconName: isPressed ? "delete.left.fill" : "delete.left" + ) } /// Adds padding to keys to position them. @@ -158,12 +161,12 @@ func styleDeleteButton(_ button: UIButton, isPressed: Bool) { /// - width: the width of the padding. /// - key: the key associated with the button. func addPadding(to stackView: UIStackView, width: CGFloat, key _: String) { - let padding = UIButton(frame: CGRect(x: 0, y: 0, width: 0, height: 0)) - padding.setTitleColor(.clear, for: .normal) - padding.alpha = 0.0 - padding.widthAnchor.constraint(equalToConstant: width).isActive = true - padding.isUserInteractionEnabled = false + let padding = UIButton(frame: CGRect(x: 0, y: 0, width: 0, height: 0)) + padding.setTitleColor(.clear, for: .normal) + padding.alpha = 0.0 + padding.widthAnchor.constraint(equalToConstant: width).isActive = true + padding.isUserInteractionEnabled = false - paddingViews.append(padding) - stackView.addArrangedSubview(padding) + paddingViews.append(padding) + stackView.addArrangedSubview(padding) } diff --git a/Keyboards/KeyboardsBase/KeyboardViewController.swift b/Keyboards/KeyboardsBase/KeyboardViewController.swift index 15d686ef..0e1297db 100644 --- a/Keyboards/KeyboardsBase/KeyboardViewController.swift +++ b/Keyboards/KeyboardsBase/KeyboardViewController.swift @@ -1,13 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/// Classes for the parent keyboard view controller that language keyboards. +// Classes for the parent keyboard view controller that language keyboards. import GRDB import UIKit /// The parent KeyboardViewController class that is inherited by all Scribe keyboards. class KeyboardViewController: UIInputViewController { - var keyboardView: UIView! + var keyboardView: UIView! // Stack views that are populated with they keyboard rows. @IBOutlet var stackViewNum: UIStackView! @@ -16,77 +16,79 @@ class KeyboardViewController: UIInputViewController { @IBOutlet var stackView2: UIStackView! @IBOutlet var stackView3: UIStackView! - /// Changes the height of `stackViewNum` depending on device type and size. - func conditionallyShowTopNumbersRow() { - if DeviceType.isPhone { - if let stackViewNum = stackViewNum { - view.addConstraint( - NSLayoutConstraint( - item: stackViewNum, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 0 - ) - ) - } - } else if DeviceType.isPad { - // Update the size of the numbers row to add it to the view. - if usingExpandedKeyboard { - let numbersRowHeight = scribeKey.frame.height * 1.8 - if let stackViewNum = stackViewNum { - view.addConstraint( - NSLayoutConstraint( - item: stackViewNum, - attribute: .height, - relatedBy: .equal, - toItem: nil, - attribute: .height, - multiplier: 1, - constant: numbersRowHeight - ) - ) - } - } else { - if let stackViewNum = stackViewNum { - view.addConstraint( - NSLayoutConstraint( - item: stackViewNum, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 0 - ) - ) + /// Changes the height of `stackViewNum` depending on device type and size. + func conditionallyShowTopNumbersRow() { + if DeviceType.isPhone { + if let stackViewNum = stackViewNum { + view.addConstraint( + NSLayoutConstraint( + item: stackViewNum, attribute: .height, relatedBy: .equal, toItem: nil, + attribute: .height, multiplier: 1, constant: 0 + ) + ) + } + } else if DeviceType.isPad { + // Update the size of the numbers row to add it to the view. + if usingExpandedKeyboard { + let numbersRowHeight = scribeKey.frame.height * 1.8 + if let stackViewNum = stackViewNum { + view.addConstraint( + NSLayoutConstraint( + item: stackViewNum, + attribute: .height, + relatedBy: .equal, + toItem: nil, + attribute: .height, + multiplier: 1, + constant: numbersRowHeight + ) + ) + } + } else { + if let stackViewNum = stackViewNum { + view.addConstraint( + NSLayoutConstraint( + item: stackViewNum, attribute: .height, relatedBy: .equal, toItem: nil, + attribute: .height, multiplier: 1, constant: 0 + ) + ) + } + } } - } } - } - /// Changes the keyboard state such that the letters view will be shown. - func changeKeyboardToLetterKeys() { - keyboardState = .letters - loadKeys() - } + /// Changes the keyboard state such that the letters view will be shown. + func changeKeyboardToLetterKeys() { + keyboardState = .letters + loadKeys() + } - /// Changes the keyboard state such that the numbers view will be shown. - func changeKeyboardToNumberKeys() { - keyboardState = .numbers - shiftButtonState = .normal - loadKeys() - } + /// Changes the keyboard state such that the numbers view will be shown. + func changeKeyboardToNumberKeys() { + keyboardState = .numbers + shiftButtonState = .normal + loadKeys() + } - /// Changes the keyboard state such that the symbols view will be shown. - func changeKeyboardToSymbolKeys() { - keyboardState = .symbols - loadKeys() - } + /// Changes the keyboard state such that the symbols view will be shown. + func changeKeyboardToSymbolKeys() { + keyboardState = .symbols + loadKeys() + } - // MARK: Display Activation Functions + // MARK: Display Activation Functions - /// Function to load the keyboard interface into which keyboardView is instantiated. - func loadInterface() { - let keyboardNib = UINib(nibName: "Keyboard", bundle: nil) - keyboardView = keyboardNib.instantiate(withOwner: self, options: nil)[0] as? UIView - keyboardView.translatesAutoresizingMaskIntoConstraints = true - view.addSubview(keyboardView) + /// Function to load the keyboard interface into which keyboardView is instantiated. + func loadInterface() { + let keyboardNib = UINib(nibName: "Keyboard", bundle: nil) + keyboardView = keyboardNib.instantiate(withOwner: self, options: nil)[0] as? UIView + keyboardView.translatesAutoresizingMaskIntoConstraints = true + view.addSubview(keyboardView) - // Override prior command states from previous sessions. - commandState = .idle + // Override prior command states from previous sessions. + commandState = .idle - loadKeys() + loadKeys() // Set tap handler for info button on CommandBar. commandBar.infoButtonTapHandler = { [weak self] in @@ -96,184 +98,189 @@ class KeyboardViewController: UIInputViewController { } } - /// Activates a button by assigning key touch functions for their given actions. - /// - /// - Parameters - /// - btn: the button to be activated. - func activateBtn(btn: UIButton) { - btn.addTarget(self, action: #selector(executeKeyActions), for: .touchUpInside) - btn.addTarget(self, action: #selector(keyTouchDown), for: .touchDown) - btn.addTarget(self, action: #selector(keyUntouched), for: .touchDragExit) - btn.isUserInteractionEnabled = true - } - - /// Deactivates a button by removing key touch functions for their given actions and making it clear. - /// - /// - Parameters - /// - btn: the button to be deactivated. - func deactivateBtn(btn: UIButton) { - btn.setTitle("", for: .normal) - btn.configuration?.image = nil - btn.backgroundColor = UIColor.clear - btn.removeTarget(self, action: #selector(executeKeyActions), for: .touchUpInside) - btn.removeTarget(self, action: #selector(keyTouchDown), for: .touchDown) - btn.removeTarget(self, action: #selector(keyUntouched), for: .touchDragExit) - btn.isUserInteractionEnabled = false - } - - // MARK: Override UIInputViewController Functions - - /// Includes adding custom view sizing constraints. - override func updateViewConstraints() { - super.updateViewConstraints() - - checkLandscapeMode() - if DeviceType.isPhone { - if isLandscapeView { - keyboardHeight = 200 - } else { - keyboardHeight = 270 - } - } else if DeviceType.isPad { - // Expanded keyboard on larger iPads can be higher. - if UIScreen.main.bounds.width > 768 { - if isLandscapeView { - keyboardHeight = 430 - } else { - keyboardHeight = 360 - } - } else { - if isLandscapeView { - keyboardHeight = 420 - } else { - keyboardHeight = 340 + /// Activates a button by assigning key touch functions for their given actions. + /// + /// - Parameters + /// - btn: the button to be activated. + func activateBtn(btn: UIButton) { + btn.addTarget(self, action: #selector(executeKeyActions), for: .touchUpInside) + btn.addTarget(self, action: #selector(keyTouchDown), for: .touchDown) + btn.addTarget(self, action: #selector(keyUntouched), for: .touchDragExit) + btn.isUserInteractionEnabled = true + } + + /// Deactivates a button by removing key touch functions for their given actions and making it clear. + /// + /// - Parameters + /// - btn: the button to be deactivated. + func deactivateBtn(btn: UIButton) { + btn.setTitle("", for: .normal) + btn.configuration?.image = nil + btn.backgroundColor = UIColor.clear + btn.removeTarget(self, action: #selector(executeKeyActions), for: .touchUpInside) + btn.removeTarget(self, action: #selector(keyTouchDown), for: .touchDown) + btn.removeTarget(self, action: #selector(keyUntouched), for: .touchDragExit) + btn.isUserInteractionEnabled = false + } + + // MARK: Override UIInputViewController Functions + + /// Includes adding custom view sizing constraints. + override func updateViewConstraints() { + super.updateViewConstraints() + + checkLandscapeMode() + if DeviceType.isPhone { + if isLandscapeView { + keyboardHeight = 200 + } else { + keyboardHeight = 270 + } + } else if DeviceType.isPad { + // Expanded keyboard on larger iPads can be higher. + if UIScreen.main.bounds.width > 768 { + if isLandscapeView { + keyboardHeight = 430 + } else { + keyboardHeight = 360 + } + } else { + if isLandscapeView { + keyboardHeight = 420 + } else { + keyboardHeight = 340 + } + } } - } - } - guard let view = view else { - fatalError("The view is nil.") - } - - let heightConstraint = NSLayoutConstraint( - item: view, - attribute: NSLayoutConstraint.Attribute.height, - relatedBy: NSLayoutConstraint.Relation.equal, - toItem: nil, - attribute: NSLayoutConstraint.Attribute.notAnAttribute, - multiplier: 1.0, - constant: keyboardHeight - ) - view.addConstraint(heightConstraint) + guard let view = view + else { + fatalError("The view is nil.") + } - keyboardView.frame.size = view.frame.size - } + let heightConstraint = NSLayoutConstraint( + item: view, + attribute: NSLayoutConstraint.Attribute.height, + relatedBy: NSLayoutConstraint.Relation.equal, + toItem: nil, + attribute: NSLayoutConstraint.Attribute.notAnAttribute, + multiplier: 1.0, + constant: keyboardHeight + ) + view.addConstraint(heightConstraint) - // Button to be assigned as the select keyboard button if necessary. - @IBOutlet var selectKeyboardButton: UIButton! - - /// Includes the following: - /// - Assignment of the proxy - /// - Loading the Scribe interface - /// - Making keys letters - /// - Adding the keyboard selector target - override func viewDidLoad() { - super.viewDidLoad() - // If alternateKeysView is already added than remove it so it's not colored wrong. - if view.viewWithTag(1001) != nil { - let viewWithTag = view.viewWithTag(1001) - viewWithTag?.removeFromSuperview() - alternatesShapeLayer.removeFromSuperlayer() + keyboardView.frame.size = view.frame.size } - proxy = textDocumentProxy as UITextDocumentProxy - keyboardState = .letters - annotationState = false - isFirstKeyboardLoad = true - loadInterface() - isFirstKeyboardLoad = false - - selectKeyboardButton.addTarget(self, action: #selector(handleInputModeList(from:with:)), for: .allTouchEvents) - } - /// Includes hiding the keyboard selector button if it is not needed for the current device. - override func viewWillLayoutSubviews() { - selectKeyboardButton.isHidden = !needsInputModeSwitchKey - super.viewWillLayoutSubviews() - } + /// Button to be assigned as the select keyboard button if necessary. + @IBOutlet var selectKeyboardButton: UIButton! - /// Includes updateViewConstraints to change the keyboard height given device type and orientation. - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - updateViewConstraints() - isFirstKeyboardLoad = true - loadKeys() - isFirstKeyboardLoad = false - } + /// Includes the following: + /// - Assignment of the proxy + /// - Loading the Scribe interface + /// - Making keys letters + /// - Adding the keyboard selector target + override func viewDidLoad() { + super.viewDidLoad() + // If alternateKeysView is already added than remove it so it's not colored wrong. + if view.viewWithTag(1001) != nil { + let viewWithTag = view.viewWithTag(1001) + viewWithTag?.removeFromSuperview() + alternatesShapeLayer.removeFromSuperlayer() + } + proxy = textDocumentProxy as UITextDocumentProxy + keyboardState = .letters + annotationState = false + isFirstKeyboardLoad = true + loadInterface() + isFirstKeyboardLoad = false - /// Includes: - /// - updateViewConstraints to change the keyboard height - /// - A call to loadKeys to reload the display after an orientation change - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - coordinator.animate(alongsideTransition: { _ in - self.updateViewConstraints() - self.loadKeys() - }) - Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in - isFirstKeyboardLoad = true - self.loadKeys() - isFirstKeyboardLoad = false + selectKeyboardButton.addTarget( + self, action: #selector(handleInputModeList(from:with:)), for: .allTouchEvents + ) } - } - /// Overrides the previous color variables if the user switches between light and dark mode. - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - // If alternateKeysView is already added than remove it so it's not colored wrong. - if view.viewWithTag(1001) != nil { - let viewWithTag = view.viewWithTag(1001) - viewWithTag?.removeFromSuperview() - alternatesShapeLayer.removeFromSuperlayer() - } - annotationState = false - Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { _ in - isFirstKeyboardLoad = true - self.loadKeys() - isFirstKeyboardLoad = false + /// Includes hiding the keyboard selector button if it is not needed for the current device. + override func viewWillLayoutSubviews() { + selectKeyboardButton.isHidden = !needsInputModeSwitchKey + super.viewWillLayoutSubviews() } - } - - // MARK: Scribe Command Elements - // Partitions for autocomplete and autosuggest - @IBOutlet var leftAutoPartition: UILabel! - @IBOutlet var rightAutoPartition: UILabel! - - /// Sets the user interaction potential of the partitions for autocomplete and autosuggest. - func setAutoActionPartitions() { - leftAutoPartition.isUserInteractionEnabled = false - rightAutoPartition.isUserInteractionEnabled = false - } + /// Includes updateViewConstraints to change the keyboard height given device type and orientation. + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + updateViewConstraints() + isFirstKeyboardLoad = true + loadKeys() + isFirstKeyboardLoad = false + } - /// Shows the partitions for autocomplete and autosuggest. - func conditionallyShowAutoActionPartitions() { - if commandState == .idle { - if UITraitCollection.current.userInterfaceStyle == .light { - leftAutoPartition.backgroundColor = specialKeyColor - rightAutoPartition.backgroundColor = specialKeyColor - } else if UITraitCollection.current.userInterfaceStyle == .dark { - leftAutoPartition.backgroundColor = UIColor(cgColor: commandBarPlaceholderColorCG) - rightAutoPartition.backgroundColor = UIColor(cgColor: commandBarPlaceholderColorCG) - } + /// Includes: + /// - updateViewConstraints to change the keyboard height + /// - A call to loadKeys to reload the display after an orientation change + override func viewWillTransition( + to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator + ) { + super.viewWillTransition(to: size, with: coordinator) + coordinator.animate(alongsideTransition: { _ in + self.updateViewConstraints() + self.loadKeys() + }) + Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in + isFirstKeyboardLoad = true + self.loadKeys() + isFirstKeyboardLoad = false + } + } + + /// Overrides the previous color variables if the user switches between light and dark mode. + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + // If alternateKeysView is already added than remove it so it's not colored wrong. + if view.viewWithTag(1001) != nil { + let viewWithTag = view.viewWithTag(1001) + viewWithTag?.removeFromSuperview() + alternatesShapeLayer.removeFromSuperlayer() + } + annotationState = false + Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { _ in + isFirstKeyboardLoad = true + self.loadKeys() + isFirstKeyboardLoad = false + } + } + + // MARK: Scribe Command Elements + + // Partitions for autocomplete and autosuggest + @IBOutlet var leftAutoPartition: UILabel! + @IBOutlet var rightAutoPartition: UILabel! + + /// Sets the user interaction potential of the partitions for autocomplete and autosuggest. + func setAutoActionPartitions() { + leftAutoPartition.isUserInteractionEnabled = false + rightAutoPartition.isUserInteractionEnabled = false + } + + /// Shows the partitions for autocomplete and autosuggest. + func conditionallyShowAutoActionPartitions() { + if commandState == .idle { + if UITraitCollection.current.userInterfaceStyle == .light { + leftAutoPartition.backgroundColor = specialKeyColor + rightAutoPartition.backgroundColor = specialKeyColor + } else if UITraitCollection.current.userInterfaceStyle == .dark { + leftAutoPartition.backgroundColor = UIColor(cgColor: commandBarPlaceholderColorCG) + rightAutoPartition.backgroundColor = UIColor(cgColor: commandBarPlaceholderColorCG) + } + } } - } - /// Hides the partitions for autocomplete and autosuggest. - /// Note: this function is called during command mode when the commandBar is viewable and the Scribe key state. - func hideAutoActionPartitions() { - leftAutoPartition.backgroundColor = .clear - rightAutoPartition.backgroundColor = .clear - } + /// Hides the partitions for autocomplete and autosuggest. + /// Note: this function is called during command mode when the commandBar is viewable and the Scribe key state. + func hideAutoActionPartitions() { + leftAutoPartition.backgroundColor = .clear + rightAutoPartition.backgroundColor = .clear + } // Toggles visibility of the Conjugate and Plural buttons. func hideConjugateAndPluralKeys(state: Bool) { @@ -323,614 +330,712 @@ class KeyboardViewController: UIInputViewController { infoVC.didMove(toParent: self) } - /// Generate emoji suggestions or completions for a given word. - /// - /// - Parameters - /// - word: the word for which corresponding emojis should be shown for. - func getEmojiAutoSuggestions(for word: String) { - let emojisToDisplay = LanguageDBManager.shared.queryEmojis(of: word.lowercased()) + /// Generate emoji suggestions or completions for a given word. + /// + /// - Parameters + /// - word: the word for which corresponding emojis should be shown for. + func getEmojiAutoSuggestions(for word: String) { + let emojisToDisplay = LanguageDBManager.shared.queryEmojis(of: word.lowercased()) - if !emojisToDisplay[0].isEmpty { - emojisToDisplayArray = [String]() - currentEmojiTriggerWord = word.lowercased() + if !emojisToDisplay[0].isEmpty { + emojisToDisplayArray = [String]() + currentEmojiTriggerWord = word.lowercased() - if !emojisToDisplay[2].isEmpty && DeviceType.isPad { - for i in 0 ..< 3 { - emojisToDisplayArray.append(emojisToDisplay[i]) - } - autoAction2Visible = false - emojisToShow = .three + if !emojisToDisplay[2].isEmpty, DeviceType.isPad { + for i in 0 ..< 3 { + emojisToDisplayArray.append(emojisToDisplay[i]) + } + autoAction2Visible = false + emojisToShow = .three + + if UITraitCollection.current.userInterfaceStyle == .light { + padEmojiDivider0.backgroundColor = specialKeyColor + padEmojiDivider1.backgroundColor = specialKeyColor + } else if UITraitCollection.current.userInterfaceStyle == .dark { + padEmojiDivider0.backgroundColor = UIColor( + cgColor: commandBarPlaceholderColorCG + ) + padEmojiDivider1.backgroundColor = UIColor( + cgColor: commandBarPlaceholderColorCG + ) + } + conditionallyHideEmojiDividers() + } else if !emojisToDisplay[1].isEmpty { + for i in 0 ..< 2 { + emojisToDisplayArray.append(emojisToDisplay[i]) + } + autoAction2Visible = false + emojisToShow = .two + + if UITraitCollection.current.userInterfaceStyle == .light { + phoneEmojiDivider.backgroundColor = specialKeyColor + } else if UITraitCollection.current.userInterfaceStyle == .dark { + phoneEmojiDivider.backgroundColor = UIColor( + cgColor: commandBarPlaceholderColorCG + ) + } + conditionallyHideEmojiDividers() + } else { + emojisToDisplayArray.append(emojisToDisplay[0]) - if UITraitCollection.current.userInterfaceStyle == .light { - padEmojiDivider0.backgroundColor = specialKeyColor - padEmojiDivider1.backgroundColor = specialKeyColor - } else if UITraitCollection.current.userInterfaceStyle == .dark { - padEmojiDivider0.backgroundColor = UIColor(cgColor: commandBarPlaceholderColorCG) - padEmojiDivider1.backgroundColor = UIColor(cgColor: commandBarPlaceholderColorCG) - } - conditionallyHideEmojiDividers() - } else if !emojisToDisplay[1].isEmpty { - for i in 0 ..< 2 { - emojisToDisplayArray.append(emojisToDisplay[i]) + emojisToShow = .one + } } - autoAction2Visible = false - emojisToShow = .two + } + + /// Generates an array of the three autocomplete words. + func getAutocompletions() { + completionWords = [" ", " ", " "] + if let documentContext = proxy.documentContextBeforeInput, !documentContext.isEmpty { + if let inString = proxy.documentContextBeforeInput { + // To only focus on the current word as prefix in autocomplete. + currentPrefix = inString.replacingOccurrences(of: pastStringInTextProxy, with: "") + + if currentPrefix.hasPrefix("(") || currentPrefix.hasPrefix("#") + || currentPrefix.hasPrefix("/") || currentPrefix.hasPrefix("\"") { + currentPrefix = currentPrefix.replacingOccurrences( + of: #"[\"(#\/]"#, with: "", options: .regularExpression + ) + } + + // Post commands pastStringInTextProxy is "", so take last word. + if currentPrefix.contains(" ") { + currentPrefix = + currentPrefix.components( + separatedBy: " " + ).last ?? "" + } - if UITraitCollection.current.userInterfaceStyle == .light { - phoneEmojiDivider.backgroundColor = specialKeyColor - } else if UITraitCollection.current.userInterfaceStyle == .dark { - phoneEmojiDivider.backgroundColor = UIColor(cgColor: commandBarPlaceholderColorCG) + // If there's a line break, take the word after it. + if currentPrefix.contains("\n") { + currentPrefix = + currentPrefix.components( + separatedBy: "\n" + ).last ?? "" + } + + // Trigger autocompletions for selected text instead. + if proxy.selectedText != nil, + [.idle, .selectCommand, .alreadyPlural, .invalid].contains(commandState) { + if let selectedText = proxy.selectedText { + currentPrefix = selectedText + } + } + + // Get options for completion that start with the current prefix and are not just one letter. + let completionOptions = LanguageDBManager.shared.queryAutocompletions( + word: currentPrefix + ) + + if !completionOptions.isEmpty, !completionOptions[0].isEmpty { + if completionOptions.count <= 3 { + for i in 0 ..< completionOptions.count { + if shiftButtonState == .shift { + completionWords[i] = completionOptions[i].capitalize() + } else if shiftButtonState == .capsLocked { + completionWords[i] = completionOptions[i].uppercased() + } else if currentPrefix.isCapitalized { + if completionOptions[i].isUppercase { + completionWords[i] = completionOptions[i] + } else { + completionWords[i] = completionOptions[i].capitalize() + } + } else { + completionWords[i] = completionOptions[i] + } + } + } else { + for i in 0 ..< 3 { + if shiftButtonState == .shift { + completionWords[i] = completionOptions[i].capitalize() + } else if shiftButtonState == .capsLocked { + completionWords[i] = completionOptions[i].uppercased() + } else if currentPrefix.isCapitalized { + if completionOptions[i].isUppercase { + completionWords[i] = completionOptions[i] + } else { + completionWords[i] = completionOptions[i].capitalize() + } + } else { + completionWords[i] = completionOptions[i] + } + } + } + } + + // Disable the third auto action button if we'll have emoji suggestions. + if emojiAutosuggestIsEnabled() { + getEmojiAutoSuggestions(for: currentPrefix) + } + } else { + getDefaultAutosuggestions() + } + } else { + // For getting words on launch when the user hasn't typed anything in the proxy. + getDefaultAutosuggestions() } - conditionallyHideEmojiDividers() - } else { - emojisToDisplayArray.append(emojisToDisplay[0]) + } - emojisToShow = .one - } + /// Gets consistent autosguestions for all pronouns in the given language. + /// Note: currently only works for German, Spanish and French languages. + func getPronounAutosuggestions() { + let prefix = + proxy.documentContextBeforeInput?.components(separatedBy: " ").secondToLast() ?? "" + + completionWords = [String]() + for i in 0 ..< 3 { + // Get conjugations of the preselected verbs. + if let tense = pronounAutosuggestionTenses[prefix.lowercased()] { + let outputCols = [tense] + var suggestion = LanguageDBManager.shared.queryVerb( + of: verbsAfterPronounsArray[i], with: outputCols + )[0] + + if suggestion == "" { + suggestion = verbsAfterPronounsArray[i] + } + + if suggestion == "REFLEXIVE_PRONOUN", controllerLanguage == "Spanish" { + suggestion = getESReflexivePronoun(pronoun: prefix.lowercased()) + } + + switch shiftButtonState { + case .shift: + completionWords.append(suggestion.capitalize()) + case .capsLocked: + completionWords.append(suggestion.uppercased()) + default: + completionWords.append(suggestion) + } + } + } } - } - /// Generates an array of the three autocomplete words. - func getAutocompletions() { - completionWords = [" ", " ", " "] - if let documentContext = proxy.documentContextBeforeInput, !documentContext.isEmpty { - if let inString = proxy.documentContextBeforeInput { - // To only focus on the current word as prefix in autocomplete. - currentPrefix = inString.replacingOccurrences(of: pastStringInTextProxy, with: "") + /// Generates an array of three words that serve as baseline autosuggestions. + func getDefaultAutosuggestions() { + completionWords = [String]() + for i in 0 ..< 3 { + if allowUndo { + completionWords.append(previousWord) + continue + } - if currentPrefix.hasPrefix("(") || currentPrefix.hasPrefix("#") || - currentPrefix.hasPrefix("/") || currentPrefix.hasPrefix("\"") { - currentPrefix = currentPrefix.replacingOccurrences(of: #"[\"(#\/]"#, with: "", options: .regularExpression) + switch shiftButtonState { + case .shift: + completionWords.append(baseAutosuggestions[i].capitalize()) + case .capsLocked: + completionWords.append(baseAutosuggestions[i].uppercased()) + default: + completionWords.append(baseAutosuggestions[i]) + } } + } - // Post commands pastStringInTextProxy is "", so take last word. - if currentPrefix.contains(" ") { - currentPrefix = currentPrefix.components( - separatedBy: " " - ).last ?? "" + /// Generates an array of the three autosuggest words. + func getAutosuggestions() { + var prefix = + proxy.documentContextBeforeInput?.components( + separatedBy: " " + ).secondToLast() ?? "" + + if emojiAutoActionRepeatPossible { + prefix = currentEmojiTriggerWord } // If there's a line break, take the word after it. - if currentPrefix.contains("\n") { - currentPrefix = currentPrefix.components( - separatedBy: "\n" - ).last ?? "" + if prefix.contains("\n") { + prefix = + prefix.components( + separatedBy: "\n" + ).last ?? "" } // Trigger autocompletions for selected text instead. - if proxy.selectedText != nil && [.idle, .selectCommand, .alreadyPlural, .invalid].contains(commandState) { - if let selectedText = proxy.selectedText { - currentPrefix = selectedText - } + if proxy.selectedText != nil, + [.idle, .selectCommand, .alreadyPlural, .invalid].contains(commandState) { + if let selectedText = proxy.selectedText { + prefix = selectedText + } } - // Get options for completion that start with the current prefix and are not just one letter. - let completionOptions = LanguageDBManager.shared.queryAutocompletions(word: currentPrefix) + if prefix.isNumeric { + completionWords = numericAutosuggestions + } else if ["English", "French", "German", "Spanish"].contains(controllerLanguage), + pronounAutosuggestionTenses.keys.contains(prefix.lowercased()) { + getPronounAutosuggestions() + } else { + // We have to consider these different cases as the key always has to match. + // Else, even if the lowercased prefix is present in the dictionary, if the actual prefix isn't present we won't get an output. + let suggestionsLowerCasePrefix = LanguageDBManager.shared.queryAutosuggestions( + of: prefix.lowercased() + ) + let suggestionsCapitalizedPrefix = LanguageDBManager.shared.queryAutosuggestions( + of: prefix.capitalized + ) - if !completionOptions.isEmpty && !completionOptions[0].isEmpty { - if completionOptions.count <= 3 { - for i in 0 ..< completionOptions.count { - if shiftButtonState == .shift { - completionWords[i] = completionOptions[i].capitalize() - } else if shiftButtonState == .capsLocked { - completionWords[i] = completionOptions[i].uppercased() - } else if currentPrefix.isCapitalized { - if completionOptions[i].isUppercase { - completionWords[i] = completionOptions[i] - } else { - completionWords[i] = completionOptions[i].capitalize() - } - } else { - completionWords[i] = completionOptions[i] - } - } - } else { - for i in 0 ..< 3 { - if shiftButtonState == .shift { - completionWords[i] = completionOptions[i].capitalize() - } else if shiftButtonState == .capsLocked { - completionWords[i] = completionOptions[i].uppercased() - } else if currentPrefix.isCapitalized { - if completionOptions[i].isUppercase { - completionWords[i] = completionOptions[i] - } else { - completionWords[i] = completionOptions[i].capitalize() + if !suggestionsLowerCasePrefix[0].isEmpty { + completionWords = [String]() + for i in 0 ..< 3 { + if allowUndo { + completionWords.append(previousWord) + continue + } + if shiftButtonState == .shift { + completionWords.append(suggestionsLowerCasePrefix[i].capitalize()) + } else if shiftButtonState == .capsLocked { + completionWords.append(suggestionsLowerCasePrefix[i].uppercased()) + } else { + let nounForm = LanguageDBManager.shared.queryNounForm( + of: suggestionsLowerCasePrefix[i] + )[0] + hasNounForm = !nounForm.isEmpty + + if !hasNounForm { + completionWords.append(suggestionsLowerCasePrefix[i].lowercased()) + } else { + completionWords.append(suggestionsLowerCasePrefix[i]) + } + } } - } else { - completionWords[i] = completionOptions[i] - } + } else if !suggestionsCapitalizedPrefix[0].isEmpty { + completionWords = [String]() + for i in 0 ..< 3 { + if allowUndo { + completionWords.append(previousWord) + continue + } + + if shiftButtonState == .shift { + completionWords.append(suggestionsCapitalizedPrefix[i].capitalize()) + } else if shiftButtonState == .capsLocked { + completionWords.append(suggestionsCapitalizedPrefix[i].uppercased()) + } else { + completionWords.append(suggestionsCapitalizedPrefix[i]) + } + } + } else { + getDefaultAutosuggestions() } - } } // Disable the third auto action button if we'll have emoji suggestions. if emojiAutosuggestIsEnabled() { - getEmojiAutoSuggestions(for: currentPrefix) + getEmojiAutoSuggestions(for: prefix) } - } else { - getDefaultAutosuggestions() - } - } else { - // For getting words on launch when the user hasn't typed anything in the proxy. - getDefaultAutosuggestions() } - } - - /// Gets consistent autosguestions for all pronouns in the given language. - /// Note: currently only works for German, Spanish and French languages. - func getPronounAutosuggestions() { - let prefix = proxy.documentContextBeforeInput?.components(separatedBy: " ").secondToLast() ?? "" - completionWords = [String]() - for i in 0 ..< 3 { - // Get conjugations of the preselected verbs. - if let tense = pronounAutosuggestionTenses[prefix.lowercased()] { - let outputCols = [tense] - var suggestion = LanguageDBManager.shared.queryVerb(of: verbsAfterPronounsArray[i], with: outputCols)[0] + /// Sets up command buttons to execute autocomplete and autosuggest. + func conditionallySetAutoActionBtns() { + // Clear noun auto action annotations. + autoActionAnnotationBtns.forEach { $0.removeFromSuperview() } + autoActionAnnotationBtns.removeAll() + autoActionAnnotationSeparators.forEach { $0.removeFromSuperview() } + autoActionAnnotationSeparators.removeAll() - if suggestion == "" { - suggestion = verbsAfterPronounsArray[i] + if autoActionState == .suggest { + getAutosuggestions() + } else { + getAutocompletions() } + if commandState == .idle { + deactivateBtn(btn: translateKey) + deactivateBtn(btn: conjugateKey) + deactivateBtn(btn: pluralKey) - if suggestion == "REFLEXIVE_PRONOUN" && controllerLanguage == "Spanish" { - suggestion = getESReflexivePronoun(pronoun: prefix.lowercased()) - } + deactivateBtn(btn: phoneEmojiKey0) + deactivateBtn(btn: phoneEmojiKey1) + deactivateBtn(btn: padEmojiKey0) + deactivateBtn(btn: padEmojiKey1) + deactivateBtn(btn: padEmojiKey2) - switch shiftButtonState { - case .shift: - completionWords.append(suggestion.capitalize()) - case .capsLocked: - completionWords.append(suggestion.uppercased()) - default: - completionWords.append(suggestion) - } - } - } - } + if controllerLanguage == "Indonesian" { + hideConjugateAndPluralKeys(state: false) + } - /// Generates an array of three words that serve as baseline autosuggestions. - func getDefaultAutosuggestions() { - completionWords = [String]() - for i in 0 ..< 3 { - if allowUndo { - completionWords.append(previousWord) - continue - } + if autoAction0Visible { + allowUndo = false + firstCompletionIsHighlighted = false + // Highlight if the current prefix is the first autocompletion. + if currentPrefix == completionWords[0], completionWords[1] != " " { + firstCompletionIsHighlighted = true + } + setBtn( + btn: translateKey, + color: firstCompletionIsHighlighted + ? keyColor.withAlphaComponent(0.5) : keyboardBgColor, + name: "AutoAction0", + canBeCapitalized: false, + isSpecial: false + ) + styleBtn( + btn: translateKey, + title: completionWords[0], + radius: firstCompletionIsHighlighted + ? commandKeyCornerRadius / 2.5 : commandKeyCornerRadius + ) + if translateKey.currentTitle != " " { + activateBtn(btn: translateKey) + } + autoActionAnnotation(autoActionWord: completionWords[0], index: 0, KVC: self) + } - switch shiftButtonState { - case .shift: - completionWords.append(baseAutosuggestions[i].capitalize()) - case .capsLocked: - completionWords.append(baseAutosuggestions[i].uppercased()) - default: - completionWords.append(baseAutosuggestions[i]) - } - } - } + // Add the current word being typed to the completion words if there is only one option that's highlighted. + if firstCompletionIsHighlighted, completionWords[1] == " ", + completionWords[0] != currentPrefix { + // spaceAutoInsertIsPossible = true + completionWords[1] = currentPrefix + } - /// Generates an array of the three autosuggest words. - func getAutosuggestions() { - var prefix = proxy.documentContextBeforeInput?.components( - separatedBy: " " - ).secondToLast() ?? "" + setBtn( + btn: conjugateKey, + color: keyboardBgColor, name: "AutoAction1", + canBeCapitalized: false, + isSpecial: false + ) + styleBtn( + btn: conjugateKey, + title: !autoAction0Visible ? completionWords[0] : completionWords[1], + radius: commandKeyCornerRadius + ) + if conjugateKey.currentTitle != " " { + activateBtn(btn: conjugateKey) + } + autoActionAnnotation( + autoActionWord: !autoAction0Visible ? completionWords[0] : completionWords[1], + index: 1, + KVC: self + ) - if emojiAutoActionRepeatPossible { - prefix = currentEmojiTriggerWord - } + if autoAction2Visible, emojisToShow == .zero { + setBtn( + btn: pluralKey, + color: keyboardBgColor, + name: "AutoAction2", + canBeCapitalized: false, + isSpecial: false + ) + styleBtn( + btn: pluralKey, + title: !autoAction0Visible ? completionWords[1] : completionWords[2], + radius: commandKeyCornerRadius + ) + if pluralKey.currentTitle != " " { + activateBtn(btn: pluralKey) + } + autoActionAnnotation( + autoActionWord: !autoAction0Visible ? completionWords[1] : completionWords[2], + index: 2, + KVC: self + ) - // If there's a line break, take the word after it. - if prefix.contains("\n") { - prefix = prefix.components( - separatedBy: "\n" - ).last ?? "" - } + conditionallyHideEmojiDividers() + } else if autoAction2Visible, emojisToShow == .one { + setBtn( + btn: pluralKey, + color: keyboardBgColor, + name: "AutoAction2", + canBeCapitalized: false, + isSpecial: false + ) + styleBtn( + btn: pluralKey, + title: emojisToDisplayArray[0], + radius: commandKeyCornerRadius + ) + if DeviceType.isPhone { + pluralKey.titleLabel?.font = .systemFont( + ofSize: scribeKey.frame.height * scalarFontPhone + ) + } else if DeviceType.isPad { + pluralKey.titleLabel?.font = .systemFont( + ofSize: scribeKey.frame.height * scalarFontPad + ) + } + activateBtn(btn: pluralKey) + + conditionallyHideEmojiDividers() + } else if !autoAction2Visible, emojisToShow == .two { + setBtn( + btn: phoneEmojiKey0, + color: keyboardBgColor, + name: "EmojiKey0", + canBeCapitalized: false, + isSpecial: false + ) + setBtn( + btn: phoneEmojiKey1, + color: keyboardBgColor, + name: "EmojiKey1", + canBeCapitalized: false, + isSpecial: false + ) + styleBtn( + btn: phoneEmojiKey0, title: emojisToDisplayArray[0], + radius: commandKeyCornerRadius + ) + styleBtn( + btn: phoneEmojiKey1, title: emojisToDisplayArray[1], + radius: commandKeyCornerRadius + ) - // Trigger autocompletions for selected text instead. - if proxy.selectedText != nil && [.idle, .selectCommand, .alreadyPlural, .invalid].contains(commandState) { - if let selectedText = proxy.selectedText { - prefix = selectedText - } - } + if DeviceType.isPhone { + phoneEmojiKey0.titleLabel?.font = .systemFont( + ofSize: scribeKey.frame.height * scalarFontPhone + ) + phoneEmojiKey1.titleLabel?.font = .systemFont( + ofSize: scribeKey.frame.height * scalarFontPhone + ) + } else if DeviceType.isPad { + phoneEmojiKey0.titleLabel?.font = .systemFont( + ofSize: scribeKey.frame.height * scalarFontPad + ) + phoneEmojiKey1.titleLabel?.font = .systemFont( + ofSize: scribeKey.frame.height * scalarFontPad + ) + } - if prefix.isNumeric { - completionWords = numericAutosuggestions - } else if ["English", "French", "German", "Spanish"].contains(controllerLanguage) && pronounAutosuggestionTenses.keys.contains(prefix.lowercased()) { - getPronounAutosuggestions() - } else { - // We have to consider these different cases as the key always has to match. - // Else, even if the lowercased prefix is present in the dictionary, if the actual prefix isn't present we won't get an output. - let suggestionsLowerCasePrefix = LanguageDBManager.shared.queryAutosuggestions(of: prefix.lowercased()) - let suggestionsCapitalizedPrefix = LanguageDBManager.shared.queryAutosuggestions(of: prefix.capitalized) + activateBtn(btn: phoneEmojiKey0) + activateBtn(btn: phoneEmojiKey1) - if !suggestionsLowerCasePrefix[0].isEmpty { - completionWords = [String]() - for i in 0 ..< 3 { - if allowUndo { - completionWords.append(previousWord) - continue - } - if shiftButtonState == .shift { - completionWords.append(suggestionsLowerCasePrefix[i].capitalize()) - } else if shiftButtonState == .capsLocked { - completionWords.append(suggestionsLowerCasePrefix[i].uppercased()) - } else { - let nounForm = LanguageDBManager.shared.queryNounForm(of: suggestionsLowerCasePrefix[i])[0] - hasNounForm = !nounForm.isEmpty - - if !hasNounForm { - completionWords.append(suggestionsLowerCasePrefix[i].lowercased()) - } else { - completionWords.append(suggestionsLowerCasePrefix[i]) - } - } - } - } else if !suggestionsCapitalizedPrefix[0].isEmpty { - completionWords = [String]() - for i in 0 ..< 3 { - if allowUndo { - completionWords.append(previousWord) - continue - } + conditionallyHideEmojiDividers() + } else if !autoAction2Visible, emojisToShow == .three { + setBtn( + btn: padEmojiKey0, color: keyboardBgColor, name: "EmojiKey0", + canBeCapitalized: false, + isSpecial: false + ) + setBtn( + btn: padEmojiKey1, color: keyboardBgColor, name: "EmojiKey1", + canBeCapitalized: false, + isSpecial: false + ) + setBtn( + btn: padEmojiKey2, color: keyboardBgColor, name: "EmojiKey2", + canBeCapitalized: false, + isSpecial: false + ) + styleBtn( + btn: padEmojiKey0, title: emojisToDisplayArray[0], + radius: commandKeyCornerRadius + ) + styleBtn( + btn: padEmojiKey1, title: emojisToDisplayArray[1], + radius: commandKeyCornerRadius + ) + styleBtn( + btn: padEmojiKey2, title: emojisToDisplayArray[2], + radius: commandKeyCornerRadius + ) - if shiftButtonState == .shift { - completionWords.append(suggestionsCapitalizedPrefix[i].capitalize()) - } else if shiftButtonState == .capsLocked { - completionWords.append(suggestionsCapitalizedPrefix[i].uppercased()) - } else { - completionWords.append(suggestionsCapitalizedPrefix[i]) - } - } - } else { - getDefaultAutosuggestions() - } - } + padEmojiKey0.titleLabel?.font = .systemFont( + ofSize: scribeKey.frame.height * scalarEmojiKeyFont + ) + padEmojiKey1.titleLabel?.font = .systemFont( + ofSize: scribeKey.frame.height * scalarEmojiKeyFont + ) + padEmojiKey2.titleLabel?.font = .systemFont( + ofSize: scribeKey.frame.height * scalarEmojiKeyFont + ) - // Disable the third auto action button if we'll have emoji suggestions. - if emojiAutosuggestIsEnabled() { - getEmojiAutoSuggestions(for: prefix) - } - } + activateBtn(btn: padEmojiKey0) + activateBtn(btn: padEmojiKey1) + activateBtn(btn: padEmojiKey2) - /// Sets up command buttons to execute autocomplete and autosuggest. - func conditionallySetAutoActionBtns() { - // Clear noun auto action annotations. - autoActionAnnotationBtns.forEach { $0.removeFromSuperview() } - autoActionAnnotationBtns.removeAll() - autoActionAnnotationSeparators.forEach { $0.removeFromSuperview() } - autoActionAnnotationSeparators.removeAll() + conditionallyHideEmojiDividers() + } - if autoActionState == .suggest { - getAutosuggestions() - } else { - getAutocompletions() + translateKey.layer.shadowColor = UIColor.clear.cgColor + conjugateKey.layer.shadowColor = UIColor.clear.cgColor + pluralKey.layer.shadowColor = UIColor.clear.cgColor + } + + // Reset autocorrect and autosuggest button visibility. + autoAction0Visible = true + autoAction2Visible = true } - if commandState == .idle { - deactivateBtn(btn: translateKey) - deactivateBtn(btn: conjugateKey) - deactivateBtn(btn: pluralKey) - - deactivateBtn(btn: phoneEmojiKey0) - deactivateBtn(btn: phoneEmojiKey1) - deactivateBtn(btn: padEmojiKey0) - deactivateBtn(btn: padEmojiKey1) - deactivateBtn(btn: padEmojiKey2) - - if controllerLanguage == "Indonesian" { - hideConjugateAndPluralKeys(state: false) - } - if autoAction0Visible { - allowUndo = false - firstCompletionIsHighlighted = false - // Highlight if the current prefix is the first autocompletion. - if currentPrefix == completionWords[0] && completionWords[1] != " " { - firstCompletionIsHighlighted = true + /// Clears the text proxy when inserting using an auto action. + /// Note: the completion is appended after the typed text if this is not ran. + func clearPrefixFromTextFieldProxy() { + // Only delete characters for autocomplete, not autosuggest. + guard !currentPrefix.isEmpty, autoActionState != .suggest + else { + return } - setBtn( - btn: translateKey, - color: firstCompletionIsHighlighted ? keyColor.withAlphaComponent(0.5) : keyboardBgColor, - name: "AutoAction0", - canBeCapitalized: false, - isSpecial: false - ) - styleBtn( - btn: translateKey, - title: completionWords[0], - radius: firstCompletionIsHighlighted ? commandKeyCornerRadius / 2.5 : commandKeyCornerRadius - ) - if translateKey.currentTitle != " " { - activateBtn(btn: translateKey) + + guard let documentContext = proxy.documentContextBeforeInput, !documentContext.isEmpty + else { + return } - autoActionAnnotation(autoActionWord: completionWords[0], index: 0, KVC: self) - } - // Add the current word being typed to the completion words if there is only one option that's highlighted. - if firstCompletionIsHighlighted && completionWords[1] == " " && completionWords[0] != currentPrefix { -// spaceAutoInsertIsPossible = true - completionWords[1] = currentPrefix - } + // Delete characters in text proxy. + for _ in 0 ..< currentPrefix.count { + proxy.deleteBackward() + } + } - setBtn( - btn: conjugateKey, - color: keyboardBgColor, name: "AutoAction1", - canBeCapitalized: false, - isSpecial: false - ) - styleBtn( - btn: conjugateKey, - title: !autoAction0Visible ? completionWords[0] : completionWords[1], - radius: commandKeyCornerRadius - ) - if conjugateKey.currentTitle != " " { - activateBtn(btn: conjugateKey) - } - autoActionAnnotation( - autoActionWord: !autoAction0Visible ? completionWords[0] : completionWords[1], index: 1, KVC: self - ) + /// Inserts the word that appears on the given auto action key and executes all following actions. + /// + /// - Parameters + /// - keyPressed: the auto action button that was executed. + func executeAutoAction(keyPressed: UIButton) { + // Remove all prior annotations. + annotationBtns.forEach { $0.removeFromSuperview() } + annotationBtns.removeAll() + annotationSeparators.forEach { $0.removeFromSuperview() } + annotationSeparators.removeAll() - if autoAction2Visible && emojisToShow == .zero { - setBtn( - btn: pluralKey, - color: keyboardBgColor, - name: "AutoAction2", - canBeCapitalized: false, - isSpecial: false - ) - styleBtn( - btn: pluralKey, - title: !autoAction0Visible ? completionWords[1] : completionWords[2], - radius: commandKeyCornerRadius - ) - if pluralKey.currentTitle != " " { - activateBtn(btn: pluralKey) + // If user doesn't want the completion and wants what they typed back, + // Completion is made the currentPrefix to be removed from the proxy. + // Then autoActionButton title is inserted like normal. + if allowUndo && completionWords.contains(previousWord) { + // Auto Action state has to be .complete else clearPrefixFromTextFieldProxy() won't work. + autoActionState = .complete + currentPrefix = + (proxy.documentContextBeforeInput?.components(separatedBy: " ").secondToLast() ?? "") + + " " + previousWord = "" + allowUndo = false } - autoActionAnnotation( - autoActionWord: !autoAction0Visible ? completionWords[1] : completionWords[2], index: 2, KVC: self - ) - conditionallyHideEmojiDividers() - } else if autoAction2Visible && emojisToShow == .one { - setBtn( - btn: pluralKey, - color: keyboardBgColor, - name: "AutoAction2", - canBeCapitalized: false, - isSpecial: false - ) - styleBtn( - btn: pluralKey, - title: emojisToDisplayArray[0], - radius: commandKeyCornerRadius - ) - if DeviceType.isPhone { - pluralKey.titleLabel?.font = .systemFont(ofSize: scribeKey.frame.height * scalarFontPhone) - } else if DeviceType.isPad { - pluralKey.titleLabel?.font = .systemFont(ofSize: scribeKey.frame.height * scalarFontPad) + clearPrefixFromTextFieldProxy() + emojisToDisplayArray = [String]() + // Remove the space from the previous auto action or replace the current prefix. + if emojiAutoActionRepeatPossible + && ((keyPressed == phoneEmojiKey0 || keyPressed == phoneEmojiKey1) + || (keyPressed == padEmojiKey0 || keyPressed == padEmojiKey1 + || keyPressed == padEmojiKey2) + || (keyPressed == pluralKey && emojisToShow == .one)) { + proxy.deleteBackward() + } else { + currentPrefix = "" } - activateBtn(btn: pluralKey) - - conditionallyHideEmojiDividers() - } else if !autoAction2Visible && emojisToShow == .two { - setBtn( - btn: phoneEmojiKey0, - color: keyboardBgColor, - name: "EmojiKey0", - canBeCapitalized: false, - isSpecial: false - ) - setBtn( - btn: phoneEmojiKey1, - color: keyboardBgColor, - name: "EmojiKey1", - canBeCapitalized: false, - isSpecial: false - ) - styleBtn(btn: phoneEmojiKey0, title: emojisToDisplayArray[0], radius: commandKeyCornerRadius) - styleBtn(btn: phoneEmojiKey1, title: emojisToDisplayArray[1], radius: commandKeyCornerRadius) - - if DeviceType.isPhone { - phoneEmojiKey0.titleLabel?.font = .systemFont(ofSize: scribeKey.frame.height * scalarFontPhone) - phoneEmojiKey1.titleLabel?.font = .systemFont(ofSize: scribeKey.frame.height * scalarFontPhone) - } else if DeviceType.isPad { - phoneEmojiKey0.titleLabel?.font = .systemFont(ofSize: scribeKey.frame.height * scalarFontPad) - phoneEmojiKey1.titleLabel?.font = .systemFont(ofSize: scribeKey.frame.height * scalarFontPad) + proxy.insertText(keyPressed.titleLabel?.text ?? "") + proxy.insertText(" ") + autoActionState = .suggest + if shiftButtonState == .shift { + shiftButtonState = .normal + loadKeys() + } + conditionallyDisplayAnnotation() + if (keyPressed == phoneEmojiKey0 || keyPressed == phoneEmojiKey1) + || (keyPressed == padEmojiKey0 || keyPressed == padEmojiKey1 + || keyPressed == padEmojiKey2) + || (keyPressed == pluralKey && emojisToShow == .one) { + emojiAutoActionRepeatPossible = true } - - activateBtn(btn: phoneEmojiKey0) - activateBtn(btn: phoneEmojiKey1) - - conditionallyHideEmojiDividers() - } else if !autoAction2Visible && emojisToShow == .three { - setBtn(btn: padEmojiKey0, color: keyboardBgColor, name: "EmojiKey0", canBeCapitalized: false, isSpecial: false) - setBtn(btn: padEmojiKey1, color: keyboardBgColor, name: "EmojiKey1", canBeCapitalized: false, isSpecial: false) - setBtn(btn: padEmojiKey2, color: keyboardBgColor, name: "EmojiKey2", canBeCapitalized: false, isSpecial: false) - styleBtn(btn: padEmojiKey0, title: emojisToDisplayArray[0], radius: commandKeyCornerRadius) - styleBtn(btn: padEmojiKey1, title: emojisToDisplayArray[1], radius: commandKeyCornerRadius) - styleBtn(btn: padEmojiKey2, title: emojisToDisplayArray[2], radius: commandKeyCornerRadius) - - padEmojiKey0.titleLabel?.font = .systemFont(ofSize: scribeKey.frame.height * scalarEmojiKeyFont) - padEmojiKey1.titleLabel?.font = .systemFont(ofSize: scribeKey.frame.height * scalarEmojiKeyFont) - padEmojiKey2.titleLabel?.font = .systemFont(ofSize: scribeKey.frame.height * scalarEmojiKeyFont) - - activateBtn(btn: padEmojiKey0) - activateBtn(btn: padEmojiKey1) - activateBtn(btn: padEmojiKey2) - - conditionallyHideEmojiDividers() - } - - translateKey.layer.shadowColor = UIColor.clear.cgColor - conjugateKey.layer.shadowColor = UIColor.clear.cgColor - pluralKey.layer.shadowColor = UIColor.clear.cgColor } - // Reset autocorrect and autosuggest button visibility. - autoAction0Visible = true - autoAction2Visible = true - } - - /// Clears the text proxy when inserting using an auto action. - /// Note: the completion is appended after the typed text if this is not ran. - func clearPrefixFromTextFieldProxy() { - // Only delete characters for autocomplete, not autosuggest. - guard !currentPrefix.isEmpty, autoActionState != .suggest else { - return - } + /// The background for the Scribe command elements. + @IBOutlet var commandBackground: UILabel! - guard let documentContext = proxy.documentContextBeforeInput, !documentContext.isEmpty else { - return + /// Sets the background and user interactivity of the command bar. + func setCommandBackground() { + commandBackground.backgroundColor = keyboardBgColor + commandBackground.isUserInteractionEnabled = false } - // Delete characters in text proxy. - for _ in 0 ..< currentPrefix.count { - proxy.deleteBackward() - } - } + // The bar that displays language logic or is typed into for Scribe commands. + @IBOutlet var commandBar: CommandBar! + @IBOutlet var commandBarShadow: UIButton! - /// Inserts the word that appears on the given auto action key and executes all following actions. - /// - /// - Parameters - /// - keyPressed: the auto action button that was executed. - func executeAutoAction(keyPressed: UIButton) { - // Remove all prior annotations. - annotationBtns.forEach { $0.removeFromSuperview() } - annotationBtns.removeAll() - annotationSeparators.forEach { $0.removeFromSuperview() } - annotationSeparators.removeAll() - - // If user doesn't want the completion and wants what they typed back, - // Completion is made the currentPrefix to be removed from the proxy. - // Then autoActionButton title is inserted like normal. - if allowUndo && completionWords.contains(previousWord) { - // Auto Action state has to be .complete else clearPrefixFromTextFieldProxy() won't work. - autoActionState = .complete - currentPrefix = (proxy.documentContextBeforeInput?.components(separatedBy: " ").secondToLast() ?? "") + " " - previousWord = "" - allowUndo = false + /// Deletes in the proxy or command bar given the current constraints. + func handleDeleteButtonPressed() { + if [.idle, .selectCommand, .alreadyPlural, .invalid].contains(commandState) { + if wordForWordDeletionIsEnabled(), longPressOnDelete { + deleteWordBackward() + } else { + proxy.deleteBackward() + } + } else if [.translate, .conjugate, .plural].contains(commandState), + !(allPrompts.contains(commandBar.text ?? "") + || allColoredPrompts.contains(commandBar.attributedText ?? NSAttributedString())) { + guard let inputText = commandBar.text, !inputText.isEmpty + else { + return + } + commandBar.text = inputText.deletePriorToCursor() + } else { + backspaceTimer?.invalidate() + backspaceTimer = nil + } } - clearPrefixFromTextFieldProxy() - emojisToDisplayArray = [String]() - // Remove the space from the previous auto action or replace the current prefix. - if emojiAutoActionRepeatPossible && ( - (keyPressed == phoneEmojiKey0 || keyPressed == phoneEmojiKey1) - || (keyPressed == padEmojiKey0 || keyPressed == padEmojiKey1 || keyPressed == padEmojiKey2) - || (keyPressed == pluralKey && emojisToShow == .one) - ) { - proxy.deleteBackward() - } else { - currentPrefix = "" - } - proxy.insertText(keyPressed.titleLabel?.text ?? "") - proxy.insertText(" ") - autoActionState = .suggest - if shiftButtonState == .shift { - shiftButtonState = .normal - loadKeys() - } - conditionallyDisplayAnnotation() - if (keyPressed == phoneEmojiKey0 || keyPressed == phoneEmojiKey1) - || (keyPressed == padEmojiKey0 || keyPressed == padEmojiKey1 || keyPressed == padEmojiKey2) - || (keyPressed == pluralKey && emojisToShow == .one) { - emojiAutoActionRepeatPossible = true - } - } + func deleteWordBackward() { + guard let documentText = proxy.documentContextBeforeInput + else { + return + } - // The background for the Scribe command elements. - @IBOutlet var commandBackground: UILabel! + var words = documentText.split(separator: " ").map(String.init) - /// Sets the background and user interactivity of the command bar. - func setCommandBackground() { - commandBackground.backgroundColor = keyboardBgColor - commandBackground.isUserInteractionEnabled = false - } + guard !words.isEmpty + else { + return + } - // The bar that displays language logic or is typed into for Scribe commands. - @IBOutlet var commandBar: CommandBar! - @IBOutlet var commandBarShadow: UIButton! + words.removeLast() - /// Deletes in the proxy or command bar given the current constraints. - func handleDeleteButtonPressed() { - if [.idle, .selectCommand, .alreadyPlural, .invalid].contains(commandState) { - if wordForWordDeletionIsEnabled() && longPressOnDelete { - deleteWordBackward() - } else { - proxy.deleteBackward() - } - } else if [.translate, .conjugate, .plural].contains(commandState) && !(allPrompts.contains(commandBar.text ?? "") || allColoredPrompts.contains(commandBar.attributedText ?? NSAttributedString())) { - guard let inputText = commandBar.text, !inputText.isEmpty else { - return - } - commandBar.text = inputText.deletePriorToCursor() - } else { - backspaceTimer?.invalidate() - backspaceTimer = nil - } - } + let updatedText = words.joined(separator: " ") - func deleteWordBackward() { - guard let documentText = proxy.documentContextBeforeInput else { - return - } + for _ in documentText { + proxy.deleteBackward() + } - var words = documentText.split(separator: " ").map(String.init) + for character in updatedText { + proxy.insertText(String(character)) + } - guard !words.isEmpty else { - return + proxy.adjustTextPosition(byCharacterOffset: 0) } - words.removeLast() + // The button used to display Scribe commands and its shadow. + @IBOutlet var scribeKey: ScribeKey! + @IBOutlet var scribeKeyShadow: UIButton! - let updatedText = words.joined(separator: " ") - - for _ in documentText { - proxy.deleteBackward() + /// Links various UI elements that interact concurrently. + func linkShadowBlendElements() { + scribeKey.shadow = scribeKeyShadow + commandBar.shadow = commandBarShadow } - for character in updatedText { - proxy.insertText(String(character)) - } + // Buttons used to trigger Scribe command functionality. + @IBOutlet var translateKey: UIButton! + @IBOutlet var conjugateKey: UIButton! + @IBOutlet var pluralKey: UIButton! - proxy.adjustTextPosition(byCharacterOffset: 0) - } + @IBOutlet var phoneEmojiKey0: UIButton! + @IBOutlet var phoneEmojiKey1: UIButton! + @IBOutlet var phoneEmojiDivider: UILabel! - // The button used to display Scribe commands and its shadow. - @IBOutlet var scribeKey: ScribeKey! - @IBOutlet var scribeKeyShadow: UIButton! + @IBOutlet var padEmojiKey0: UIButton! + @IBOutlet var padEmojiKey1: UIButton! + @IBOutlet var padEmojiKey2: UIButton! + @IBOutlet var padEmojiDivider0: UILabel! + @IBOutlet var padEmojiDivider1: UILabel! - /// Links various UI elements that interact concurrently. - func linkShadowBlendElements() { - scribeKey.shadow = scribeKeyShadow - commandBar.shadow = commandBarShadow - } + /// Sets up all buttons that are associated with Scribe commands. + func setCommandBtns() { + setBtn( + btn: translateKey, color: commandKeyColor, name: "Translate", canBeCapitalized: false, + isSpecial: false + ) + activateBtn(btn: translateKey) - // Buttons used to trigger Scribe command functionality. - @IBOutlet var translateKey: UIButton! - @IBOutlet var conjugateKey: UIButton! - @IBOutlet var pluralKey: UIButton! - - @IBOutlet var phoneEmojiKey0: UIButton! - @IBOutlet var phoneEmojiKey1: UIButton! - @IBOutlet var phoneEmojiDivider: UILabel! - - @IBOutlet var padEmojiKey0: UIButton! - @IBOutlet var padEmojiKey1: UIButton! - @IBOutlet var padEmojiKey2: UIButton! - @IBOutlet var padEmojiDivider0: UILabel! - @IBOutlet var padEmojiDivider1: UILabel! - - /// Sets up all buttons that are associated with Scribe commands. - func setCommandBtns() { - setBtn(btn: translateKey, color: commandKeyColor, name: "Translate", canBeCapitalized: false, isSpecial: false) - activateBtn(btn: translateKey) - - if controllerLanguage != "Indonesian" { - setBtn(btn: conjugateKey, color: commandKeyColor, name: "Conjugate", canBeCapitalized: false, isSpecial: false) - setBtn(btn: pluralKey, color: commandKeyColor, name: "Plural", canBeCapitalized: false, isSpecial: false) - activateBtn(btn: conjugateKey) - activateBtn(btn: pluralKey) + if controllerLanguage != "Indonesian" { + setBtn( + btn: conjugateKey, color: commandKeyColor, name: "Conjugate", + canBeCapitalized: false, + isSpecial: false + ) + setBtn( + btn: pluralKey, color: commandKeyColor, name: "Plural", canBeCapitalized: false, + isSpecial: false + ) + activateBtn(btn: conjugateKey) + activateBtn(btn: pluralKey) + } } - } /// Hides all emoji dividers based on conditions determined by the keyboard state. func conditionallyHideEmojiDividers() { @@ -956,313 +1061,311 @@ class KeyboardViewController: UIInputViewController { } } - // MARK: Key Sizing - - func setKeywidth() { - // keyWidth determined per keyboard by the top row. - if isLandscapeView { - if DeviceType.isPhone { - letterKeyWidth = (UIScreen.main.bounds.height - 5) / CGFloat(letterKeys[0].count) * scalarLetterNumSymKeyWidthLandscapeViewPhone - numSymKeyWidth = (UIScreen.main.bounds.height - 5) / CGFloat(numberKeys[0].count) * scalarLetterNumSymKeyWidthLandscapeViewPhone - } else if DeviceType.isPad { - letterKeyWidth = (UIScreen.main.bounds.height - 5) / CGFloat(letterKeys[0].count) * scalarLetterNumSymKeyWidthLandscapeViewPad - if !usingExpandedKeyboard { - numSymKeyWidth = (UIScreen.main.bounds.height - 5) / CGFloat(numberKeys[0].count) * scalarLetterNumSymKeyWidthLandscapeViewPad - } - } - } else { - letterKeyWidth = (UIScreen.main.bounds.width - 6) / CGFloat(letterKeys[0].count) * scalarLetterNumSymKeyWidth - numSymKeyWidth = (UIScreen.main.bounds.width - 6) / CGFloat(symbolKeys[0].count) * scalarLetterNumSymKeyWidth - } - } + // MARK: Key Sizing - func setKeyPadding() { - let numRows = keyboard.count - for row in 0 ..< numRows { - for idx in 0 ..< keyboard[row].count { - // Set up button as a key with its values and properties. - let btn = KeyboardKey(type: .custom) - btn.row = row - btn.idx = idx - btn.style() - btn.setChar() - btn.setCharSize() - - let key: String = btn.key - - // Pad before key is added. - var leftPadding = CGFloat(0) - if DeviceType.isPhone - && key == "y" - && ( - ( - commandState != .translate - && ["German", "Swedish"].contains(controllerLanguage) - ) || ( - commandState == .translate - && ["de", "sv"].contains(getControllerTranslateLangCode()) - ) - ) - && !disableAccentCharacters { - leftPadding = keyWidth / 3 - addPadding(to: stackView2, width: leftPadding, key: "y") - } - if DeviceType.isPhone - && key == "a" - && ( - ( - commandState != .translate - && ( - ["English", "Indonesian", "Italian", "Portuguese"].contains(controllerLanguage) - || ( - ["German", "Spanish", "Swedish"].contains(controllerLanguage) - && disableAccentCharacters - ) - ) - ) || ( - commandState == .translate - && ["en", "id", "it", "pt"].contains(getControllerTranslateLangCode()) - )) { - leftPadding = keyWidth / 4 - addPadding(to: stackView1, width: leftPadding, key: "a") - } - if DeviceType.isPad - && key == "a" - && !usingExpandedKeyboard - && ( - ( - commandState != .translate - && ["English", "Indonesian", "Italian", "Portuguese"].contains(controllerLanguage) - ) || ( - commandState == .translate - && ["en", "id", "it", "pt"].contains(getControllerTranslateLangCode()) - )) { - leftPadding = keyWidth / 3 - addPadding(to: stackView1, width: leftPadding, key: "a") - } - if DeviceType.isPad - && key == "@" - && !usingExpandedKeyboard - && ( - ( - commandState != .translate - && ["English", "Italian", "Portuguese"].contains(controllerLanguage) - ) || ( - commandState == .translate - && ["en", "it", "pt"].contains(getControllerTranslateLangCode()) - )) { - leftPadding = keyWidth / 3 - addPadding(to: stackView1, width: leftPadding, key: "@") - } - if DeviceType.isPad - && key == "€" - && !usingExpandedKeyboard - && ( - ( - commandState != .translate - && ["English", "Portuguese"].contains(controllerLanguage) - ) || ( - commandState == .translate - && ["en", "pt"].contains(getControllerTranslateLangCode()) - )) { - leftPadding = keyWidth / 3 - addPadding(to: stackView1, width: leftPadding, key: "€") - } - - keyboardKeys.append(btn) - if !usingExpandedKeyboard { - switch row { - case 0: stackView0.addArrangedSubview(btn) - case 1: stackView1.addArrangedSubview(btn) - case 2: stackView2.addArrangedSubview(btn) - case 3: stackView3.addArrangedSubview(btn) - default: - break - } + func setKeywidth() { + // keyWidth determined per keyboard by the top row. + if isLandscapeView { + if DeviceType.isPhone { + letterKeyWidth = + (UIScreen.main.bounds.height - 5) / CGFloat(letterKeys[0].count) + * scalarLetterNumSymKeyWidthLandscapeViewPhone + numSymKeyWidth = + (UIScreen.main.bounds.height - 5) / CGFloat(numberKeys[0].count) + * scalarLetterNumSymKeyWidthLandscapeViewPhone + } else if DeviceType.isPad { + letterKeyWidth = + (UIScreen.main.bounds.height - 5) / CGFloat(letterKeys[0].count) + * scalarLetterNumSymKeyWidthLandscapeViewPad + if !usingExpandedKeyboard { + numSymKeyWidth = + (UIScreen.main.bounds.height - 5) / CGFloat(numberKeys[0].count) + * scalarLetterNumSymKeyWidthLandscapeViewPad + } + } } else { - switch row { - case 0: stackViewNum.addArrangedSubview(btn) - case 1: stackView0.addArrangedSubview(btn) - case 2: stackView1.addArrangedSubview(btn) - case 3: stackView2.addArrangedSubview(btn) - case 4: stackView3.addArrangedSubview(btn) - default: - break - } - } + letterKeyWidth = + (UIScreen.main.bounds.width - 6) / CGFloat(letterKeys[0].count) + * scalarLetterNumSymKeyWidth + numSymKeyWidth = + (UIScreen.main.bounds.width - 6) / CGFloat(symbolKeys[0].count) + * scalarLetterNumSymKeyWidth + } + } + + func setKeyPadding() { + let numRows = keyboard.count + for row in 0 ..< numRows { + for idx in 0 ..< keyboard[row].count { + // Set up button as a key with its values and properties. + let btn = KeyboardKey(type: .custom) + btn.row = row + btn.idx = idx + btn.style() + btn.setChar() + btn.setCharSize() + + let key: String = btn.key + + // Pad before key is added. + var leftPadding = CGFloat(0) + if DeviceType.isPhone + && key == "y" + && ((commandState != .translate + && ["German", "Swedish"].contains(controllerLanguage)) + || (commandState == .translate + && ["de", "sv"].contains(getControllerTranslateLangCode()))) + && !disableAccentCharacters { + leftPadding = keyWidth / 3 + addPadding(to: stackView2, width: leftPadding, key: "y") + } + if DeviceType.isPhone + && key == "a" + && ((commandState != .translate + && (["English", "Indonesian", "Italian", "Portuguese"].contains( + controllerLanguage + ) + || (["German", "Spanish", "Swedish"].contains(controllerLanguage) + && disableAccentCharacters))) + || (commandState == .translate + && ["en", "id", "it", "pt"].contains(getControllerTranslateLangCode()))) { + leftPadding = keyWidth / 4 + addPadding(to: stackView1, width: leftPadding, key: "a") + } + if DeviceType.isPad + && key == "a" + && !usingExpandedKeyboard + && ((commandState != .translate + && ["English", "Indonesian", "Italian", "Portuguese"].contains( + controllerLanguage + )) + || (commandState == .translate + && ["en", "id", "it", "pt"].contains(getControllerTranslateLangCode()))) { + leftPadding = keyWidth / 3 + addPadding(to: stackView1, width: leftPadding, key: "a") + } + if DeviceType.isPad + && key == "@" + && !usingExpandedKeyboard + && ((commandState != .translate + && ["English", "Italian", "Portuguese"].contains(controllerLanguage)) + || (commandState == .translate + && ["en", "it", "pt"].contains(getControllerTranslateLangCode()))) { + leftPadding = keyWidth / 3 + addPadding(to: stackView1, width: leftPadding, key: "@") + } + if DeviceType.isPad + && key == "€" + && !usingExpandedKeyboard + && ((commandState != .translate + && ["English", "Portuguese"].contains(controllerLanguage)) + || (commandState == .translate + && ["en", "pt"].contains(getControllerTranslateLangCode()))) { + leftPadding = keyWidth / 3 + addPadding(to: stackView1, width: leftPadding, key: "€") + } - // Special key styling. - if key == "delete" { - let deleteLongPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(deleteLongPressed(_:))) - btn.addGestureRecognizer(deleteLongPressRecognizer) - } + keyboardKeys.append(btn) + if !usingExpandedKeyboard { + switch row { + case 0: stackView0.addArrangedSubview(btn) + case 1: stackView1.addArrangedSubview(btn) + case 2: stackView2.addArrangedSubview(btn) + case 3: stackView3.addArrangedSubview(btn) + default: + break + } + } else { + switch row { + case 0: stackViewNum.addArrangedSubview(btn) + case 1: stackView0.addArrangedSubview(btn) + case 2: stackView1.addArrangedSubview(btn) + case 3: stackView2.addArrangedSubview(btn) + case 4: stackView3.addArrangedSubview(btn) + default: + break + } + } - if key == "selectKeyboard" { - selectKeyboardButton = btn - selectKeyboardButton.addTarget( - self, - action: #selector(handleInputModeList(from:with:)), - for: .allTouchEvents - ) - styleIconBtn(btn: btn, color: keyCharColor, iconName: "globe") - } + // Special key styling. + if key == "delete" { + let deleteLongPressRecognizer = UILongPressGestureRecognizer( + target: self, action: #selector(deleteLongPressed(_:)) + ) + btn.addGestureRecognizer(deleteLongPressRecognizer) + } - if key == "hideKeyboard" { - styleIconBtn(btn: btn, color: keyCharColor, iconName: "keyboard.chevron.compact.down") - } + if key == "selectKeyboard" { + selectKeyboardButton = btn + selectKeyboardButton.addTarget( + self, + action: #selector(handleInputModeList(from:with:)), + for: .allTouchEvents + ) + styleIconBtn(btn: btn, color: keyCharColor, iconName: "globe") + } - if key == SpecialKeys.indent { - styleIconBtn(btn: btn, color: keyCharColor, iconName: "arrow.forward.to.line") - } + if key == "hideKeyboard" { + styleIconBtn( + btn: btn, color: keyCharColor, iconName: "keyboard.chevron.compact.down" + ) + } - if key == SpecialKeys.capsLock { - styleIconBtn(btn: btn, color: keyCharColor, iconName: "capslock") - } + if key == SpecialKeys.indent { + styleIconBtn(btn: btn, color: keyCharColor, iconName: "arrow.forward.to.line") + } - if key == "shift" { - styleIconBtn(btn: btn, color: keyCharColor, iconName: "shift") - } + if key == SpecialKeys.capsLock { + styleIconBtn(btn: btn, color: keyCharColor, iconName: "capslock") + } - if key == "return" { - if [.translate, .conjugate, .plural].contains(commandState) { - styleIconBtn(btn: btn, color: keyCharColor, iconName: "arrowtriangle.right.fill") - } else { - styleIconBtn(btn: btn, color: keyCharColor, iconName: "arrow.turn.down.left") - } - } + if key == "shift" { + styleIconBtn(btn: btn, color: keyCharColor, iconName: "shift") + } - if key == "delete" { - styleIconBtn(btn: btn, color: keyCharColor, iconName: "delete.left") - } + if key == "return" { + if [.translate, .conjugate, .plural].contains(commandState) { + styleIconBtn( + btn: btn, color: keyCharColor, iconName: "arrowtriangle.right.fill" + ) + } else { + styleIconBtn( + btn: btn, color: keyCharColor, iconName: "arrow.turn.down.left" + ) + } + } - // Setting key pop functionality. - let keyHoldPop = UILongPressGestureRecognizer( - target: self, - action: #selector(genHoldPopUpView(sender:)) - ) - keyHoldPop.minimumPressDuration = 0.125 - - if allNonSpecialKeys.contains(key) { - btn.addTarget(self, action: #selector(genPopUpView), for: .touchDown) - btn.addGestureRecognizer(keyHoldPop) - } - - // Pad after key is added. - var rightPadding = CGFloat(0) - if DeviceType.isPhone - && key == "m" - && ( - ( - commandState != .translate - && ["German", "Swedish"].contains(controllerLanguage) - ) || ( - commandState == .translate - && ["de", "sv"].contains(getControllerTranslateLangCode()) - ) - ) - && !disableAccentCharacters { - rightPadding = keyWidth / 3 - addPadding(to: stackView2, width: rightPadding, key: "m") - } - if DeviceType.isPhone - && key == "l" - && ( - ( - commandState != .translate - && ( - ["English", "Indonesian", "Italian", "Portuguese"].contains(controllerLanguage) - || ( - ["German", "Spanish", "Swedish"].contains(controllerLanguage) - && disableAccentCharacters + if key == "delete" { + styleIconBtn(btn: btn, color: keyCharColor, iconName: "delete.left") + } + + // Setting key pop functionality. + let keyHoldPop = UILongPressGestureRecognizer( + target: self, + action: #selector(genHoldPopUpView(sender:)) ) - ) - ) || ( - commandState == .translate - && ["en", "id", "it", "pt"].contains(getControllerTranslateLangCode()) - )) { - rightPadding = keyWidth / 4 - addPadding(to: stackView1, width: rightPadding, key: "l") - } - - // Set the width of the key given device and the given key. - btn.adjustKeyWidth() - - // Update the button style. - btn.adjustButtonStyle() - - if key == "return" && proxy.keyboardType == .webSearch && ![.translate, .conjugate, .plural].contains(commandState) { - // Override background color from adjustKeyWidth for "search" blue for web searches. - styleIconBtn(btn: btn, color: .white.withAlphaComponent(0.9), iconName: "arrow.turn.down.left") - btn.backgroundColor = UIColor(red: 0.0 / 255.0, green: 121.0 / 255.0, blue: 251.0 / 255.0, alpha: 1.0) - } - - // Extend button touch areas. - var widthOfSpacing = CGFloat(0) - if keyboardState == .letters { - widthOfSpacing = ( - (UIScreen.main.bounds.width - 6.0) - - (CGFloat(letterKeys[0].count) * keyWidth) - ) / (CGFloat(letterKeys[0].count) - - 1.0 - ) - } else { - widthOfSpacing = ( - (UIScreen.main.bounds.width - 6.0) - - (CGFloat(usingExpandedKeyboard == true ? symbolKeys[0].count : numberKeys[0].count) * numSymKeyWidth) - ) / (CGFloat(letterKeys[0].count) - - 1.0 - ) - } - - switch row { - case 0: - btn.topShift = -5 - btn.bottomShift = -6 - case 1: - btn.topShift = -6 - btn.bottomShift = -6 - case 2: - btn.topShift = -6 - btn.bottomShift = -6 - case 3: - btn.topShift = -6 - btn.bottomShift = -5 - default: - break - } + keyHoldPop.minimumPressDuration = 0.125 - // Pad left and right based on if the button has been shifted. - if leftPadding == CGFloat(0) { - btn.leftShift = -(widthOfSpacing / 2) - } else { - btn.leftShift = -leftPadding - } - if rightPadding == CGFloat(0) { - btn.rightShift = -(widthOfSpacing / 2) - } else { - btn.rightShift = -rightPadding - } + if allNonSpecialKeys.contains(key) { + btn.addTarget(self, action: #selector(genPopUpView), for: .touchDown) + btn.addGestureRecognizer(keyHoldPop) + } + + // Pad after key is added. + var rightPadding = CGFloat(0) + if DeviceType.isPhone + && key == "m" + && ((commandState != .translate + && ["German", "Swedish"].contains(controllerLanguage)) + || (commandState == .translate + && ["de", "sv"].contains(getControllerTranslateLangCode()))) + && !disableAccentCharacters { + rightPadding = keyWidth / 3 + addPadding(to: stackView2, width: rightPadding, key: "m") + } + if DeviceType.isPhone + && key == "l" + && ((commandState != .translate + && (["English", "Indonesian", "Italian", "Portuguese"].contains( + controllerLanguage + ) + || (["German", "Spanish", "Swedish"].contains(controllerLanguage) + && disableAccentCharacters))) + || (commandState == .translate + && ["en", "id", "it", "pt"].contains(getControllerTranslateLangCode()))) { + rightPadding = keyWidth / 4 + addPadding(to: stackView1, width: rightPadding, key: "l") + } + + // Set the width of the key given device and the given key. + btn.adjustKeyWidth() + + // Update the button style. + btn.adjustButtonStyle() + + if key == "return" && proxy.keyboardType == .webSearch + && ![.translate, .conjugate, .plural].contains(commandState) { + // Override background color from adjustKeyWidth for "search" blue for web searches. + styleIconBtn( + btn: btn, color: .white.withAlphaComponent(0.9), + iconName: "arrow.turn.down.left" + ) + btn.backgroundColor = UIColor( + red: 0.0 / 255.0, green: 121.0 / 255.0, blue: 251.0 / 255.0, alpha: 1.0 + ) + } + + // Extend button touch areas. + var widthOfSpacing = CGFloat(0) + if keyboardState == .letters { + widthOfSpacing = + ((UIScreen.main.bounds.width - 6.0) + - (CGFloat(letterKeys[0].count) * keyWidth)) + / (CGFloat(letterKeys[0].count) + - 1.0) + } else { + widthOfSpacing = + ((UIScreen.main.bounds.width - 6.0) + - (CGFloat( + usingExpandedKeyboard == true + ? symbolKeys[0].count : numberKeys[0].count + ) + * numSymKeyWidth)) + / (CGFloat(letterKeys[0].count) + - 1.0) + } + + switch row { + case 0: + btn.topShift = -5 + btn.bottomShift = -6 + case 1: + btn.topShift = -6 + btn.bottomShift = -6 + case 2: + btn.topShift = -6 + btn.bottomShift = -6 + case 3: + btn.topShift = -6 + btn.bottomShift = -5 + default: + break + } + + // Pad left and right based on if the button has been shifted. + if leftPadding == CGFloat(0) { + btn.leftShift = -(widthOfSpacing / 2) + } else { + btn.leftShift = -leftPadding + } + if rightPadding == CGFloat(0) { + btn.rightShift = -(widthOfSpacing / 2) + } else { + btn.rightShift = -rightPadding + } - // Activate keyboard interface buttons. - activateBtn(btn: btn) - if key == "shift" || key == spaceBar || key == languageTextForSpaceBar { - btn.addTarget(self, action: #selector(keyMultiPress(_:event:)), for: .touchDownRepeat) + // Activate keyboard interface buttons. + activateBtn(btn: btn) + if key == "shift" || key == spaceBar || key == languageTextForSpaceBar { + btn.addTarget( + self, action: #selector(keyMultiPress(_:event:)), for: .touchDownRepeat + ) + } + } } - } - } - // End padding. - switch keyboardState { - case .letters: - break - case .numbers: - break - case .symbols: - break + // End padding. + switch keyboardState { + case .letters: + break + case .numbers: + break + case .symbols: + break + } } - } - // MARK: Load Keys + // MARK: Load Keys /// Loads the keys given the current constraints. func loadKeys() { @@ -1280,86 +1383,86 @@ class KeyboardViewController: UIInputViewController { } } - // Actions to be done only on initial loads. - if isFirstKeyboardLoad { - shiftButtonState = .shift - commandBar.textColor = keyCharColor - commandBar.conditionallyAddPlaceholder() // in case of color mode change during commands - keyboardView.backgroundColor? = keyboardBgColor - allNonSpecialKeys = allKeys.filter { !specialKeys.contains($0) } - - // Check if the device has a home button and is large enough so we should use an expanded keyboard. - if DeviceType.isPad { - if UIScreen.main.bounds.width > 768 { - usingExpandedKeyboard = true - } else { - usingExpandedKeyboard = false - } - } + // Actions to be done only on initial loads. + if isFirstKeyboardLoad { + shiftButtonState = .shift + commandBar.textColor = keyCharColor + commandBar.conditionallyAddPlaceholder() // in case of color mode change during commands + keyboardView.backgroundColor? = keyboardBgColor + allNonSpecialKeys = allKeys.filter { !specialKeys.contains($0) } + + // Check if the device has a home button and is large enough so we should use an expanded keyboard. + if DeviceType.isPad { + if UIScreen.main.bounds.width > 768 { + usingExpandedKeyboard = true + } else { + usingExpandedKeyboard = false + } + } - linkShadowBlendElements() - setAutoActionPartitions() - conditionallyShowTopNumbersRow() + linkShadowBlendElements() + setAutoActionPartitions() + conditionallyShowTopNumbersRow() - // Show the name of the keyboard to the user. - showKeyboardLanguage = true + // Show the name of the keyboard to the user. + showKeyboardLanguage = true - // Add UILexicon words including unpaired first and last names from Contacts to autocompletions. - requestSupplementaryLexicon { (userLexicon: UILexicon?) in - if let lexicon = userLexicon { - for item in lexicon.entries where item.documentText.count > 1 { - LanguageDBManager.shared.insertAutocompleteLexicon(of: item.documentText) - } - } - } + // Add UILexicon words including unpaired first and last names from Contacts to autocompletions. + requestSupplementaryLexicon { (userLexicon: UILexicon?) in + if let lexicon = userLexicon { + for item in lexicon.entries where item.documentText.count > 1 { + LanguageDBManager.shared.insertAutocompleteLexicon(of: item.documentText) + } + } + } - // Drop non-unique values in case the lexicon has added words that were already present. - LanguageDBManager.shared.deleteNonUniqueAutocompletions() + // Drop non-unique values in case the lexicon has added words that were already present. + LanguageDBManager.shared.deleteNonUniqueAutocompletions() - // Load plural words for the current language. - if let allPlurals = LanguageDBManager.shared.queryAllPluralForms() { - pluralWords = Set(allPlurals.map { $0.lowercased() }) - } - } + // Load plural words for the current language. + if let allPlurals = LanguageDBManager.shared.queryAllPluralForms() { + pluralWords = Set(allPlurals.map { $0.lowercased() }) + } + } - setKeyboard() - setCommaAndPeriodKeysConditionally() - setCommandBackground() - setCommandBtns() - - // Clear annotation state if a keyboard state change dictates it. - if !annotationState { - annotationBtns.forEach { $0.removeFromSuperview() } - annotationBtns.removeAll() - annotationSeparators.forEach { $0.removeFromSuperview() } - annotationSeparators.removeAll() - } + setKeyboard() + setCommaAndPeriodKeysConditionally() + setCommandBackground() + setCommandBtns() + + // Clear annotation state if a keyboard state change dictates it. + if !annotationState { + annotationBtns.forEach { $0.removeFromSuperview() } + annotationBtns.removeAll() + annotationSeparators.forEach { $0.removeFromSuperview() } + annotationSeparators.removeAll() + } - // Clear interface from the last state. - keyboardKeys.forEach { $0.removeFromSuperview() } - paddingViews.forEach { $0.removeFromSuperview() } + // Clear interface from the last state. + keyboardKeys.forEach { $0.removeFromSuperview() } + paddingViews.forEach { $0.removeFromSuperview() } - setKeywidth() + setKeywidth() - // Derive keyboard given current states and set widths. - switch keyboardState { - case .letters: - keyboard = letterKeys - keyWidth = letterKeyWidth + // Derive keyboard given current states and set widths. + switch keyboardState { + case .letters: + keyboard = letterKeys + keyWidth = letterKeyWidth - // Auto-capitalization if the cursor is at the start of the proxy. - if let documentContext = proxy.documentContextBeforeInput, documentContext.isEmpty { - shiftButtonState = .shift - } + // Auto-capitalization if the cursor is at the start of the proxy. + if let documentContext = proxy.documentContextBeforeInput, documentContext.isEmpty { + shiftButtonState = .shift + } - case .numbers: - keyboard = numberKeys - keyWidth = numSymKeyWidth + case .numbers: + keyboard = numberKeys + keyWidth = numSymKeyWidth - case .symbols: - keyboard = symbolKeys - keyWidth = numSymKeyWidth - } + case .symbols: + keyboard = symbolKeys + keyWidth = numSymKeyWidth + } // Derive corner radii. if DeviceType.isPhone { @@ -1380,190 +1483,202 @@ class KeyboardViewController: UIInputViewController { } } - // Normal keyboard view. - for view in [stackViewNum, stackView0, stackView1, stackView2, stackView3] { - view?.isUserInteractionEnabled = true - view?.isLayoutMarginsRelativeArrangement = true + // Normal keyboard view. + for view in [stackViewNum, stackView0, stackView1, stackView2, stackView3] { + view?.isUserInteractionEnabled = true + view?.isLayoutMarginsRelativeArrangement = true + + // Set edge insets for stack views to provide vertical key spacing. + if DeviceType.isPad { + if view == stackViewNum { + view?.layoutMargins = UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0) + } else if view == stackView0 { + view?.layoutMargins = UIEdgeInsets(top: 2, left: 0, bottom: 6, right: 0) + } else if view == stackView1 { + view?.layoutMargins = UIEdgeInsets(top: 3, left: 0, bottom: 4, right: 0) + } else if view == stackView2 { + view?.layoutMargins = UIEdgeInsets(top: 3, left: 0, bottom: 4, right: 0) + } else if view == stackView3 { + view?.layoutMargins = UIEdgeInsets(top: 4, left: 0, bottom: 3, right: 0) + } + } else { + if view == stackViewNum { + view?.layoutMargins = UIEdgeInsets(top: 3, left: 0, bottom: 4, right: 0) + } else if view == stackView0 { + view?.layoutMargins = UIEdgeInsets(top: 3, left: 0, bottom: 8, right: 0) + } else if view == stackView1 { + view?.layoutMargins = UIEdgeInsets(top: 5, left: 0, bottom: 6, right: 0) + } else if view == stackView2 { + view?.layoutMargins = UIEdgeInsets(top: 5, left: 0, bottom: 6, right: 0) + } else if view == stackView3 { + view?.layoutMargins = UIEdgeInsets(top: 6, left: 0, bottom: 5, right: 0) + } + } + } - // Set edge insets for stack views to provide vertical key spacing. - if DeviceType.isPad { - if view == stackViewNum { - view?.layoutMargins = UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0) - } else if view == stackView0 { - view?.layoutMargins = UIEdgeInsets(top: 2, left: 0, bottom: 6, right: 0) - } else if view == stackView1 { - view?.layoutMargins = UIEdgeInsets(top: 3, left: 0, bottom: 4, right: 0) - } else if view == stackView2 { - view?.layoutMargins = UIEdgeInsets(top: 3, left: 0, bottom: 4, right: 0) - } else if view == stackView3 { - view?.layoutMargins = UIEdgeInsets(top: 4, left: 0, bottom: 3, right: 0) - } - } else { - if view == stackViewNum { - view?.layoutMargins = UIEdgeInsets(top: 3, left: 0, bottom: 4, right: 0) - } else if view == stackView0 { - view?.layoutMargins = UIEdgeInsets(top: 3, left: 0, bottom: 8, right: 0) - } else if view == stackView1 { - view?.layoutMargins = UIEdgeInsets(top: 5, left: 0, bottom: 6, right: 0) - } else if view == stackView2 { - view?.layoutMargins = UIEdgeInsets(top: 5, left: 0, bottom: 6, right: 0) - } else if view == stackView3 { - view?.layoutMargins = UIEdgeInsets(top: 6, left: 0, bottom: 5, right: 0) - } + // Set up and activate Scribe key and other command elements. + scribeKey.set() + activateBtn(btn: scribeKey) + styleBtn(btn: scribeKey, title: "Scribe", radius: commandKeyCornerRadius) + scribeKey.setTitle("", for: .normal) + commandBar.set() // set here so text spacing is appropriate + conditionallyShowAutoActionPartitions() + if DeviceType.isPhone { + translateKey.titleLabel?.font = .systemFont( + ofSize: scribeKey.frame.height * scalarCommandKeyHeightPhone + ) + conjugateKey.titleLabel?.font = .systemFont( + ofSize: scribeKey.frame.height * scalarCommandKeyHeightPhone + ) + pluralKey.titleLabel?.font = .systemFont( + ofSize: scribeKey.frame.height * scalarCommandKeyHeightPhone + ) + } else if DeviceType.isPad { + translateKey.titleLabel?.font = .systemFont( + ofSize: scribeKey.frame.height * scalarCommandKeyHeightPad + ) + conjugateKey.titleLabel?.font = .systemFont( + ofSize: scribeKey.frame.height * scalarCommandKeyHeightPad + ) + pluralKey.titleLabel?.font = .systemFont( + ofSize: scribeKey.frame.height * scalarCommandKeyHeightPad + ) } - } - // Set up and activate Scribe key and other command elements. - scribeKey.set() - activateBtn(btn: scribeKey) - styleBtn(btn: scribeKey, title: "Scribe", radius: commandKeyCornerRadius) - scribeKey.setTitle("", for: .normal) - commandBar.set() // set here so text spacing is appropriate - conditionallyShowAutoActionPartitions() - if DeviceType.isPhone { - translateKey.titleLabel?.font = .systemFont(ofSize: scribeKey.frame.height * scalarCommandKeyHeightPhone) - conjugateKey.titleLabel?.font = .systemFont(ofSize: scribeKey.frame.height * scalarCommandKeyHeightPhone) - pluralKey.titleLabel?.font = .systemFont(ofSize: scribeKey.frame.height * scalarCommandKeyHeightPhone) - } else if DeviceType.isPad { - translateKey.titleLabel?.font = .systemFont(ofSize: scribeKey.frame.height * scalarCommandKeyHeightPad) - conjugateKey.titleLabel?.font = .systemFont(ofSize: scribeKey.frame.height * scalarCommandKeyHeightPad) - pluralKey.titleLabel?.font = .systemFont(ofSize: scribeKey.frame.height * scalarCommandKeyHeightPad) - } + if commandState == .selectCommand { + styleBtn(btn: translateKey, title: translateKeyLbl, radius: commandKeyCornerRadius) + if controllerLanguage == "Indonesian" { + hideConjugateAndPluralKeys(state: true) + } else { + styleBtn(btn: conjugateKey, title: conjugateKeyLbl, radius: commandKeyCornerRadius) + styleBtn(btn: pluralKey, title: pluralKeyLbl, radius: commandKeyCornerRadius) + } - if commandState == .selectCommand { - styleBtn(btn: translateKey, title: translateKeyLbl, radius: commandKeyCornerRadius) - if controllerLanguage == "Indonesian" { - hideConjugateAndPluralKeys(state: true) + scribeKey.toEscape() + scribeKey.setFullCornerRadius() + scribeKey.setFullShadow() + commandBar.hide() + hideAutoActionPartitions() } else { - styleBtn(btn: conjugateKey, title: conjugateKeyLbl, radius: commandKeyCornerRadius) - styleBtn(btn: pluralKey, title: pluralKeyLbl, radius: commandKeyCornerRadius) + if controllerLanguage != "Indonesian" { + deactivateBtn(btn: conjugateKey) + deactivateBtn(btn: pluralKey) + } + deactivateBtn(btn: translateKey) + deactivateBtn(btn: phoneEmojiKey0) + deactivateBtn(btn: phoneEmojiKey1) + deactivateBtn(btn: padEmojiKey0) + deactivateBtn(btn: padEmojiKey1) + deactivateBtn(btn: padEmojiKey2) + + if [.translate, .conjugate, .plural].contains(commandState) { + scribeKey.setPartialCornerRadius() + scribeKey.setPartialShadow() + scribeKey.toEscape() + + commandBar.set() + commandBar.setCornerRadiusAndShadow() + hideAutoActionPartitions() + } else if [.alreadyPlural, .invalid].contains(commandState) { + // Command bar as a view for invalid messages with a Scribe key to allow for new commands. + scribeKey.setPartialCornerRadius() + scribeKey.setPartialShadow() + + commandBar.set() + commandBar.setCornerRadiusAndShadow() + hideAutoActionPartitions() + } else if commandState == .idle { + scribeKey.setFullCornerRadius() + scribeKey.setFullShadow() + + commandBar.text = "" + commandBar.hide() + // Set autosuggestions on keyboard's first load. + if isFirstKeyboardLoad { + conditionallySetAutoActionBtns() + } + } } - scribeKey.toEscape() - scribeKey.setFullCornerRadius() - scribeKey.setFullShadow() - commandBar.hide() - hideAutoActionPartitions() - } else { - if controllerLanguage != "Indonesian" { - deactivateBtn(btn: conjugateKey) - deactivateBtn(btn: pluralKey) - } - deactivateBtn(btn: translateKey) - deactivateBtn(btn: phoneEmojiKey0) - deactivateBtn(btn: phoneEmojiKey1) - deactivateBtn(btn: padEmojiKey0) - deactivateBtn(btn: padEmojiKey1) - deactivateBtn(btn: padEmojiKey2) - - if [.translate, .conjugate, .plural].contains(commandState) { - scribeKey.setPartialCornerRadius() - scribeKey.setPartialShadow() - scribeKey.toEscape() - - commandBar.set() - commandBar.setCornerRadiusAndShadow() - hideAutoActionPartitions() - } else if [.alreadyPlural, .invalid].contains(commandState) { - // Command bar as a view for invalid messages with a Scribe key to allow for new commands. - scribeKey.setPartialCornerRadius() - scribeKey.setPartialShadow() - - commandBar.set() - commandBar.setCornerRadiusAndShadow() - hideAutoActionPartitions() - } else if commandState == .idle { - scribeKey.setFullCornerRadius() - scribeKey.setFullShadow() - - commandBar.text = "" - commandBar.hide() - // Set autosuggestions on keyboard's first load. - if isFirstKeyboardLoad { - conditionallySetAutoActionBtns() - } - } - } + setKeyPadding() + } - setKeyPadding() - } + func setCommaAndPeriodKeysConditionally() { + let langCode = languagesAbbrDict[controllerLanguage] ?? "unknown" + if let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") { + let dictionaryKey = langCode + "CommaAndPeriod" + let letterKeysHaveCommaPeriod = userDefaults.bool(forKey: dictionaryKey) + let spaceIndex = letterKeys[3].firstIndex(where: { $0 == "space" }) - func setCommaAndPeriodKeysConditionally() { - let langCode = languagesAbbrDict[controllerLanguage] ?? "unknown" - if let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") { - let dictionaryKey = langCode + "CommaAndPeriod" - let letterKeysHaveCommaPeriod = userDefaults.bool(forKey: dictionaryKey) - let spaceIndex = letterKeys[3].firstIndex(where: { $0 == "space" }) - - if letterKeysHaveCommaPeriod { - letterKeys[3].insert(",", at: spaceIndex!) - letterKeys[3].insert(".", at: spaceIndex! + 2) - } else if proxy.keyboardType == .webSearch { - letterKeys[3].insert(".", at: spaceIndex! + 1) - } + if letterKeysHaveCommaPeriod { + letterKeys[3].insert(",", at: spaceIndex!) + letterKeys[3].insert(".", at: spaceIndex! + 2) + } else if proxy.keyboardType == .webSearch { + letterKeys[3].insert(".", at: spaceIndex! + 1) + } + } } - } - func doubleSpacePeriodsEnabled() -> Bool { - let langCode = languagesAbbrDict[controllerLanguage] ?? "unknown" - if let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") { - let dictionaryKey = langCode + "DoubleSpacePeriods" + func doubleSpacePeriodsEnabled() -> Bool { + let langCode = languagesAbbrDict[controllerLanguage] ?? "unknown" + if let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") { + let dictionaryKey = langCode + "DoubleSpacePeriods" - return userDefaults.bool(forKey: dictionaryKey) - } else { - return true // return the default value + return userDefaults.bool(forKey: dictionaryKey) + } else { + return true // return the default value + } } - } - func emojiAutosuggestIsEnabled() -> Bool { - let langCode = languagesAbbrDict[controllerLanguage] ?? "unknown" - if let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") { - let dictionaryKey = langCode + "EmojiAutosuggest" + func emojiAutosuggestIsEnabled() -> Bool { + let langCode = languagesAbbrDict[controllerLanguage] ?? "unknown" + if let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") { + let dictionaryKey = langCode + "EmojiAutosuggest" - return userDefaults.bool(forKey: dictionaryKey) - } else { - return true // return the default value + return userDefaults.bool(forKey: dictionaryKey) + } else { + return true // return the default value + } } - } - func wordForWordDeletionIsEnabled() -> Bool { - let langCode = languagesAbbrDict[controllerLanguage] ?? "unknown" - if let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") { - let dictionaryKey = langCode + "WordForWordDeletion" + func wordForWordDeletionIsEnabled() -> Bool { + let langCode = languagesAbbrDict[controllerLanguage] ?? "unknown" + if let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") { + let dictionaryKey = langCode + "WordForWordDeletion" - return userDefaults.bool(forKey: dictionaryKey) - } else { - return false // return the default value + return userDefaults.bool(forKey: dictionaryKey) + } else { + return false // return the default value + } } - } + // MARK: Button Actions - // MARK: Button Actions - - /// Triggers actions based on the press of a key. - /// - /// - Parameters - /// - sender: the button pressed as sender. - @IBAction func executeKeyActions(_ sender: UIButton) { - guard let originalKey = sender.layer.value( - forKey: "original" - ) as? String, - let keyToDisplay = sender.layer.value(forKey: "keyToDisplay") as? String - else { - return - } + /// Triggers actions based on the press of a key. + /// + /// - Parameters + /// - sender: the button pressed as sender. + @IBAction func executeKeyActions(_ sender: UIButton) { + guard + let originalKey = sender.layer.value( + forKey: "original" + ) as? String, + let keyToDisplay = sender.layer.value(forKey: "keyToDisplay") as? String + else { + return + } - guard let isSpecial = sender.layer.value(forKey: "isSpecial") as? Bool else { return } - sender.backgroundColor = isSpecial ? specialKeyColor : keyColor + guard let isSpecial = sender.layer.value(forKey: "isSpecial") as? Bool else { return } + sender.backgroundColor = isSpecial ? specialKeyColor : keyColor - // Disable the possibility of a double shift call. - if originalKey != "shift" { - capsLockPossible = false - } - // Disable the possibility of a double space period call. - if originalKey != spaceBar && originalKey != languageTextForSpaceBar { - doubleSpacePeriodPossible = false - } + // Disable the possibility of a double shift call. + if originalKey != "shift" { + capsLockPossible = false + } + // Disable the possibility of a double space period call. + if originalKey != spaceBar, originalKey != languageTextForSpaceBar { + doubleSpacePeriodPossible = false + } switch originalKey { case "Scribe": @@ -1622,9 +1737,9 @@ class KeyboardViewController: UIInputViewController { queryPlural(commandBar: commandBar) } - if [.invalid, .alreadyPlural].contains(commandState) { // invalid state - loadKeys() - autoCapAtStartOfProxy() + if [.invalid, .alreadyPlural].contains(commandState) { // invalid state + loadKeys() + autoCapAtStartOfProxy() if commandState == .invalid { let invalidMsg = prevToInvalidState == .translate ? invalidCommandMsgWiktionary : invalidCommandMsgWikidata @@ -1652,30 +1767,30 @@ class KeyboardViewController: UIInputViewController { prevToInvalidState = .translate queryWordToTranslate(queriedWordToTranslate: selectedText) - if commandState == .invalid { // invalid state - loadKeys() - proxy.insertText(selectedText) - autoCapAtStartOfProxy() - commandBar.text = commandPromptSpacing + invalidCommandMsgWiktionary - commandBar.isShowingInfoButton = true - commandBar.textColor = keyCharColor - return - } else { // functional commands above - autoActionState = .suggest - commandState = .idle - autoCapAtStartOfProxy() - loadKeys() - conditionallyDisplayAnnotation() - } - } else { - commandState = .translate - // Always start in letters with a new keyboard. - keyboardState = .letters - conditionallyHideEmojiDividers() - loadKeys() - commandBar.textColor = keyCharColor - commandBar.attributedText = translatePromptAndColorPlaceholder - } + if commandState == .invalid { // invalid state + loadKeys() + proxy.insertText(selectedText) + autoCapAtStartOfProxy() + commandBar.text = commandPromptSpacing + invalidCommandMsgWiktionary + commandBar.isShowingInfoButton = true + commandBar.textColor = keyCharColor + return + } else { // functional commands above + autoActionState = .suggest + commandState = .idle + autoCapAtStartOfProxy() + loadKeys() + conditionallyDisplayAnnotation() + } + } else { + commandState = .translate + // Always start in letters with a new keyboard. + keyboardState = .letters + conditionallyHideEmojiDividers() + loadKeys() + commandBar.textColor = keyCharColor + commandBar.attributedText = translatePromptAndColorPlaceholder + } case "Conjugate": if let selectedText = proxy.selectedText { @@ -1709,336 +1824,341 @@ class KeyboardViewController: UIInputViewController { prevToInvalidState = .plural queryPluralNoun(queriedNoun: selectedText) - if [.invalid, .alreadyPlural].contains(commandState) { - loadKeys() - - if commandState == .invalid { - proxy.insertText(selectedText) - commandBar.text = commandPromptSpacing + invalidCommandMsgWikidata - commandBar.isShowingInfoButton = true - } else { - commandBar.isShowingInfoButton = false - if commandState == .alreadyPlural { - commandBar.text = commandPromptSpacing + alreadyPluralMsg + if [.invalid, .alreadyPlural].contains(commandState) { + loadKeys() + + if commandState == .invalid { + proxy.insertText(selectedText) + commandBar.text = commandPromptSpacing + invalidCommandMsgWikidata + commandBar.isShowingInfoButton = true + } else { + commandBar.isShowingInfoButton = false + if commandState == .alreadyPlural { + commandBar.text = commandPromptSpacing + alreadyPluralMsg + } + } + autoCapAtStartOfProxy() + commandBar.textColor = keyCharColor + return + } else { // functional commands above + autoActionState = .suggest + commandState = .idle + autoCapAtStartOfProxy() + loadKeys() + conditionallyDisplayAnnotation() + } + } else { + commandState = .plural + if controllerLanguage == "German" { // capitalize for nouns + if shiftButtonState == .normal { + shiftButtonState = .shift + } + } + conditionallyHideEmojiDividers() + loadKeys() + commandBar.textColor = keyCharColor + commandBar.attributedText = pluralPromptAndColorPlaceholder } - } - autoCapAtStartOfProxy() - commandBar.textColor = keyCharColor - return - } else { // functional commands above - autoActionState = .suggest - commandState = .idle - autoCapAtStartOfProxy() - loadKeys() - conditionallyDisplayAnnotation() - } - } else { - commandState = .plural - if controllerLanguage == "German" { // capitalize for nouns - if shiftButtonState == .normal { - shiftButtonState = .shift - } - } - conditionallyHideEmojiDividers() - loadKeys() - commandBar.textColor = keyCharColor - commandBar.attributedText = pluralPromptAndColorPlaceholder - } case "AutoAction0": executeAutoAction(keyPressed: translateKey) - case "AutoAction1": - executeAutoAction(keyPressed: conjugateKey) + case "AutoAction1": + executeAutoAction(keyPressed: conjugateKey) - case "AutoAction2": - executeAutoAction(keyPressed: pluralKey) - if emojisToShow == .one { - if shiftButtonState == .normal { - shiftButtonState = .shift - } - loadKeys() - } + case "AutoAction2": + executeAutoAction(keyPressed: pluralKey) + if emojisToShow == .one { + if shiftButtonState == .normal { + shiftButtonState = .shift + } + loadKeys() + } - case "EmojiKey0": - if DeviceType.isPhone || emojisToShow == .two { - executeAutoAction(keyPressed: phoneEmojiKey0) - } else if DeviceType.isPad { - executeAutoAction(keyPressed: padEmojiKey0) - } - if shiftButtonState == .normal { - shiftButtonState = .shift - } - loadKeys() + case "EmojiKey0": + if DeviceType.isPhone || emojisToShow == .two { + executeAutoAction(keyPressed: phoneEmojiKey0) + } else if DeviceType.isPad { + executeAutoAction(keyPressed: padEmojiKey0) + } + if shiftButtonState == .normal { + shiftButtonState = .shift + } + loadKeys() - case "EmojiKey1": - if DeviceType.isPhone || emojisToShow == .two { - executeAutoAction(keyPressed: phoneEmojiKey1) - } else if DeviceType.isPad { - executeAutoAction(keyPressed: padEmojiKey1) - } - if shiftButtonState == .normal { - shiftButtonState = .shift - } - loadKeys() + case "EmojiKey1": + if DeviceType.isPhone || emojisToShow == .two { + executeAutoAction(keyPressed: phoneEmojiKey1) + } else if DeviceType.isPad { + executeAutoAction(keyPressed: padEmojiKey1) + } + if shiftButtonState == .normal { + shiftButtonState = .shift + } + loadKeys() - case "EmojiKey2": - executeAutoAction(keyPressed: padEmojiKey2) - if shiftButtonState == .normal { - shiftButtonState = .shift - } - loadKeys() + case "EmojiKey2": + executeAutoAction(keyPressed: padEmojiKey2) + if shiftButtonState == .normal { + shiftButtonState = .shift + } + loadKeys() - case "GetAnnotationInfo": - // Remove all prior annotations. - annotationBtns.forEach { $0.removeFromSuperview() } - annotationBtns.removeAll() - annotationSeparators.forEach { $0.removeFromSuperview() } - annotationSeparators.removeAll() + case "GetAnnotationInfo": + // Remove all prior annotations. + annotationBtns.forEach { $0.removeFromSuperview() } + annotationBtns.removeAll() + annotationSeparators.forEach { $0.removeFromSuperview() } + annotationSeparators.removeAll() - for i in 0 ..< annotationBtns.count { - annotationBtns[i].backgroundColor = annotationColors[i] - } + for i in 0 ..< annotationBtns.count { + annotationBtns[i].backgroundColor = annotationColors[i] + } - if let wordSelected = proxy.selectedText { - wordToCheck = wordSelected - } else { - if let contextBeforeInput = proxy.documentContextBeforeInput { - wordsTyped = contextBeforeInput.components(separatedBy: " ") - let lastWordTyped = wordsTyped.secondToLast() - if !languagesWithCapitalizedNouns.contains(controllerLanguage) { - wordToCheck = lastWordTyped!.lowercased() - } else { - wordToCheck = lastWordTyped! - } - } - } + if let wordSelected = proxy.selectedText { + wordToCheck = wordSelected + } else { + if let contextBeforeInput = proxy.documentContextBeforeInput { + wordsTyped = contextBeforeInput.components(separatedBy: " ") + let lastWordTyped = wordsTyped.secondToLast() + if !languagesWithCapitalizedNouns.contains(controllerLanguage) { + wordToCheck = lastWordTyped!.lowercased() + } else { + wordToCheck = lastWordTyped! + } + } + } - let prepForm = LanguageDBManager.shared.queryPrepForm(of: wordToCheck.lowercased())[0] - hasPrepForm = !prepForm.isEmpty - if hasPrepForm { - prepAnnotationForm = prepForm - commandState = .selectCaseDeclension - showDynamicDeclensionView(preposition: wordToCheck) - return - } else { - return - } + let prepForm = LanguageDBManager.shared.queryPrepForm(of: wordToCheck.lowercased())[0] + hasPrepForm = !prepForm.isEmpty + if hasPrepForm { + prepAnnotationForm = prepForm + commandState = .selectCaseDeclension + showDynamicDeclensionView(preposition: wordToCheck) + return + } else { + return + } - case "ScribeAnnotation": - for i in 0 ..< annotationBtns.count { - annotationBtns[i].backgroundColor = annotationColors[i] - } - let emojisToSelectFrom = "🥳🎉" - let emojis = String((0 ..< 3).compactMap { _ in emojisToSelectFrom.randomElement() }) - sender.setTitle(emojis, for: .normal) - return - - case "delete": - styleDeleteButton(sender, isPressed: false) - if ![.translate, .conjugate, .plural].contains(commandState) { - // Control shift state on delete. - if keyboardState == .letters && shiftButtonState == .shift && proxy.documentContextBeforeInput != nil { - shiftButtonState = .normal - loadKeys() - } else if keyboardState == .letters && shiftButtonState == .normal && proxy.documentContextBeforeInput == nil { - autoCapAtStartOfProxy() - pastStringInTextProxy = "" - } + case "ScribeAnnotation": + for i in 0 ..< annotationBtns.count { + annotationBtns[i].backgroundColor = annotationColors[i] + } + let emojisToSelectFrom = "🥳🎉" + let emojis = String((0 ..< 3).compactMap { _ in emojisToSelectFrom.randomElement() }) + sender.setTitle(emojis, for: .normal) + return - handleDeleteButtonPressed() - autoCapAtStartOfProxy() + case "delete": + styleDeleteButton(sender, isPressed: false) + if ![.translate, .conjugate, .plural].contains(commandState) { + // Control shift state on delete. + if keyboardState == .letters, shiftButtonState == .shift, + proxy.documentContextBeforeInput != nil { + shiftButtonState = .normal + loadKeys() + } else if keyboardState == .letters, shiftButtonState == .normal, + proxy.documentContextBeforeInput == nil { + autoCapAtStartOfProxy() + pastStringInTextProxy = "" + } - // Check if the last character is a space such that the user is deleting multiple spaces and suggest is so. - if proxy.documentContextBeforeInput?.suffix(" ".count) == " " { - autoActionState = .suggest - } else { - autoActionState = .complete - } - conditionallySetAutoActionBtns() - } else { - // Shift state if the user presses delete when the prompt is present. - if let commandBarText = commandBar?.text, let commandBarAttributedText = commandBar?.attributedText { - if allPrompts.contains(commandBarText) || allColoredPrompts.contains(commandBarAttributedText) { - shiftButtonState = .shift // Auto-capitalization - loadKeys() - // Function call required due to return. - // Not including means placeholder is never added on last delete action. - commandBar.conditionallyAddPlaceholder() - return - } - } + handleDeleteButtonPressed() + autoCapAtStartOfProxy() - handleDeleteButtonPressed() + // Check if the last character is a space such that the user is deleting multiple spaces and suggest is so. + if proxy.documentContextBeforeInput?.suffix(" ".count) == " " { + autoActionState = .suggest + } else { + autoActionState = .complete + } + conditionallySetAutoActionBtns() + } else { + // Shift state if the user presses delete when the prompt is present. + if let commandBarText = commandBar?.text, + let commandBarAttributedText = commandBar?.attributedText { + if allPrompts.contains(commandBarText) + || allColoredPrompts.contains(commandBarAttributedText) { + shiftButtonState = .shift // Auto-capitalization + loadKeys() + // Function call required due to return. + // Not including means placeholder is never added on last delete action. + commandBar.conditionallyAddPlaceholder() + return + } + } - // Inserting the placeholder when commandBar text is deleted. - commandBar.conditionallyAddPlaceholder() - } + handleDeleteButtonPressed() - case spaceBar, languageTextForSpaceBar: - if currentPrefix != completionWords[0] && (completionWords[0] != " " && spaceAutoInsertIsPossible) { - previousWord = currentPrefix - clearPrefixFromTextFieldProxy() - proxy.insertText(completionWords[0]) - autoActionState = .suggest - currentPrefix = "" - firstCompletionIsHighlighted = false - spaceAutoInsertIsPossible = false - allowUndo = true - } - autoActionState = .suggest - commandBar.conditionallyRemovePlaceholder() - if ![.translate, .conjugate, .plural].contains(commandState) { - proxy.insertText(" ") - if [". ", "? ", "! "].contains(proxy.documentContextBeforeInput?.suffix(2)) { - shiftButtonState = .shift - } - if keyboardState != .letters { - changeKeyboardToLetterKeys() - } - } else { - if let commandBarText = commandBar?.text { - commandBar?.text = commandBarText.insertPriorToCursor(char: " ") - if [". " + commandCursor, "? " + commandCursor, "! " + commandCursor].contains(String(commandBarText.suffix(3))) { - shiftButtonState = .shift - } - if keyboardState != .letters { - changeKeyboardToLetterKeys() - } - } - } + // Inserting the placeholder when commandBar text is deleted. + commandBar.conditionallyAddPlaceholder() + } - if proxy.documentContextBeforeInput?.suffix(" ".count) == " " { - // Remove all prior annotations. - annotationBtns.forEach { $0.removeFromSuperview() } - annotationBtns.removeAll() - annotationSeparators.forEach { $0.removeFromSuperview() } - annotationSeparators.removeAll() - } + case spaceBar, languageTextForSpaceBar: + if currentPrefix != completionWords[0], + completionWords[0] != " ", spaceAutoInsertIsPossible { + previousWord = currentPrefix + clearPrefixFromTextFieldProxy() + proxy.insertText(completionWords[0]) + autoActionState = .suggest + currentPrefix = "" + firstCompletionIsHighlighted = false + spaceAutoInsertIsPossible = false + allowUndo = true + } + autoActionState = .suggest + commandBar.conditionallyRemovePlaceholder() + if ![.translate, .conjugate, .plural].contains(commandState) { + proxy.insertText(" ") + if [". ", "? ", "! "].contains(proxy.documentContextBeforeInput?.suffix(2)) { + shiftButtonState = .shift + } + if keyboardState != .letters { + changeKeyboardToLetterKeys() + } + } else { + if let commandBarText = commandBar?.text { + commandBar?.text = commandBarText.insertPriorToCursor(char: " ") + if [". " + commandCursor, "? " + commandCursor, "! " + commandCursor].contains( + String(commandBarText.suffix(3)) + ) { + shiftButtonState = .shift + } + if keyboardState != .letters { + changeKeyboardToLetterKeys() + } + } + } - conditionallyDisplayAnnotation() - doubleSpacePeriodPossible = true + if proxy.documentContextBeforeInput?.suffix(" ".count) == " " { + // Remove all prior annotations. + annotationBtns.forEach { $0.removeFromSuperview() } + annotationBtns.removeAll() + annotationSeparators.forEach { $0.removeFromSuperview() } + annotationSeparators.removeAll() + } - case "'": - // Change back to letter keys. - commandBar.conditionallyRemovePlaceholder() - if ![.translate, .conjugate, .plural].contains(commandState) { - proxy.insertText("'") - } else { - if let commandBarText = commandBar.text { - commandBar.text = commandBarText.insertPriorToCursor(char: "'") - } - } - changeKeyboardToLetterKeys() + conditionallyDisplayAnnotation() + doubleSpacePeriodPossible = true - case "-": - if [.idle, .selectCommand, .alreadyPlural, .invalid].contains(commandState) { - if proxy.documentContextBeforeInput?.last == "-" { - proxy.deleteBackward() - proxy.insertText("—") - } else { - proxy.insertText(keyToDisplay) - } - } else { - if let commandBarText = commandBar.text { - commandBar.text = commandBarText.insertPriorToCursor(char: keyToDisplay) - } - } + case "'": + // Change back to letter keys. + commandBar.conditionallyRemovePlaceholder() + if ![.translate, .conjugate, .plural].contains(commandState) { + proxy.insertText("'") + } else { + if let commandBarText = commandBar.text { + commandBar.text = commandBarText.insertPriorToCursor(char: "'") + } + } + changeKeyboardToLetterKeys() - case ",", ".", "!", "?": - if [.idle, .selectCommand, .alreadyPlural, .invalid].contains(commandState) { - if proxy.documentContextBeforeInput?.last == " " { - proxy.deleteBackward() - } - proxy.insertText(keyToDisplay) - } else { - if let commandBarText = commandBar.text { - commandBar.text = commandBarText.insertPriorToCursor(char: keyToDisplay) - } - } + case "-": + if [.idle, .selectCommand, .alreadyPlural, .invalid].contains(commandState) { + if proxy.documentContextBeforeInput?.last == "-" { + proxy.deleteBackward() + proxy.insertText("—") + } else { + proxy.insertText(keyToDisplay) + } + } else { + if let commandBarText = commandBar.text { + commandBar.text = commandBarText.insertPriorToCursor(char: keyToDisplay) + } + } - case "shift": - if shiftButtonState == .capsLocked { - shiftButtonState = .normal - } else { - shiftButtonState = shiftButtonState == .normal ? .shift : .normal - } + case ",", ".", "!", "?": + if [.idle, .selectCommand, .alreadyPlural, .invalid].contains(commandState) { + if proxy.documentContextBeforeInput?.last == " " { + proxy.deleteBackward() + } + proxy.insertText(keyToDisplay) + } else { + if let commandBarText = commandBar.text { + commandBar.text = commandBarText.insertPriorToCursor(char: keyToDisplay) + } + } - loadKeys() - capsLockPossible = true + case "shift": + if shiftButtonState == .capsLocked { + shiftButtonState = .normal + } else { + shiftButtonState = shiftButtonState == .normal ? .shift : .normal + } - case "123", ".?123": - if usingExpandedKeyboard { - changeKeyboardToSymbolKeys() - } else { - changeKeyboardToNumberKeys() - } + loadKeys() + capsLockPossible = true + + case "123", ".?123": + if usingExpandedKeyboard { + changeKeyboardToSymbolKeys() + } else { + changeKeyboardToNumberKeys() + } - case "#+=": - changeKeyboardToSymbolKeys() + case "#+=": + changeKeyboardToSymbolKeys() - case "ABC", "АБВ": - changeKeyboardToLetterKeys() - autoCapAtStartOfProxy() + case "ABC", "АБВ": + changeKeyboardToLetterKeys() + autoCapAtStartOfProxy() - case SpecialKeys.capsLock: - switchToFullCaps() + case SpecialKeys.capsLock: + switchToFullCaps() - case SpecialKeys.indent: - proxy.insertText("\t") + case SpecialKeys.indent: + proxy.insertText("\t") - case "selectKeyboard": - advanceToNextInputMode() + case "selectKeyboard": + advanceToNextInputMode() - case "hideKeyboard": - dismissKeyboard() + case "hideKeyboard": + dismissKeyboard() - default: - autoActionState = .complete - commandBar.conditionallyRemovePlaceholder() - if shiftButtonState == .shift { - shiftButtonState = .normal - loadKeys() - } - if [.idle, .selectCommand, .alreadyPlural, .invalid].contains(commandState) { - proxy.insertText(keyToDisplay) - } else { - if let currentText = commandBar.text { - commandBar.text = currentText.insertPriorToCursor(char: keyToDisplay) + default: + autoActionState = .complete + commandBar.conditionallyRemovePlaceholder() + if shiftButtonState == .shift { + shiftButtonState = .normal + loadKeys() + } + if [.idle, .selectCommand, .alreadyPlural, .invalid].contains(commandState) { + proxy.insertText(keyToDisplay) + } else { + if let currentText = commandBar.text { + commandBar.text = currentText.insertPriorToCursor(char: keyToDisplay) + } + } } - } - } - // Cancel already plural and invalid states after another key press. - if [.alreadyPlural, .invalid].contains(commandState) { - commandState = .idle - loadKeys() - } + // Cancel already plural and invalid states after another key press. + if [.alreadyPlural, .invalid].contains(commandState) { + commandState = .idle + loadKeys() + } - // Reset emoji repeat functionality. - if !( - ["EmojiKey0", "EmojiKey1", "EmojiKey2"].contains(originalKey) - || (originalKey == "AutoAction2" && emojisToShow == .one) - ) { - emojiAutoActionRepeatPossible = false - } + // Reset emoji repeat functionality. + if !(["EmojiKey0", "EmojiKey1", "EmojiKey2"].contains(originalKey) + || (originalKey == "AutoAction2" && emojisToShow == .one)) { + emojiAutoActionRepeatPossible = false + } - if !["Scribe"].contains(originalKey) { - emojisToShow = .zero - } + if !["Scribe"].contains(originalKey) { + emojisToShow = .zero + } - // Add partitions and show auto actions if the keyboard states dictate. - conditionallyShowAutoActionPartitions() - conditionallySetAutoActionBtns() + // Add partitions and show auto actions if the keyboard states dictate. + conditionallyShowAutoActionPartitions() + conditionallySetAutoActionBtns() - if !annotationState { - annotationBtns.forEach { $0.removeFromSuperview() } - annotationBtns.removeAll() - annotationSeparators.forEach { $0.removeFromSuperview() } - annotationSeparators.removeAll() - } - annotationState = false - activateAnnotationBtn = false + if !annotationState { + annotationBtns.forEach { $0.removeFromSuperview() } + annotationBtns.removeAll() + annotationSeparators.forEach { $0.removeFromSuperview() } + annotationSeparators.removeAll() + } + annotationState = false + activateAnnotationBtn = false // Remove alternates view if it's present. if view.viewWithTag(1001) != nil { @@ -2048,390 +2168,414 @@ class KeyboardViewController: UIInputViewController { } } - // MARK: Key Press Functions - - /// Auto-capitalization if the cursor is at the start of the proxy. - func autoCapAtStartOfProxy() { - proxy.insertText(" ") - if proxy.documentContextBeforeInput == " " { - if shiftButtonState == .normal { - shiftButtonState = .shift - loadKeys() - } - } - proxy.deleteBackward() - } + // MARK: Key Press Functions - /// Colors keys to show they have been pressed. - /// - /// - Parameters - /// - sender: the key that was pressed. - @objc func keyTouchDown(_ sender: UIButton) { - guard let originalKey = sender.layer.value( - forKey: "original" - ) as? String else { - return + /// Auto-capitalization if the cursor is at the start of the proxy. + func autoCapAtStartOfProxy() { + proxy.insertText(" ") + if proxy.documentContextBeforeInput == " " { + if shiftButtonState == .normal { + shiftButtonState = .shift + loadKeys() + } + } + proxy.deleteBackward() } - if originalKey == "GetAnnotationInfo" { - // Blink each btn in the annotation display if one is pressed. - for btn in annotationBtns { - btn.backgroundColor = keyPressedColor - } - } else if originalKey == "delete" { - // Change the icon of the delete button to be filled in. - sender.backgroundColor = keyPressedColor - styleDeleteButton(sender, isPressed: true) - } else { - sender.backgroundColor = keyPressedColor - } - } + /// Colors keys to show they have been pressed. + /// + /// - Parameters + /// - sender: the key that was pressed. + @objc func keyTouchDown(_ sender: UIButton) { + guard + let originalKey = sender.layer.value( + forKey: "original" + ) as? String + else { + return + } - /// Shows the conjugation view for verbs. - /// - Parameters - /// - verb: the verb to show conjugations for. - func showDynamicConjugationView(verb: String) { - // Remove any existing view. - children.forEach { child in - if child is DynamicConjugationViewController { - child.removeFromParent() - child.view.removeFromSuperview() - } + if originalKey == "GetAnnotationInfo" { + // Blink each btn in the annotation display if one is pressed. + for btn in annotationBtns { + btn.backgroundColor = keyPressedColor + } + } else if originalKey == "delete" { + // Change the icon of the delete button to be filled in. + sender.backgroundColor = keyPressedColor + styleDeleteButton(sender, isPressed: true) + } else { + sender.backgroundColor = keyPressedColor + } } - // Hide the command buttons. - deactivateBtn(btn: translateKey) - deactivateBtn(btn: conjugateKey) - deactivateBtn(btn: pluralKey) - hideAutoActionPartitions() + /// Shows the conjugation view for verbs. + /// - Parameters + /// - verb: the verb to show conjugations for. + func showDynamicConjugationView(verb: String) { + // Remove any existing view. + for child in children { + if child is DynamicConjugationViewController { + child.removeFromParent() + child.view.removeFromSuperview() + } + } - // Update Scribe key to escape mode. - scribeKey.toEscape() - scribeKey.setPartialCornerRadius() - scribeKey.setPartialShadow() + // Hide the command buttons. + deactivateBtn(btn: translateKey) + deactivateBtn(btn: conjugateKey) + deactivateBtn(btn: pluralKey) + hideAutoActionPartitions() - // Set up command bar with verb title. - commandBar.set() - commandBar.setCornerRadiusAndShadow() - commandBar.backgroundColor = commandBarColor + // Update Scribe key to escape mode. + scribeKey.toEscape() + scribeKey.setPartialCornerRadius() + scribeKey.setPartialShadow() + + // Set up command bar with verb title. + commandBar.set() + commandBar.setCornerRadiusAndShadow() + commandBar.backgroundColor = commandBarColor + + // Build conjugation cases array. + guard + let cases = NavigationBuilder.buildConjugationCases( + verb: verb, + language: languagesAbbrDict[controllerLanguage] ?? "" + ) + else { + return + } - // Build conjugation cases array. - guard let cases = NavigationBuilder.buildConjugationCases( - verb: verb, - language: languagesAbbrDict[controllerLanguage] ?? "" - ) else { - return - } + let conjugationVC = DynamicConjugationViewController( + linearCases: cases, + commandBar: commandBar, + startingIndex: 0 // always start at first tense + ) - let conjugationVC = DynamicConjugationViewController( - linearCases: cases, - commandBar: commandBar, - startingIndex: 0 // always start at first tense - ) + addChild(conjugationVC) - addChild(conjugationVC) + conjugationVC.view.frame = CGRect( + x: 0, + y: commandBar.frame.maxY, + width: view.bounds.width, + height: view.bounds.height - commandBar.frame.maxY + ) - conjugationVC.view.frame = CGRect( - x: 0, - y: commandBar.frame.maxY, - width: view.bounds.width, - height: view.bounds.height - commandBar.frame.maxY - ) + view.addSubview(conjugationVC.view) + conjugationVC.didMove(toParent: self) + } - view.addSubview(conjugationVC.view) - conjugationVC.didMove(toParent: self) - } + /// Shows the declension view for prepositions with declensions. + /// - Parameters + /// - preposition: the preposition to show declensions for. + func showDynamicDeclensionView(preposition _: String) { + // Remove any existing view. + for child in children { + if child is DynamicConjugationViewController { + child.removeFromParent() + child.view.removeFromSuperview() + } + } - /// Shows the declension view for prepositions with declensions. - /// - Parameters - /// - preposition: the preposition to show declensions for. - func showDynamicDeclensionView(preposition: String) { - // Remove any existing view. - children.forEach { child in - if child is DynamicConjugationViewController { - child.removeFromParent() - child.view.removeFromSuperview() - } - } + // Set up UI state. + deactivateBtn(btn: translateKey) + deactivateBtn(btn: conjugateKey) + deactivateBtn(btn: pluralKey) + hideAutoActionPartitions() - // Set up UI state. - deactivateBtn(btn: translateKey) - deactivateBtn(btn: conjugateKey) - deactivateBtn(btn: pluralKey) - hideAutoActionPartitions() + scribeKey.toEscape() + scribeKey.setPartialCornerRadius() + scribeKey.setPartialShadow() - scribeKey.toEscape() - scribeKey.setPartialCornerRadius() - scribeKey.setPartialShadow() + commandBar.set() + commandBar.setCornerRadiusAndShadow() + commandBar.backgroundColor = commandBarColor - commandBar.set() - commandBar.setCornerRadiusAndShadow() - commandBar.backgroundColor = commandBarColor + // Build declension cases array. + let language = languagesAbbrDict[controllerLanguage] ?? "" + guard let cases = NavigationBuilder.getDeclensionCases(language: language) + else { + return + } - // Build declension cases array. - let language = languagesAbbrDict[controllerLanguage] ?? "" - guard let cases = NavigationBuilder.getDeclensionCases(language: language) else { - return - } + // Find starting index. + let startingIndex = NavigationBuilder.findStartingCaseIndex( + prepForm: convertFullPrepositionToAbbr(prepAnnotationForm), + language: language + ) - // Find starting index. - let startingIndex = NavigationBuilder.findStartingCaseIndex( - prepForm: convertFullPrepositionToAbbr(prepAnnotationForm), - language: language - ) + let declensionVC = DynamicConjugationViewController( + linearCases: cases, + commandBar: commandBar, + startingIndex: startingIndex + ) - let declensionVC = DynamicConjugationViewController( - linearCases: cases, - commandBar: commandBar, - startingIndex: startingIndex - ) + addChild(declensionVC) - addChild(declensionVC) + declensionVC.view.frame = CGRect( + x: 0, + y: commandBar.frame.maxY, + width: view.bounds.width, + height: view.bounds.height - commandBar.frame.maxY + ) - declensionVC.view.frame = CGRect( - x: 0, - y: commandBar.frame.maxY, - width: view.bounds.width, - height: view.bounds.height - commandBar.frame.maxY - ) + view.addSubview(declensionVC.view) + declensionVC.didMove(toParent: self) + } - view.addSubview(declensionVC.view) - declensionVC.didMove(toParent: self) - } + /// Defines events that occur given multiple presses of a single key. + /// + /// - Parameters + /// - sender: the key that was pressed multiple times. + /// - event: event to derive tap counts. + @objc func keyMultiPress(_ sender: UIButton, event: UIEvent) { + guard var originalKey = sender.layer.value(forKey: "original") as? String else { return } - /// Defines events that occur given multiple presses of a single key. - /// - /// - Parameters - /// - sender: the key that was pressed multiple times. - /// - event: event to derive tap counts. - @objc func keyMultiPress(_ sender: UIButton, event: UIEvent) { - guard var originalKey = sender.layer.value(forKey: "original") as? String else { return } - - if let touches = event.allTouches, let touch = touches.first { - // Caps lock given two taps of shift. - if touch.tapCount == 2 && originalKey == "shift" && capsLockPossible { - switchToFullCaps() - } + if let touches = event.allTouches, let touch = touches.first { + // Caps lock given two taps of shift. + if touch.tapCount == 2, originalKey == "shift", capsLockPossible { + switchToFullCaps() + } - // To make sure that the user can still use the double space period shortcut after numbers and symbols. - let punctuationThatCancelsShortcut = ["?", "!", ",", ".", ":", ";", "-"] - if originalKey != "shift" && proxy.documentContextBeforeInput?.count != 1 && ![.translate, .conjugate, .plural].contains(commandState) { - let charBeforeSpace = String(Array(proxy.documentContextBeforeInput!).secondToLast()!) - if punctuationThatCancelsShortcut.contains(charBeforeSpace) { - originalKey = "Cancel shortcut" - } - } else if [.translate, .conjugate, .plural].contains(commandState) { - let charBeforeSpace = String(Array((commandBar?.text!)!).secondToLast()!) - if punctuationThatCancelsShortcut.contains(charBeforeSpace) { - originalKey = "Cancel shortcut" - } - } - // Double space period shortcut. - if touch.tapCount == 2 - && (originalKey == spaceBar || originalKey == languageTextForSpaceBar) - && proxy.documentContextBeforeInput?.count != 1 - && doubleSpacePeriodPossible - && doubleSpacePeriodsEnabled() { - // The first condition prevents a period if the prior characters are spaces as the user wants a series of spaces. - if proxy.documentContextBeforeInput?.suffix(2) != " " && ![.translate, .conjugate, .plural].contains(commandState) { - proxy.deleteBackward() - proxy.insertText(". ") - emojisToShow = .zero // was showing empty emoji spots - keyboardState = .letters - shiftButtonState = .shift - loadKeys() - // The fist condition prevents a period if the prior characters are spaces as the user wants a series of spaces. - } else if commandBar.text!.suffix(2) != " " && [.translate, .conjugate, .plural].contains(commandState) { - commandBar.text! = (commandBar?.text!.deletePriorToCursor())! - commandBar.text! = (commandBar?.text!.insertPriorToCursor(char: ". "))! - keyboardState = .letters - shiftButtonState = .shift - loadKeys() + // To make sure that the user can still use the double space period shortcut after numbers and symbols. + let punctuationThatCancelsShortcut = ["?", "!", ",", ".", ":", ";", "-"] + if originalKey != "shift", proxy.documentContextBeforeInput?.count != 1, + ![.translate, .conjugate, .plural].contains(commandState) { + let charBeforeSpace = String( + Array(proxy.documentContextBeforeInput!).secondToLast()! + ) + if punctuationThatCancelsShortcut.contains(charBeforeSpace) { + originalKey = "Cancel shortcut" + } + } else if [.translate, .conjugate, .plural].contains(commandState) { + let charBeforeSpace = String(Array((commandBar?.text!)!).secondToLast()!) + if punctuationThatCancelsShortcut.contains(charBeforeSpace) { + originalKey = "Cancel shortcut" + } + } + // Double space period shortcut. + if touch.tapCount == 2, + originalKey == spaceBar || originalKey == languageTextForSpaceBar, + proxy.documentContextBeforeInput?.count != 1, + doubleSpacePeriodPossible, + doubleSpacePeriodsEnabled() { + // The first condition prevents a period if the prior characters are spaces as the user wants a series of spaces. + if proxy.documentContextBeforeInput?.suffix(2) != " ", + ![.translate, .conjugate, .plural].contains(commandState) { + proxy.deleteBackward() + proxy.insertText(". ") + emojisToShow = .zero // was showing empty emoji spots + keyboardState = .letters + shiftButtonState = .shift + loadKeys() + // The fist condition prevents a period if the prior characters are spaces as the user wants a series of spaces. + } else if commandBar.text!.suffix(2) != " ", + [.translate, .conjugate, .plural].contains(commandState) { + commandBar.text! = (commandBar?.text!.deletePriorToCursor())! + commandBar.text! = (commandBar?.text!.insertPriorToCursor(char: ". "))! + keyboardState = .letters + shiftButtonState = .shift + loadKeys() + } + // Show auto actions if the keyboard states dictate. + conditionallySetAutoActionBtns() + } } - // Show auto actions if the keyboard states dictate. - conditionallySetAutoActionBtns() - } } - } - - private func switchToFullCaps() { - shiftButtonState = .capsLocked - loadKeys() - conditionallySetAutoActionBtns() - } + private func switchToFullCaps() { + shiftButtonState = .capsLocked - /// Defines the criteria under which delete is long pressed. - /// - /// - Parameters - /// - gesture: the gesture that was received. - @objc func deleteLongPressed(_ gesture: UIGestureRecognizer) { - // Prevent the command state prompt from being deleted. - if let commandBarText = commandBar?.text, [.translate, .conjugate, .plural].contains(commandState), allPrompts.contains(commandBarText) { - gesture.state = .cancelled - commandBar.conditionallyAddPlaceholder() + loadKeys() + conditionallySetAutoActionBtns() } - longPressOnDelete = true - - // Delete is sped up based on the number of deletes that have been completed. - var deleteCount = 0 - if gesture.state == .began { - backspaceTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in - deleteCount += 1 - self.handleDeleteButtonPressed() + /// Defines the criteria under which delete is long pressed. + /// + /// - Parameters + /// - gesture: the gesture that was received. + @objc func deleteLongPressed(_ gesture: UIGestureRecognizer) { + // Prevent the command state prompt from being deleted. + if let commandBarText = commandBar?.text, + [.translate, .conjugate, .plural].contains(commandState), + allPrompts.contains(commandBarText) { + gesture.state = .cancelled + commandBar.conditionallyAddPlaceholder() + } - if deleteCount == 20 { - backspaceTimer?.invalidate() - backspaceTimer = Timer.scheduledTimer(withTimeInterval: 0.07, repeats: true) { _ in - deleteCount += 1 - self.handleDeleteButtonPressed() + longPressOnDelete = true - if deleteCount == 50 { - backspaceTimer?.invalidate() - backspaceTimer = Timer.scheduledTimer(withTimeInterval: 0.04, repeats: true) { _ in + // Delete is sped up based on the number of deletes that have been completed. + var deleteCount = 0 + if gesture.state == .began { + backspaceTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in + deleteCount += 1 self.handleDeleteButtonPressed() - } + + if deleteCount == 20 { + backspaceTimer?.invalidate() + backspaceTimer = Timer.scheduledTimer(withTimeInterval: 0.07, repeats: true) { + _ in + deleteCount += 1 + self.handleDeleteButtonPressed() + + if deleteCount == 50 { + backspaceTimer?.invalidate() + backspaceTimer = Timer.scheduledTimer( + withTimeInterval: 0.04, repeats: true + ) { _ in + self.handleDeleteButtonPressed() + } + } + } + } + } + } else if gesture.state == .ended || gesture.state == .cancelled { + backspaceTimer?.invalidate() + backspaceTimer = nil + longPressOnDelete = false + if let button = gesture.view as? UIButton { + button.backgroundColor = specialKeyColor + styleDeleteButton(button, isPressed: false) } - } } - } - } else if gesture.state == .ended || gesture.state == .cancelled { - backspaceTimer?.invalidate() - backspaceTimer = nil - longPressOnDelete = false - if let button = gesture.view as? UIButton { - button.backgroundColor = specialKeyColor - styleDeleteButton(button, isPressed: false) - } } - } - /// Resets key coloration after they have been changed to keyPressedColor. - /// - /// - Parameters - /// - sender: the key that was pressed. - @objc func keyUntouched(_ sender: UIButton) { - guard let isSpecial = sender.layer.value(forKey: "isSpecial") as? Bool else { return } - sender.backgroundColor = isSpecial ? specialKeyColor : keyColor - } + /// Resets key coloration after they have been changed to keyPressedColor. + /// + /// - Parameters + /// - sender: the key that was pressed. + @objc func keyUntouched(_ sender: UIButton) { + guard let isSpecial = sender.layer.value(forKey: "isSpecial") as? Bool else { return } + sender.backgroundColor = isSpecial ? specialKeyColor : keyColor + } + + /// Generates a pop up of the key pressed. + /// + /// - Parameters + /// - key: the key pressed. + @objc func genPopUpView(key: UIButton) { + let charPressed = key.layer.value(forKey: "original") as? String ?? "" + let displayChar = key.layer.value(forKey: "keyToDisplay") as? String ?? "" + genKeyPop(key: key, layer: keyPopLayer, char: charPressed, displayChar: displayChar) + + view.layer.addSublayer(keyPopLayer) + view.addSubview(keyPopChar) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.125) { + keyPopLayer.removeFromSuperlayer() + keyPopChar.removeFromSuperview() + } + } + + /// Generates a pop up of the key long pressed. + /// + /// - Parameters + /// - sender: the long press of the given key. + @objc func genHoldPopUpView(sender: UILongPressGestureRecognizer) { + // Derive which button was pressed and get its alternates. + guard let key: UIButton = sender.view as? UIButton else { return } + let charPressed = key.layer.value(forKey: "original") as? String ?? "" + let displayChar = key.layer.value(forKey: "keyToDisplay") as? String ?? "" + + // Timer is short as the alternates view gets canceled by sender.state.changed. + _ = Timer.scheduledTimer(withTimeInterval: 0.00001, repeats: false) { _ in + if keysWithAlternates.contains(charPressed) { + self.setAlternatesView(sender: sender) + keyHoldPopLayer.removeFromSuperlayer() + keyHoldPopChar.removeFromSuperview() + } + } - /// Generates a pop up of the key pressed. - /// - /// - Parameters - /// - key: the key pressed. - @objc func genPopUpView(key: UIButton) { - let charPressed = key.layer.value(forKey: "original") as? String ?? "" - let displayChar = key.layer.value(forKey: "keyToDisplay") as? String ?? "" - genKeyPop(key: key, layer: keyPopLayer, char: charPressed, displayChar: displayChar) - - view.layer.addSublayer(keyPopLayer) - view.addSubview(keyPopChar) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.125) { - keyPopLayer.removeFromSuperlayer() - keyPopChar.removeFromSuperview() - } - } + switch sender.state { + case .began: + genKeyPop(key: key, layer: keyHoldPopLayer, char: charPressed, displayChar: displayChar) + view.layer.addSublayer(keyHoldPopLayer) + view.addSubview(keyHoldPopChar) + + case .ended: + // Remove the key hold pop up and execute key only if the alternates view isn't present. + keyHoldPopLayer.removeFromSuperlayer() + keyHoldPopChar.removeFromSuperview() + if !keysWithAlternates.contains(charPressed) { + executeKeyActions(key) + } else if view.viewWithTag(1001) == nil { + executeKeyActions(key) + } + keyUntouched(key) - /// Generates a pop up of the key long pressed. - /// - /// - Parameters - /// - sender: the long press of the given key. - @objc func genHoldPopUpView(sender: UILongPressGestureRecognizer) { - // Derive which button was pressed and get its alternates. - guard let key: UIButton = sender.view as? UIButton else { return } - let charPressed = key.layer.value(forKey: "original") as? String ?? "" - let displayChar = key.layer.value(forKey: "keyToDisplay") as? String ?? "" - - // Timer is short as the alternates view gets canceled by sender.state.changed. - _ = Timer.scheduledTimer(withTimeInterval: 0.00001, repeats: false) { _ in - if keysWithAlternates.contains(charPressed) { - self.setAlternatesView(sender: sender) - keyHoldPopLayer.removeFromSuperlayer() - keyHoldPopChar.removeFromSuperview() - } + default: + break + } } - switch sender.state { - case .began: - genKeyPop(key: key, layer: keyHoldPopLayer, char: charPressed, displayChar: displayChar) - view.layer.addSublayer(keyHoldPopLayer) - view.addSubview(keyHoldPopChar) - - case .ended: - // Remove the key hold pop up and execute key only if the alternates view isn't present. - keyHoldPopLayer.removeFromSuperlayer() - keyHoldPopChar.removeFromSuperview() - if !keysWithAlternates.contains(charPressed) { - executeKeyActions(key) - } else if view.viewWithTag(1001) == nil { - executeKeyActions(key) - } - keyUntouched(key) - - default: - break - } - } + /// Sets the characters that can be selected on an alternates view that is generated. + /// + /// - Parameters + /// - sender: the long press of the given key. + @objc func setAlternatesView(sender: UILongPressGestureRecognizer) { + // Only run this code when the state begins. + if sender.state != UIGestureRecognizer.State.began { + return + } - /// Sets the characters that can be selected on an alternates view that is generated. - /// - /// - Parameters - /// - sender: the long press of the given key. - @objc func setAlternatesView(sender: UILongPressGestureRecognizer) { - // Only run this code when the state begins. - if sender.state != UIGestureRecognizer.State.began { - return - } + // Derive which button was pressed and get its alternates. + guard let key: UIButton = sender.view as? UIButton else { return } + genAlternatesView(key: key) - // Derive which button was pressed and get its alternates. - guard let key: UIButton = sender.view as? UIButton else { return } - genAlternatesView(key: key) + alternateBtnStartX = 5.0 + var alternatesBtnY = key.frame.height * scalarAlternatesBtnYPhone + if DeviceType.isPad { + alternatesBtnY = key.frame.height * scalarAlternatesBtnYPad + } + for char in alternateKeys { + let alternateKey = KeyboardKey( + frame: CGRect( + x: alternateBtnStartX, + y: alternatesBtnY, + width: key.frame.width, + height: alternatesBtnHeight + ) + ) + if shiftButtonState == .normal || char == "ß" { + alternateKey.setTitle(char, for: .normal) + } else { + alternateKey.setTitle(char.capitalized, for: .normal) + } + alternateKey.setCharSize() + alternateKey.setTitleColor(keyCharColor, for: .normal) + alternateKey.layer.cornerRadius = keyCornerRadius + + alternatesKeyView.addSubview(alternateKey) + if char == alternateKeys.first, keysWithAlternatesLeft.contains(char) { + setBtn( + btn: alternateKey, color: commandKeyColor, name: char, canBeCapitalized: true, + isSpecial: false + ) + } else if char == alternateKeys.last, keysWithAlternatesRight.contains(char) { + setBtn( + btn: alternateKey, color: commandKeyColor, name: char, canBeCapitalized: true, + isSpecial: false + ) + } else { + setBtn( + btn: alternateKey, color: keyColor, name: char, canBeCapitalized: true, + isSpecial: false + ) + } + activateBtn(btn: alternateKey) - alternateBtnStartX = 5.0 - var alternatesBtnY = key.frame.height * scalarAlternatesBtnYPhone - if DeviceType.isPad { - alternatesBtnY = key.frame.height * scalarAlternatesBtnYPad - } - for char in alternateKeys { - let alternateKey = KeyboardKey( - frame: CGRect( - x: alternateBtnStartX, - y: alternatesBtnY, - width: key.frame.width, - height: alternatesBtnHeight - ) - ) - if shiftButtonState == .normal || char == "ß" { - alternateKey.setTitle(char, for: .normal) - } else { - alternateKey.setTitle(char.capitalized, for: .normal) - } - alternateKey.setCharSize() - alternateKey.setTitleColor(keyCharColor, for: .normal) - alternateKey.layer.cornerRadius = keyCornerRadius - - alternatesKeyView.addSubview(alternateKey) - if char == alternateKeys.first && keysWithAlternatesLeft.contains(char) { - setBtn(btn: alternateKey, color: commandKeyColor, name: char, canBeCapitalized: true, isSpecial: false) - } else if char == alternateKeys.last && keysWithAlternatesRight.contains(char) { - setBtn(btn: alternateKey, color: commandKeyColor, name: char, canBeCapitalized: true, isSpecial: false) - } else { - setBtn(btn: alternateKey, color: keyColor, name: char, canBeCapitalized: true, isSpecial: false) - } - activateBtn(btn: alternateKey) + alternateBtnStartX += (key.frame.width + 3.0) + } - alternateBtnStartX += (key.frame.width + 3.0) - } + // If alternateKeysView is already added than remove and then add again. + if view.viewWithTag(1001) != nil { + let viewWithTag = view.viewWithTag(1001) + viewWithTag?.removeFromSuperview() + alternatesShapeLayer.removeFromSuperlayer() + } - // If alternateKeysView is already added than remove and then add again. - if view.viewWithTag(1001) != nil { - let viewWithTag = view.viewWithTag(1001) - viewWithTag?.removeFromSuperview() - alternatesShapeLayer.removeFromSuperlayer() + view.layer.addSublayer(alternatesShapeLayer) + view.addSubview(alternatesKeyView) } - - view.layer.addSublayer(alternatesShapeLayer) - view.addSubview(alternatesKeyView) - } } diff --git a/Keyboards/KeyboardsBase/NavigationStructure.swift b/Keyboards/KeyboardsBase/NavigationStructure.swift index 932868b1..ab8137a1 100644 --- a/Keyboards/KeyboardsBase/NavigationStructure.swift +++ b/Keyboards/KeyboardsBase/NavigationStructure.swift @@ -1,125 +1,131 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/// Recursive navigation structure for dynamic conjugation and declension views. -/// Supports arbitrary depth navigation. +// Recursive navigation structure for dynamic conjugation and declension views. +// Supports arbitrary depth navigation. import Foundation /// Represents a single option in the navigation. enum NavigationNode { - case nextLevel(NavigationLevel, displayValue: String?) // navigate deeper, with optional display value - case finalValue(String) // terminal node, insert this text + case nextLevel(NavigationLevel, displayValue: String?) // navigate deeper, with optional display value + case finalValue(String) // terminal node, insert this text } /// Represents a level in the navigation hierarchy. struct NavigationLevel { - let title: String // title for command bar - let options: [(label: String, node: NavigationNode)] // buttons to display + let title: String // title for command bar + let options: [(label: String, node: NavigationNode)] // buttons to display } /// Builds navigation trees for conjugations and declensions. -struct NavigationBuilder { - - /// Builds the conjugation navigation levels for a given verb and language. - /// - Parameters: - /// - verb: The verb to conjugate. - /// - language: The language code (e.g., "de", "ru"). - static func buildConjugationCases( - verb: String, - language: String - ) -> [NavigationLevel]? { - guard let conjugationData = ConjugationManager.shared.getConjugations( - verb: verb, - language: language - ) else { - return nil - } - - var tenseLevels: [NavigationLevel] = [] - - for (tenseTitle, conjugationTypes) in conjugationData { - if conjugationTypes.count == 1 { - // Single type: show forms directly. - let (_, forms) = conjugationTypes[0] - let formOptions = forms.map { (pronoun, conjugatedForm) in - (label: pronoun, node: NavigationNode.finalValue(conjugatedForm)) +enum NavigationBuilder { + /// Builds the conjugation navigation levels for a given verb and language. + /// - Parameters: + /// - verb: The verb to conjugate. + /// - language: The language code (e.g., "de", "ru"). + static func buildConjugationCases( + verb: String, + language: String + ) -> [NavigationLevel]? { + guard + let conjugationData = ConjugationManager.shared.getConjugations( + verb: verb, + language: language + ) + else { + return nil } - tenseLevels.append(NavigationLevel( - title: "\(tenseTitle): \(verb)", - options: formOptions - )) - - } else { - // Multiple types: create type selection buttons with display values. - var typeOptions: [(label: String, node: NavigationNode)] = [] - - for (typeTitle, forms) in conjugationTypes { - // Create display value from first 2-3 forms. - let displayValue = forms.prefix(3).map { $0.1 }.joined(separator: "/") - - let formOptions = forms.map { (pronoun, conjugatedForm) in - (label: pronoun, node: NavigationNode.finalValue(conjugatedForm)) - } - let formLevel = NavigationLevel( - title: "\(tenseTitle) - \(typeTitle): \(verb)", - options: formOptions - ) - typeOptions.append((label: typeTitle, node: .nextLevel(formLevel, displayValue: displayValue))) + var tenseLevels: [NavigationLevel] = [] + + for (tenseTitle, conjugationTypes) in conjugationData { + if conjugationTypes.count == 1 { + // Single type: show forms directly. + let (_, forms) = conjugationTypes[0] + let formOptions = forms.map { pronoun, conjugatedForm in + (label: pronoun, node: NavigationNode.finalValue(conjugatedForm)) + } + + tenseLevels.append( + NavigationLevel( + title: "\(tenseTitle): \(verb)", + options: formOptions + ) + ) + } else { + // Multiple types: create type selection buttons with display values. + var typeOptions: [(label: String, node: NavigationNode)] = [] + + for (typeTitle, forms) in conjugationTypes { + // Create display value from first 2-3 forms. + let displayValue = forms.prefix(3).map { $0.1 }.joined(separator: "/") + + let formOptions = forms.map { pronoun, conjugatedForm in + (label: pronoun, node: NavigationNode.finalValue(conjugatedForm)) + } + let formLevel = NavigationLevel( + title: "\(tenseTitle) - \(typeTitle): \(verb)", + options: formOptions + ) + typeOptions.append( + (label: typeTitle, node: .nextLevel(formLevel, displayValue: displayValue)) + ) + } + + tenseLevels.append( + NavigationLevel( + title: "\(tenseTitle): \(verb)", + options: typeOptions + ) + ) + } } - tenseLevels.append(NavigationLevel( - title: "\(tenseTitle): \(verb)", - options: typeOptions - )) - } + return tenseLevels } - return tenseLevels - } - - /// Loads declension cases for a given language. - /// - Parameters: - /// -language: The language code (e.g., "de", "ru"). - static func getDeclensionCases(language: String) -> [NavigationLevel]? { - return DeclensionManager.shared.loadDeclensions(language: language) - } - - /// Finds the starting index for a declension case based on prep form. - static func findStartingCaseIndex(prepForm: String, language: String) -> Int { - if language == "de" { - if prepForm.contains("Acc") { - return 0 // Akkusativ Definitpronomen - } else if prepForm.contains("Dat") { - return 5 // Dativ Definitpronomen - } else { - return 10 // Genitiv Definitpronomen - } - } else if language == "ru" { - if prepForm.contains("Acc") { + /// Loads declension cases for a given language. + /// - Parameters: + /// -language: The language code (e.g., "de", "ru"). + static func getDeclensionCases(language: String) -> [NavigationLevel]? { + return DeclensionManager.shared.loadDeclensions(language: language) + } + + /// Finds the starting index for a declension case based on prep form. + static func findStartingCaseIndex(prepForm: String, language: String) -> Int { + if language == "de" { + if prepForm.contains("Acc") { + return 0 // Akkusativ Definitpronomen + } else if prepForm.contains("Dat") { + return 5 // Dativ Definitpronomen + } else { + return 10 // Genitiv Definitpronomen + } + } else if language == "ru" { + if prepForm.contains("Acc") { + return 0 + } else if prepForm.contains("Dat") { + return 1 + } else if prepForm.contains("Gen") { + return 2 + } else if prepForm.contains("Ins") { + return 3 + } else { + return 4 + } + } return 0 - } else if prepForm.contains("Dat") { - return 1 - } else if prepForm.contains("Gen") { - return 2 - } else if prepForm.contains("Ins") { - return 3 - } else { - return 4 - } } - return 0 - } - - /// Builds information navigation levels for tooltips. - static func buildInformationCases(isTranslate: Bool = false) -> [NavigationLevel] { - let content = isTranslate ? InformationToolTipData.getWiktionaryContent() : InformationToolTipData.getContent() - - return content.map { attributedText in - NavigationLevel( - title: isTranslate ? invalidCommandMsgWiktionary : invalidCommandMsgWikidata, - options: [(label: "", node: .finalValue(attributedText.string))] - ) - } -} + + /// Builds information navigation levels for tooltips. + static func buildInformationCases(isTranslate: Bool = false) -> [NavigationLevel] { + let content = isTranslate ? InformationToolTipData.getWiktionaryContent() : InformationToolTipData.getContent() + + return content.map { attributedText in + NavigationLevel( + title: isTranslate ? invalidCommandMsgWiktionary : invalidCommandMsgWikidata, + options: [(label: "", node: .finalValue(attributedText.string))] + ) + } + } } diff --git a/Keyboards/KeyboardsBase/ScribeFunctionality/Annotate.swift b/Keyboards/KeyboardsBase/ScribeFunctionality/Annotate.swift index ffa40df9..d553ddf0 100644 --- a/Keyboards/KeyboardsBase/ScribeFunctionality/Annotate.swift +++ b/Keyboards/KeyboardsBase/ScribeFunctionality/Annotate.swift @@ -1,63 +1,65 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Functions and elements that control word annotations. */ import UIKit -// Dictionary for accessing keyboard conjugation state. +/// Dictionary for accessing keyboard conjugation state. var nounFormToColorDict = [ - "F": annotateRed, - "M": annotateBlue, - "C": annotatePurple, - "U": annotatePurple, - "N": annotateGreen, - "PL": annotateOrange, - "Ж": annotateRed, - "М": annotateBlue, - "Н": annotateGreen, - "МН": annotateOrange + "F": annotateRed, + "M": annotateBlue, + "C": annotatePurple, + "U": annotatePurple, + "N": annotateGreen, + "PL": annotateOrange, + "Ж": annotateRed, + "М": annotateBlue, + "Н": annotateGreen, + "МН": annotateOrange ] -// Dictionary to convert noun annotations into the keyboard language. +/// Dictionary to convert noun annotations into the keyboard language. let nounAnnotationConversionDict = [ - "Swedish": ["C": "U"], - "Russian": ["F": "Ж", "M": "М", "N": "Н", "PL": "МН"] + "Swedish": ["C": "U"], + "Russian": ["F": "Ж", "M": "М", "N": "Н", "PL": "МН"] ] -// Dictionary to convert case annotations into the keyboard language. +/// Dictionary to convert case annotations into the keyboard language. let prepAnnotationConversionDict = [ - "German": ["Acc": "Akk"], - "Russian": ["Acc": "Вин", "Dat": "Дат", "Gen": "Род", "Loc": "Мес", "Pre": "Пре", "Ins": "Инс"] + "German": ["Acc": "Akk"], + "Russian": [ + "Acc": "Вин", "Dat": "Дат", "Gen": "Род", "Loc": "Мес", "Pre": "Пре", "Ins": "Инс" + ] ] -// Converts full gender names to abbreviations (e.g., "feminine" → "F"). +/// Converts full gender names to abbreviations (e.g., "feminine" → "F"). func convertFullGenderToAbbr(_ genderFull: String) -> String { - let genderMap: [String: String] = [ - "feminine": "F", - "masculine": "M", - "neuter": "N", - "common": "C", - "common of two genders": "C", - "PL": "PL" - ] - - return genderMap[genderFull.lowercased()] ?? genderFull + let genderMap: [String: String] = [ + "feminine": "F", + "masculine": "M", + "neuter": "N", + "common": "C", + "common of two genders": "C", + "PL": "PL" + ] + + return genderMap[genderFull.lowercased()] ?? genderFull } -// Converts full preposition to abbreviations. +/// Converts full preposition to abbreviations. func convertFullPrepositionToAbbr(_ prepositionFull: String) -> String { - let prepositionMap: [String: String] = [ - "genitive case": "Gen", - "accusative case": "Acc", - "dative case": "Dat", - "locative case": "Loc", - "prepositional case": "Pre", - "instrumental case": "Ins" - ] - - return prepositionMap[prepositionFull.lowercased()] ?? prepositionFull + let prepositionMap: [String: String] = [ + "genitive case": "Gen", + "accusative case": "Acc", + "dative case": "Dat", + "locative case": "Loc", + "prepositional case": "Pre", + "instrumental case": "Ins" + ] + + return prepositionMap[prepositionFull.lowercased()] ?? prepositionFull } /// The base function for annotation that's accessed by `typedWordAnnotation`. @@ -66,201 +68,236 @@ func convertFullPrepositionToAbbr(_ prepositionFull: String) -> String { /// - wordToAnnotate: the word that an annotation should be created for. /// - KVC: the keyboard view controller. func wordAnnotation(wordToAnnotate: String, KVC: KeyboardViewController) { - var genderAnnotations: [String] = [] - var pluralAnnotation: String? - - // Get gender(s) - let nounForm = LanguageDBManager.shared.queryNounForm(of: wordToAnnotate)[0] - if !nounForm.isEmpty { - // nounForm might already be "M/F" if multiple genders found. - genderAnnotations = nounForm.components(separatedBy: "/") - } - - // Check if plural - if let pluralWords = pluralWords, pluralWords.contains(wordToAnnotate.lowercased()) { - pluralAnnotation = "PL" - } - - // Combine: gender(s) first, then PL if applicable. - var allAnnotations = genderAnnotations - if let pl = pluralAnnotation, !allAnnotations.contains("PL") { - allAnnotations.append(pl) - } - - // Join back with "/". - let combinedNounForm = allAnnotations.joined(separator: "/") - - prepAnnotationForm = LanguageDBManager.shared.queryPrepForm(of: wordToAnnotate.lowercased())[0] - if !prepAnnotationForm.isEmpty { - prepAnnotationForm = convertFullPrepositionToAbbr(prepAnnotationForm) - } - - hasNounForm = !combinedNounForm.isEmpty - hasPrepForm = !prepAnnotationForm.isEmpty - - annotationsToAssign = [String]() - annotationBtns = [UIButton]() - annotationColors = [UIColor]() - annotationSeparators = [UIView]() - - let annotationFieldWidth = KVC.translateKey.frame.width * 0.85 - let annotationHeight = KVC.scribeKey.frame.height - - if wordToAnnotate == "Scribe" || wordToAnnotate == "scribe" { - // Thank the user :) - annotationState = true - activateAnnotationBtn = true - autoAction0Visible = false - - let annotationBtn = Annotation() - annotationBtn.setAnnotationSize( - width: annotationFieldWidth, height: annotationHeight, fontSize: annotationHeight * 0.55 - ) - annotationBtn.setAnnotationLoc( - minX: KVC.translateKey.frame.origin.x - + (KVC.translateKey.frame.width / 2) - - (annotationFieldWidth / 2), - maxY: KVC.scribeKey.frame.origin.y - ) - annotationBtn.styleSingleAnnotation(fullAnnotation: true) - - let emojisToSelectFrom = "🥳🎉" - let emojis = String((0 ..< 3).compactMap { _ in emojisToSelectFrom.randomElement() }) - annotationBtn.setTitle(emojis, for: .normal) - KVC.view.addSubview(annotationBtn) - annotationBtns.append(annotationBtn) - annotationColors.append(commandKeyColor) - - KVC.activateBtn(btn: annotationBtn) - setBtn( - btn: annotationBtn, color: commandKeyColor, name: "ScribeAnnotation", canBeCapitalized: false, isSpecial: false - ) - } else { - if hasNounForm { - if !combinedNounForm.contains("/") { - annotationsToAssign.append(combinedNounForm) - } else { - for a in combinedNounForm.components(separatedBy: "/") { - annotationsToAssign.append(a) - } - } + var genderAnnotations: [String] = [] + var pluralAnnotation: String? + + // Get gender(s) + let nounForm = LanguageDBManager.shared.queryNounForm(of: wordToAnnotate)[0] + if !nounForm.isEmpty { + // nounForm might already be "M/F" if multiple genders found. + genderAnnotations = nounForm.components(separatedBy: "/") } - if hasPrepForm { - activateAnnotationBtn = true - if !prepAnnotationForm.contains("/") { - annotationsToAssign.append(prepAnnotationForm) - } else { - for a in prepAnnotationForm.components(separatedBy: "/") { - annotationsToAssign.append(a) - } - } + // Check if plural + if let pluralWords = pluralWords, pluralWords.contains(wordToAnnotate.lowercased()) { + pluralAnnotation = "PL" } - let numAnnotations = annotationsToAssign.count - if numAnnotations > 0 { - annotationState = true - autoAction0Visible = false + // Combine: gender(s) first, then PL if applicable. + var allAnnotations = genderAnnotations + if let pl = pluralAnnotation, !allAnnotations.contains("PL") { + allAnnotations.append(pl) + } - let annotationWidth = annotationFieldWidth / CGFloat(annotationsToAssign.count) + // Join back with "/". + let combinedNounForm = allAnnotations.joined(separator: "/") - for i in 0 ..< numAnnotations { - let annotationBtn = Annotation() - var annotationSep = UIView() - var annotationToDisplay: String = annotationsToAssign[i] + prepAnnotationForm = LanguageDBManager.shared.queryPrepForm(of: wordToAnnotate.lowercased())[0] + if !prepAnnotationForm.isEmpty { + prepAnnotationForm = convertFullPrepositionToAbbr(prepAnnotationForm) + } - if ["feminine", "masculine", "neuter", "common"].contains(annotationToDisplay.lowercased()) { - annotationToDisplay = convertFullGenderToAbbr(annotationToDisplay) - annotationsToAssign[i] = annotationToDisplay // update the array too - } + hasNounForm = !combinedNounForm.isEmpty + hasPrepForm = !prepAnnotationForm.isEmpty - if nounFormToColorDict.keys.contains(annotationToDisplay) { - if numAnnotations > 3 { - annotationBtn.setAnnotationSize(width: annotationWidth, height: annotationHeight, fontSize: annotationHeight * 0.4) - } else if numAnnotations > 2 { - annotationBtn.setAnnotationSize(width: annotationWidth, height: annotationHeight, fontSize: annotationHeight * 0.55) - } else { - annotationBtn.setAnnotationSize(width: annotationWidth, height: annotationHeight, fontSize: annotationHeight * 0.6) - } - } else { - if numAnnotations > 3 { - annotationBtn.setAnnotationSize(width: annotationWidth, height: annotationHeight, fontSize: annotationHeight * 0.4) - } else if numAnnotations == 1 { - annotationBtn.setAnnotationSize(width: annotationWidth, height: annotationHeight, fontSize: annotationHeight * 0.55) - } else { - annotationBtn.setAnnotationSize(width: annotationWidth, height: annotationHeight, fontSize: annotationHeight * 0.5) - } - } + annotationsToAssign = [String]() + annotationBtns = [UIButton]() + annotationColors = [UIColor]() + annotationSeparators = [UIView]() + + let annotationFieldWidth = KVC.translateKey.frame.width * 0.85 + let annotationHeight = KVC.scribeKey.frame.height + + if wordToAnnotate == "Scribe" || wordToAnnotate == "scribe" { + // Thank the user :) + annotationState = true + activateAnnotationBtn = true + autoAction0Visible = false + let annotationBtn = Annotation() + annotationBtn.setAnnotationSize( + width: annotationFieldWidth, height: annotationHeight, fontSize: annotationHeight * 0.55 + ) annotationBtn.setAnnotationLoc( - minX: KVC.translateKey.frame.origin.x - + (KVC.translateKey.frame.width / 2) - - (annotationFieldWidth / 2) - + (annotationWidth * CGFloat(i)), - maxY: KVC.scribeKey.frame.origin.y + minX: KVC.translateKey.frame.origin.x + + (KVC.translateKey.frame.width / 2) + - (annotationFieldWidth / 2), + maxY: KVC.scribeKey.frame.origin.y ) - if numAnnotations == 1 { - annotationBtn.styleSingleAnnotation(fullAnnotation: true) - } else if i == 0 { - annotationBtn.styleLeftAnnotation(fullAnnotation: true) - } else if i == numAnnotations - 1 { - annotationBtn.styleRightAnnotation(fullAnnotation: true) - } else { - annotationBtn.styleMiddleAnnotation() - } - - // Convert the annotation into the keyboard language. - if nounFormToColorDict.keys.contains(annotationToDisplay) { - if nounAnnotationConversionDict[controllerLanguage] != nil { - if nounAnnotationConversionDict[controllerLanguage]?[annotationsToAssign[i]] != nil { - annotationToDisplay = nounAnnotationConversionDict[controllerLanguage]?[annotationsToAssign[i]] ?? "" - } - } - } else { - if prepAnnotationConversionDict[controllerLanguage] != nil { - if prepAnnotationConversionDict[controllerLanguage]?[annotationsToAssign[i]] != nil { - annotationToDisplay = prepAnnotationConversionDict[controllerLanguage]?[annotationsToAssign[i]] ?? "" - } - } - } + annotationBtn.styleSingleAnnotation(fullAnnotation: true) - annotationBtn.setTitle(annotationToDisplay, for: .normal) + let emojisToSelectFrom = "🥳🎉" + let emojis = String((0 ..< 3).compactMap { _ in emojisToSelectFrom.randomElement() }) + annotationBtn.setTitle(emojis, for: .normal) KVC.view.addSubview(annotationBtn) annotationBtns.append(annotationBtn) - if nounFormToColorDict.keys.contains(annotationToDisplay) { - if let annotationColor = nounFormToColorDict[annotationsToAssign[i]] { - annotationColors.append(annotationColor) - } - } else { - annotationColors.append(UITraitCollection.current.userInterfaceStyle == .light ? .black : .white) - } - if activateAnnotationBtn { - KVC.activateBtn(btn: annotationBtn) - } + annotationColors.append(commandKeyColor) + + KVC.activateBtn(btn: annotationBtn) setBtn( - btn: annotationBtn, - color: annotationColors[i], - name: "GetAnnotationInfo", - canBeCapitalized: false, - isSpecial: false + btn: annotationBtn, color: commandKeyColor, name: "ScribeAnnotation", + canBeCapitalized: false, + isSpecial: false ) + } else { + if hasNounForm { + if !combinedNounForm.contains("/") { + annotationsToAssign.append(combinedNounForm) + } else { + for a in combinedNounForm.components(separatedBy: "/") { + annotationsToAssign.append(a) + } + } + } - if i != 0 { - annotationSep = UIView( - frame: CGRect( - x: annotationBtn.frame.minX, y: annotationBtn.frame.minY, width: 1, height: annotationBtn.frame.height - ) - ) - annotationSep.isUserInteractionEnabled = false - annotationSep.backgroundColor = UITraitCollection.current.userInterfaceStyle == .light ? keyColor : specialKeyColor - KVC.view.addSubview(annotationSep) - annotationSeparators.append(annotationSep) + if hasPrepForm { + activateAnnotationBtn = true + if !prepAnnotationForm.contains("/") { + annotationsToAssign.append(prepAnnotationForm) + } else { + for a in prepAnnotationForm.components(separatedBy: "/") { + annotationsToAssign.append(a) + } + } + } + + let numAnnotations = annotationsToAssign.count + if numAnnotations > 0 { + annotationState = true + autoAction0Visible = false + + let annotationWidth = annotationFieldWidth / CGFloat(annotationsToAssign.count) + + for i in 0 ..< numAnnotations { + let annotationBtn = Annotation() + var annotationSep = UIView() + var annotationToDisplay: String = annotationsToAssign[i] + + if ["feminine", "masculine", "neuter", "common"].contains( + annotationToDisplay.lowercased() + ) { + annotationToDisplay = convertFullGenderToAbbr(annotationToDisplay) + annotationsToAssign[i] = annotationToDisplay // update the array too + } + + if nounFormToColorDict.keys.contains(annotationToDisplay) { + if numAnnotations > 3 { + annotationBtn.setAnnotationSize( + width: annotationWidth, height: annotationHeight, + fontSize: annotationHeight * 0.4 + ) + } else if numAnnotations > 2 { + annotationBtn.setAnnotationSize( + width: annotationWidth, height: annotationHeight, + fontSize: annotationHeight * 0.55 + ) + } else { + annotationBtn.setAnnotationSize( + width: annotationWidth, height: annotationHeight, + fontSize: annotationHeight * 0.6 + ) + } + } else { + if numAnnotations > 3 { + annotationBtn.setAnnotationSize( + width: annotationWidth, height: annotationHeight, + fontSize: annotationHeight * 0.4 + ) + } else if numAnnotations == 1 { + annotationBtn.setAnnotationSize( + width: annotationWidth, height: annotationHeight, + fontSize: annotationHeight * 0.55 + ) + } else { + annotationBtn.setAnnotationSize( + width: annotationWidth, height: annotationHeight, + fontSize: annotationHeight * 0.5 + ) + } + } + + annotationBtn.setAnnotationLoc( + minX: KVC.translateKey.frame.origin.x + + (KVC.translateKey.frame.width / 2) + - (annotationFieldWidth / 2) + + (annotationWidth * CGFloat(i)), + maxY: KVC.scribeKey.frame.origin.y + ) + if numAnnotations == 1 { + annotationBtn.styleSingleAnnotation(fullAnnotation: true) + } else if i == 0 { + annotationBtn.styleLeftAnnotation(fullAnnotation: true) + } else if i == numAnnotations - 1 { + annotationBtn.styleRightAnnotation(fullAnnotation: true) + } else { + annotationBtn.styleMiddleAnnotation() + } + + // Convert the annotation into the keyboard language. + if nounFormToColorDict.keys.contains(annotationToDisplay) { + if nounAnnotationConversionDict[controllerLanguage] != nil { + if nounAnnotationConversionDict[controllerLanguage]?[annotationsToAssign[i]] + != nil { + annotationToDisplay = + nounAnnotationConversionDict[controllerLanguage]?[ + annotationsToAssign[i] + ] ?? "" + } + } + } else { + if prepAnnotationConversionDict[controllerLanguage] != nil { + if prepAnnotationConversionDict[controllerLanguage]?[annotationsToAssign[i]] + != nil { + annotationToDisplay = + prepAnnotationConversionDict[controllerLanguage]?[ + annotationsToAssign[i] + ] ?? "" + } + } + } + + annotationBtn.setTitle(annotationToDisplay, for: .normal) + KVC.view.addSubview(annotationBtn) + annotationBtns.append(annotationBtn) + if nounFormToColorDict.keys.contains(annotationToDisplay) { + if let annotationColor = nounFormToColorDict[annotationsToAssign[i]] { + annotationColors.append(annotationColor) + } + } else { + annotationColors.append( + UITraitCollection.current.userInterfaceStyle == .light ? .black : .white + ) + } + if activateAnnotationBtn { + KVC.activateBtn(btn: annotationBtn) + } + setBtn( + btn: annotationBtn, + color: annotationColors[i], + name: "GetAnnotationInfo", + canBeCapitalized: false, + isSpecial: false + ) + + if i != 0 { + annotationSep = UIView( + frame: CGRect( + x: annotationBtn.frame.minX, y: annotationBtn.frame.minY, width: 1, + height: annotationBtn.frame.height + ) + ) + annotationSep.isUserInteractionEnabled = false + annotationSep.backgroundColor = + UITraitCollection.current.userInterfaceStyle == .light + ? keyColor : specialKeyColor + KVC.view.addSubview(annotationSep) + annotationSeparators.append(annotationSep) + } + } + } else { + return } - } - } else { - return } - } } /// Annotates a typed word after a space or auto action. @@ -268,23 +305,26 @@ func wordAnnotation(wordToAnnotate: String, KVC: KeyboardViewController) { /// - Parameters /// - KVC: the keyboard view controller. func typedWordAnnotation(KVC: KeyboardViewController) { - guard let documentContextBeforeInput = proxy.documentContextBeforeInput, !documentContextBeforeInput.isEmpty else { - return - } + guard let documentContextBeforeInput = proxy.documentContextBeforeInput, + !documentContextBeforeInput.isEmpty + else { + return + } - wordsTyped = documentContextBeforeInput.components(separatedBy: " ") + wordsTyped = documentContextBeforeInput.components(separatedBy: " ") - guard let lastWordTyped = wordsTyped.secondToLast() else { - return - } + guard let lastWordTyped = wordsTyped.secondToLast() + else { + return + } - if !languagesWithCapitalizedNouns.contains(controllerLanguage) { - wordToCheck = lastWordTyped.lowercased() - } else { - wordToCheck = lastWordTyped - } + if !languagesWithCapitalizedNouns.contains(controllerLanguage) { + wordToCheck = lastWordTyped.lowercased() + } else { + wordToCheck = lastWordTyped + } - wordAnnotation(wordToAnnotate: wordToCheck, KVC: KVC) + wordAnnotation(wordToAnnotate: wordToCheck, KVC: KVC) } /// Annotates nouns during autocomplete and autosuggest. @@ -294,209 +334,226 @@ func typedWordAnnotation(KVC: KeyboardViewController) { /// - index: the auto action key index that the annotation should be set for. /// - KVC: the keyboard view controller. func autoActionAnnotation(autoActionWord: String, index: Int, KVC: KeyboardViewController) { - var genderAnnotations: [String] = [] - var pluralAnnotation: String? + var genderAnnotations: [String] = [] + var pluralAnnotation: String? - // Get gender(s) - let nounForm = LanguageDBManager.shared.queryNounForm(of: autoActionWord)[0] - if !nounForm.isEmpty { - genderAnnotations = nounForm.components(separatedBy: "/") - } + // Get gender(s) + let nounForm = LanguageDBManager.shared.queryNounForm(of: autoActionWord)[0] + if !nounForm.isEmpty { + genderAnnotations = nounForm.components(separatedBy: "/") + } - // Check if plural - if let pluralWords = pluralWords, pluralWords.contains(autoActionWord.lowercased()) { - pluralAnnotation = "PL" - } + // Check if plural + if let pluralWords = pluralWords, pluralWords.contains(autoActionWord.lowercased()) { + pluralAnnotation = "PL" + } - // Combine - var allAnnotations = genderAnnotations - if let pl = pluralAnnotation, !allAnnotations.contains("PL") { - allAnnotations.append(pl) - } + // Combine + var allAnnotations = genderAnnotations + if let pl = pluralAnnotation, !allAnnotations.contains("PL") { + allAnnotations.append(pl) + } - let combinedNounForm = allAnnotations.joined(separator: "/") + let combinedNounForm = allAnnotations.joined(separator: "/") - hasNounForm = !combinedNounForm.isEmpty + hasNounForm = !combinedNounForm.isEmpty - newAutoActionAnnotationsToAssign = [String]() - newAutoActionAnnotationBtns = [UIButton]() - newAutoActionAnnotationColors = [UIColor]() - newAutoActionAnnotationSeparators = [UIView]() + newAutoActionAnnotationsToAssign = [String]() + newAutoActionAnnotationBtns = [UIButton]() + newAutoActionAnnotationColors = [UIColor]() + newAutoActionAnnotationSeparators = [UIView]() - let annotationFieldWidth = KVC.translateKey.frame.width * 0.85 - let annotationHeight = KVC.scribeKey.frame.height * 0.1 + let annotationFieldWidth = KVC.translateKey.frame.width * 0.85 + let annotationHeight = KVC.scribeKey.frame.height * 0.1 - if hasNounForm { - if !combinedNounForm.contains("/") { - newAutoActionAnnotationsToAssign.append(combinedNounForm) - } else { - for a in combinedNounForm.components(separatedBy: "/") { - newAutoActionAnnotationsToAssign.append(a) - } + if hasNounForm { + if !combinedNounForm.contains("/") { + newAutoActionAnnotationsToAssign.append(combinedNounForm) + } else { + for a in combinedNounForm.components(separatedBy: "/") { + newAutoActionAnnotationsToAssign.append(a) + } + } } - } - let numAnnotations = newAutoActionAnnotationsToAssign.count - if numAnnotations > 0 { - let annotationWidth = annotationFieldWidth / CGFloat(newAutoActionAnnotationsToAssign.count) + let numAnnotations = newAutoActionAnnotationsToAssign.count + if numAnnotations > 0 { + let annotationWidth = annotationFieldWidth / CGFloat(newAutoActionAnnotationsToAssign.count) + + for i in 0 ..< numAnnotations { + let annotationBtn = Annotation() + var annotationSep = UIView() + var annotationToDisplay = newAutoActionAnnotationsToAssign[i] + + if ["feminine", "masculine", "neuter", "common"].contains( + annotationToDisplay.lowercased() + ) { + annotationToDisplay = convertFullGenderToAbbr(annotationToDisplay) + newAutoActionAnnotationsToAssign[i] = annotationToDisplay + } - for i in 0 ..< numAnnotations { - let annotationBtn = Annotation() - var annotationSep = UIView() - var annotationToDisplay = newAutoActionAnnotationsToAssign[i] + if numAnnotations > 3 { + annotationBtn.setAnnotationSize( + width: annotationWidth, height: annotationHeight, + fontSize: annotationHeight * 0.4 + ) + } else if numAnnotations > 2 { + annotationBtn.setAnnotationSize( + width: annotationWidth, height: annotationHeight, + fontSize: annotationHeight * 0.55 + ) + } else { + annotationBtn.setAnnotationSize( + width: annotationWidth, height: annotationHeight, + fontSize: annotationHeight * 0.6 + ) + } - if ["feminine", "masculine", "neuter", "common"].contains(annotationToDisplay.lowercased()) { - annotationToDisplay = convertFullGenderToAbbr(annotationToDisplay) - newAutoActionAnnotationsToAssign[i] = annotationToDisplay - } + if index == 0 { + annotationBtn.setAnnotationLoc( + minX: KVC.translateKey.frame.origin.x + + (KVC.translateKey.frame.width / 2) + - (annotationFieldWidth / 2) + + (annotationWidth * CGFloat(i)), + maxY: KVC.translateKey.frame.origin.y + KVC.translateKey.frame.height - KVC + .scribeKey + .frame.height * 0.1 + ) + } else if index == 1 { + annotationBtn.setAnnotationLoc( + minX: KVC.conjugateKey.frame.origin.x + + (KVC.conjugateKey.frame.width / 2) + - (annotationFieldWidth / 2) + + (annotationWidth * CGFloat(i)), + maxY: KVC.conjugateKey.frame.origin.y + KVC.conjugateKey.frame.height - KVC + .scribeKey + .frame.height * 0.1 + ) + } else if index == 2 { + annotationBtn.setAnnotationLoc( + minX: KVC.pluralKey.frame.origin.x + + (KVC.pluralKey.frame.width / 2) + - (annotationFieldWidth / 2) + + (annotationWidth * CGFloat(i)), + maxY: KVC.pluralKey.frame.origin.y + KVC.pluralKey.frame.height - KVC.scribeKey + .frame + .height * 0.1 + ) + } - if numAnnotations > 3 { - annotationBtn.setAnnotationSize( - width: annotationWidth, height: annotationHeight, fontSize: annotationHeight * 0.4 - ) - } else if numAnnotations > 2 { - annotationBtn.setAnnotationSize( - width: annotationWidth, height: annotationHeight, fontSize: annotationHeight * 0.55 - ) - } else { - annotationBtn.setAnnotationSize( - width: annotationWidth, height: annotationHeight, fontSize: annotationHeight * 0.6 - ) - } + if numAnnotations == 1 { + annotationBtn.styleSingleAnnotation(fullAnnotation: false) + } else if i == 0 { + annotationBtn.styleLeftAnnotation(fullAnnotation: false) + } else if i == numAnnotations - 1 { + annotationBtn.styleRightAnnotation(fullAnnotation: false) + } else { + annotationBtn.styleMiddleAnnotation() + } - if index == 0 { - annotationBtn.setAnnotationLoc( - minX: KVC.translateKey.frame.origin.x - + (KVC.translateKey.frame.width / 2) - - (annotationFieldWidth / 2) - + (annotationWidth * CGFloat(i)), - maxY: KVC.translateKey.frame.origin.y + KVC.translateKey.frame.height - KVC.scribeKey.frame.height * 0.1 - ) - } else if index == 1 { - annotationBtn.setAnnotationLoc( - minX: KVC.conjugateKey.frame.origin.x - + (KVC.conjugateKey.frame.width / 2) - - (annotationFieldWidth / 2) - + (annotationWidth * CGFloat(i)), - maxY: KVC.conjugateKey.frame.origin.y + KVC.conjugateKey.frame.height - KVC.scribeKey.frame.height * 0.1 - ) - } else if index == 2 { - annotationBtn.setAnnotationLoc( - minX: KVC.pluralKey.frame.origin.x - + (KVC.pluralKey.frame.width / 2) - - (annotationFieldWidth / 2) - + (annotationWidth * CGFloat(i)), - maxY: KVC.pluralKey.frame.origin.y + KVC.pluralKey.frame.height - KVC.scribeKey.frame.height * 0.1 - ) - } - - if numAnnotations == 1 { - annotationBtn.styleSingleAnnotation(fullAnnotation: false) - } else if i == 0 { - annotationBtn.styleLeftAnnotation(fullAnnotation: false) - } else if i == numAnnotations - 1 { - annotationBtn.styleRightAnnotation(fullAnnotation: false) - } else { - annotationBtn.styleMiddleAnnotation() - } - - KVC.view.addSubview(annotationBtn) - autoActionAnnotationBtns.append(annotationBtn) - if let annotationColor = nounFormToColorDict[newAutoActionAnnotationsToAssign[i]] { - let colorWithAlpha = annotationColor.withAlphaComponent(0.75) - newAutoActionAnnotationColors.append(colorWithAlpha) - } - - setBtn( - btn: annotationBtn, - color: newAutoActionAnnotationColors[i], - name: "GetAnnotationInfo", - canBeCapitalized: false, - isSpecial: false - ) - // Allow for interaction with the button beneath the annotation. - annotationBtn.isUserInteractionEnabled = false - - if i != 0 { - annotationSep = UIView( - frame: CGRect( - x: annotationBtn.frame.minX, y: annotationBtn.frame.minY, width: 1, height: annotationBtn.frame.height - ) - ) - annotationSep.isUserInteractionEnabled = false - annotationSep.backgroundColor = UITraitCollection.current.userInterfaceStyle == .light ? keyColor : specialKeyColor - KVC.view.addSubview(annotationSep) - autoActionAnnotationSeparators.append(annotationSep) - } + KVC.view.addSubview(annotationBtn) + autoActionAnnotationBtns.append(annotationBtn) + if let annotationColor = nounFormToColorDict[newAutoActionAnnotationsToAssign[i]] { + let colorWithAlpha = annotationColor.withAlphaComponent(0.75) + newAutoActionAnnotationColors.append(colorWithAlpha) + } + + setBtn( + btn: annotationBtn, + color: newAutoActionAnnotationColors[i], + name: "GetAnnotationInfo", + canBeCapitalized: false, + isSpecial: false + ) + // Allow for interaction with the button beneath the annotation. + annotationBtn.isUserInteractionEnabled = false + + if i != 0 { + annotationSep = UIView( + frame: CGRect( + x: annotationBtn.frame.minX, y: annotationBtn.frame.minY, width: 1, + height: annotationBtn.frame.height + ) + ) + annotationSep.isUserInteractionEnabled = false + annotationSep.backgroundColor = + UITraitCollection.current.userInterfaceStyle == .light + ? keyColor : specialKeyColor + KVC.view.addSubview(annotationSep) + autoActionAnnotationSeparators.append(annotationSep) + } + } + } else { + return } - } else { - return - } } /// UIButton class for annotation styling. class Annotation: UIButton { - override init(frame: CGRect) { - super.init(frame: frame) - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - } - - func style() { - clipsToBounds = true - layer.masksToBounds = false - contentHorizontalAlignment = UIControl.ContentHorizontalAlignment.center - setTitleColor(UITraitCollection.current.userInterfaceStyle == .light ? keyColor : specialKeyColor, for: .normal) - if activateAnnotationBtn { - layer.shadowColor = keyShadowColor - layer.shadowOffset = CGSize(width: 0, height: 1.25) - layer.shadowOpacity = 1.0 - layer.shadowRadius = 0 + override init(frame: CGRect) { + super.init(frame: frame) } - } - func styleSingleAnnotation(fullAnnotation: Bool) { - style() - if fullAnnotation { - layer.cornerRadius = commandKeyCornerRadius / 2.5 - } else { - layer.cornerRadius = commandKeyCornerRadius / 5 + required init?(coder: NSCoder) { + super.init(coder: coder) } - } - func styleLeftAnnotation(fullAnnotation: Bool) { - style() - if fullAnnotation { - layer.cornerRadius = commandKeyCornerRadius / 2.5 - } else { - layer.cornerRadius = commandKeyCornerRadius / 5 + func style() { + clipsToBounds = true + layer.masksToBounds = false + contentHorizontalAlignment = UIControl.ContentHorizontalAlignment.center + setTitleColor( + UITraitCollection.current.userInterfaceStyle == .light ? keyColor : specialKeyColor, + for: .normal + ) + if activateAnnotationBtn { + layer.shadowColor = keyShadowColor + layer.shadowOffset = CGSize(width: 0, height: 1.25) + layer.shadowOpacity = 1.0 + layer.shadowRadius = 0 + } } - layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] - } - func styleRightAnnotation(fullAnnotation: Bool) { - style() - if fullAnnotation { - layer.cornerRadius = commandKeyCornerRadius / 2.5 - } else { - layer.cornerRadius = commandKeyCornerRadius / 5 + func styleSingleAnnotation(fullAnnotation: Bool) { + style() + if fullAnnotation { + layer.cornerRadius = commandKeyCornerRadius / 2.5 + } else { + layer.cornerRadius = commandKeyCornerRadius / 5 + } + } + + func styleLeftAnnotation(fullAnnotation: Bool) { + style() + if fullAnnotation { + layer.cornerRadius = commandKeyCornerRadius / 2.5 + } else { + layer.cornerRadius = commandKeyCornerRadius / 5 + } + layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] + } + + func styleRightAnnotation(fullAnnotation: Bool) { + style() + if fullAnnotation { + layer.cornerRadius = commandKeyCornerRadius / 2.5 + } else { + layer.cornerRadius = commandKeyCornerRadius / 5 + } + layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner] + } + + func styleMiddleAnnotation() { + style() + } + + /// First set the size, and then the location which is based on width for proper positioning. + func setAnnotationSize(width: CGFloat, height: CGFloat, fontSize: CGFloat) { + frame = CGRect(x: 0, y: 0, width: width, height: height) + titleLabel?.font = .systemFont(ofSize: fontSize) + } + + func setAnnotationLoc(minX: CGFloat, maxY: CGFloat) { + frame = CGRect(x: minX, y: maxY, width: frame.width, height: frame.height) } - layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner] - } - - func styleMiddleAnnotation() { - style() - } - - // First set the size, and then the location which is based on width for proper positioning. - func setAnnotationSize(width: CGFloat, height: CGFloat, fontSize: CGFloat) { - frame = CGRect(x: 0, y: 0, width: width, height: height) - titleLabel?.font = .systemFont(ofSize: fontSize) - } - - func setAnnotationLoc(minX: CGFloat, maxY: CGFloat) { - frame = CGRect(x: minX, y: maxY, width: frame.width, height: frame.height) - } } diff --git a/Keyboards/KeyboardsBase/ScribeFunctionality/CommandBar.swift b/Keyboards/KeyboardsBase/ScribeFunctionality/CommandBar.swift index 60fcb490..5718990a 100644 --- a/Keyboards/KeyboardsBase/ScribeFunctionality/CommandBar.swift +++ b/Keyboards/KeyboardsBase/ScribeFunctionality/CommandBar.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Class defining the bar into which commands are typed. */ @@ -8,148 +8,162 @@ import UIKit /// A custom UILabel used to house all the functionality of the command bar. class CommandBar: UILabel { - // MARK: Internal Properties - - /// Button that is shown on the trailing edge of the command bar. - let infoButton = UIButton(type: .system) - /// The tap handler triggered when tapping `trailingButton`. - var infoButtonTapHandler: (() -> Void)? - /// Determines whether or not the trailing `infoButton` should be shown on the command bar. - var isShowingInfoButton = false { - didSet { - infoButton.isHidden = !isShowingInfoButton - isUserInteractionEnabled = isShowingInfoButton + // MARK: Internal Properties + + /// Button that is shown on the trailing edge of the command bar. + let infoButton = UIButton(type: .system) + /// The tap handler triggered when tapping `trailingButton`. + var infoButtonTapHandler: (() -> Void)? + /// Determines whether or not the trailing `infoButton` should be shown on the command bar. + var isShowingInfoButton = false { + didSet { + infoButton.isHidden = !isShowingInfoButton + isUserInteractionEnabled = isShowingInfoButton + } } - } - // MARK: Initializer + // MARK: Initializer - override init(frame: CGRect) { - super.init(frame: frame) - } + override init(frame: CGRect) { + super.init(frame: frame) + } - required init?(coder: NSCoder) { - super.init(coder: coder) - } + required init?(coder: NSCoder) { + super.init(coder: coder) + } - /// Allows the class to be accessed from Keyboard.xib. - class func instanceFromNib() -> UIView { - let nibContents = UINib(nibName: "Keyboard", bundle: nil).instantiate(withOwner: nil, options: nil) - if let view = nibContents.first as? UIView { - return view - } else { - fatalError("Failed to instantiate view from nib.") + /// Allows the class to be accessed from Keyboard.xib. + class func instanceFromNib() -> UIView { + let nibContents = UINib(nibName: "Keyboard", bundle: nil).instantiate( + withOwner: nil, options: nil + ) + if let view = nibContents.first as? UIView { + return view + } else { + fatalError("Failed to instantiate view from nib.") + } } - } - var shadow: UIButton! + var shadow: UIButton! + + // MARK: Internal methods + + /// Sets up the command bar's color and text alignment. + func set() { + addInfoButton() + backgroundColor = commandBarColor + textAlignment = NSTextAlignment.left + if DeviceType.isPhone { + font = .systemFont(ofSize: frame.height * 0.4725) + } else if DeviceType.isPad { + font = .systemFont(ofSize: frame.height * 0.57375) + } + shadow.isUserInteractionEnabled = false + + if DeviceType.isPhone { + commandPromptSpacing = String(repeating: " ", count: 2) + } else if DeviceType.isPad { + commandPromptSpacing = String(repeating: " ", count: 5) + } + } - // MARK: Internal methods + /// Adds info button to Command Bar. + private func addInfoButton() { + infoButton.removeFromSuperview() + infoButton.isHidden = true + infoButton.setImage(UIImage(systemName: "info.circle.fill"), for: .normal) + infoButton.tintColor = commandBarPlaceholderColor + infoButton.addTarget(self, action: #selector(tappedButton), for: .touchUpInside) + infoButton.translatesAutoresizingMaskIntoConstraints = false + + addSubview(infoButton) + NSLayoutConstraint.activate([ + infoButton.heightAnchor.constraint(equalTo: heightAnchor), + infoButton.widthAnchor.constraint(equalTo: heightAnchor), + infoButton.centerYAnchor.constraint(equalTo: centerYAnchor), + infoButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8) + ]) + } - /// Sets up the command bar's color and text alignment. - func set() { - addInfoButton() - backgroundColor = commandBarColor - textAlignment = NSTextAlignment.left - if DeviceType.isPhone { - font = .systemFont(ofSize: frame.height * 0.4725) - } else if DeviceType.isPad { - font = .systemFont(ofSize: frame.height * 0.57375) + /// Triggered when tapping on `trailingButton`. + @objc func tappedButton() { + infoButtonTapHandler?() } - shadow.isUserInteractionEnabled = false - if DeviceType.isPhone { - commandPromptSpacing = String(repeating: " ", count: 2) - } else if DeviceType.isPad { - commandPromptSpacing = String(repeating: " ", count: 5) + /// Sets up the command bar's radius and shadow. + func setCornerRadiusAndShadow() { + clipsToBounds = true + layer.cornerRadius = commandKeyCornerRadius + layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner] + lineBreakMode = NSLineBreakMode.byWordWrapping + + shadow.backgroundColor = specialKeyColor + shadow.layer.cornerRadius = commandKeyCornerRadius + shadow.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner] + shadow.clipsToBounds = true + shadow.layer.masksToBounds = false + shadow.layer.shadowRadius = 0 + shadow.layer.shadowOpacity = 1.0 + shadow.layer.shadowOffset = CGSize(width: 0, height: 1) + shadow.layer.shadowColor = keyShadowColor } - } - - /// Adds info button to Command Bar. - private func addInfoButton() { - infoButton.removeFromSuperview() - infoButton.isHidden = true - infoButton.setImage(UIImage(systemName: "info.circle.fill"), for: .normal) - infoButton.tintColor = commandBarPlaceholderColor - infoButton.addTarget(self, action: #selector(tappedButton), for: .touchUpInside) - infoButton.translatesAutoresizingMaskIntoConstraints = false - - addSubview(infoButton) - NSLayoutConstraint.activate([ - infoButton.heightAnchor.constraint(equalTo: heightAnchor), - infoButton.widthAnchor.constraint(equalTo: heightAnchor), - infoButton.centerYAnchor.constraint(equalTo: centerYAnchor), - infoButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8) - ]) - } - - /// Triggered when tapping on `trailingButton`. - @objc func tappedButton() { - infoButtonTapHandler?() - } - - /// Sets up the command bar's radius and shadow. - func setCornerRadiusAndShadow() { - clipsToBounds = true - layer.cornerRadius = commandKeyCornerRadius - layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner] - lineBreakMode = NSLineBreakMode.byWordWrapping - - shadow.backgroundColor = specialKeyColor - shadow.layer.cornerRadius = commandKeyCornerRadius - shadow.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner] - shadow.clipsToBounds = true - shadow.layer.masksToBounds = false - shadow.layer.shadowRadius = 0 - shadow.layer.shadowOpacity = 1.0 - shadow.layer.shadowOffset = CGSize(width: 0, height: 1) - shadow.layer.shadowColor = keyShadowColor - } - - // Hides the command bar when command buttons will be showed. - func hide() { - backgroundColor = UIColor.clear - layer.borderColor = UIColor.clear.cgColor - text = "" - shadow.backgroundColor = UIColor.clear - } - - // Removes the placeholder text for a command and replaces it with just the prompt and cursor. - func conditionallyRemovePlaceholder() { - if text == translatePromptAndPlaceholder { - text = translatePromptAndCursor - } else if text == conjugatePromptAndPlaceholder { - text = conjugatePromptAndCursor - } else if text == pluralPromptAndPlaceholder { - text = pluralPromptAndCursor + + /// Hides the command bar when command buttons will be showed. + func hide() { + backgroundColor = UIColor.clear + layer.borderColor = UIColor.clear.cgColor + text = "" + shadow.backgroundColor = UIColor.clear } - } - - // Changes the command bar text to an attributed string with a placeholder if there is no entered characters. - func conditionallyAddPlaceholder() { - if [.translate, .conjugate, .plural].contains(commandState) { - // self.text check required as attributed text changes to text when shiftButtonState == .shift. - if commandState == .translate && (text == translatePromptAndCursor || text == translatePromptAndPlaceholder) { - attributedText = colorizePrompt(for: translatePromptAndPlaceholder) - } else if commandState == .conjugate && (text == conjugatePromptAndCursor || text == conjugatePromptAndPlaceholder) { - attributedText = colorizePrompt(for: conjugatePromptAndPlaceholder) - } else if commandState == .plural && (text == pluralPromptAndCursor || text == pluralPromptAndPlaceholder) { - attributedText = colorizePrompt(for: pluralPromptAndPlaceholder) - } + + /// Removes the placeholder text for a command and replaces it with just the prompt and cursor. + func conditionallyRemovePlaceholder() { + if text == translatePromptAndPlaceholder { + text = translatePromptAndCursor + } else if text == conjugatePromptAndPlaceholder { + text = conjugatePromptAndCursor + } else if text == pluralPromptAndPlaceholder { + text = pluralPromptAndCursor + } } - } - - // Changes the color of the placeholder text to indicate that it is temporary. - func colorizePrompt(for prompt: String) -> NSMutableAttributedString { - let colorPrompt = NSMutableAttributedString(string: prompt) - if commandState == .translate { - colorPrompt.setColorForText(textForAttribute: translatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - } else if commandState == .conjugate { - colorPrompt.setColorForText(textForAttribute: conjugatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - } else if commandState == .plural { - colorPrompt.setColorForText(textForAttribute: pluralPlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) + + /// Changes the command bar text to an attributed string with a placeholder if there is no entered characters. + func conditionallyAddPlaceholder() { + if [.translate, .conjugate, .plural].contains(commandState) { + // self.text check required as attributed text changes to text when shiftButtonState == .shift. + if commandState == .translate, + text == translatePromptAndCursor || text == translatePromptAndPlaceholder { + attributedText = colorizePrompt(for: translatePromptAndPlaceholder) + } else if commandState == .conjugate, + text == conjugatePromptAndCursor || text == conjugatePromptAndPlaceholder { + attributedText = colorizePrompt(for: conjugatePromptAndPlaceholder) + } else if commandState == .plural, + text == pluralPromptAndCursor || text == pluralPromptAndPlaceholder { + attributedText = colorizePrompt(for: pluralPromptAndPlaceholder) + } + } } - return colorPrompt - } + /// Changes the color of the placeholder text to indicate that it is temporary. + func colorizePrompt(for prompt: String) -> NSMutableAttributedString { + let colorPrompt = NSMutableAttributedString(string: prompt) + if commandState == .translate { + colorPrompt.setColorForText( + textForAttribute: translatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + } else if commandState == .conjugate { + colorPrompt.setColorForText( + textForAttribute: conjugatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + } else if commandState == .plural { + colorPrompt.setColorForText( + textForAttribute: pluralPlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + } + + return colorPrompt + } } diff --git a/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift b/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift index 82a3f4d2..236fd9e5 100644 --- a/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift +++ b/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Variables associated with Scribe commands. */ @@ -15,10 +15,10 @@ var autoAction2Visible = true /// States of the emoji display corresponding to the number to show. enum EmojisToShow { - case zero - case one - case two - case three + case zero + case one + case two + case three } var emojisToShow: EmojisToShow = .zero @@ -111,11 +111,11 @@ var conjugatePromptAndColorPlaceholder = NSMutableAttributedString() /// What the view of the conjugation display to display to the user. enum FormsDisplayDimensions { - case view3x2 - case view3x1 - case view2x2 - case view1x2 - case view1x1 + case view3x2 + case view3x1 + case view2x2 + case view1x2 + case view1x1 } var formsDisplayDimensions: FormsDisplayDimensions = .view3x2 @@ -145,22 +145,22 @@ var formRight = "" var formSingle = "" var formLabelsDict = [ - "FPS": "", - "SPS": "", - "TPS": "", - "FPP": "", - "SPP": "", - "TPP": "", - "Top": "", - "Middle": "", - "Bottom": "", - "TL": "", - "TR": "", - "BL": "", - "BR": "", - "Left": "", - "Right": "", - "Single": "" + "FPS": "", + "SPS": "", + "TPS": "", + "FPP": "", + "SPP": "", + "TPP": "", + "Top": "", + "Middle": "", + "Bottom": "", + "TL": "", + "TR": "", + "BL": "", + "BR": "", + "Left": "", + "Right": "", + "Single": "" ] var verbToConjugate = "" diff --git a/Keyboards/KeyboardsBase/ScribeFunctionality/Conjugate.swift b/Keyboards/KeyboardsBase/ScribeFunctionality/Conjugate.swift index c7e93f76..ce16003a 100644 --- a/Keyboards/KeyboardsBase/ScribeFunctionality/Conjugate.swift +++ b/Keyboards/KeyboardsBase/ScribeFunctionality/Conjugate.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/// Functions and elements that control the conjugation command. +// Functions and elements that control the conjugation command. import UIKit @@ -9,19 +9,21 @@ import UIKit /// - Parameters /// - commandBar: the command bar into which an input was entered. func triggerVerbConjugation(commandBar: UILabel) -> Bool { - // Cancel via a return press. - if let commandBarText = commandBar.text, - commandBarText == conjugatePromptAndCursor || commandBarText == conjugatePromptAndCursor { - return false - } - - if let commandBarText = commandBar.text { - let startIndex = commandBarText.index(commandBarText.startIndex, offsetBy: conjugatePrompt.count) - let endIndex = commandBarText.index(commandBarText.endIndex, offsetBy: -1) - verbToConjugate = String(commandBarText[startIndex ..< endIndex]) - } - - return isVerbInConjugationTable(queriedVerbToConjugate: verbToConjugate) + // Cancel via a return press. + if let commandBarText = commandBar.text, + commandBarText == conjugatePromptAndCursor || commandBarText == conjugatePromptAndCursor { + return false + } + + if let commandBarText = commandBar.text { + let startIndex = commandBarText.index( + commandBarText.startIndex, offsetBy: conjugatePrompt.count + ) + let endIndex = commandBarText.index(commandBarText.endIndex, offsetBy: -1) + verbToConjugate = String(commandBarText[startIndex ..< endIndex]) + } + + return isVerbInConjugationTable(queriedVerbToConjugate: verbToConjugate) } /// Checks if the verb to conjugate exists in the conjugation table. @@ -29,15 +31,15 @@ func triggerVerbConjugation(commandBar: UILabel) -> Bool { /// - queriedVerbToConjugate: The verb to check for existence. /// - Returns: True if the verb exists in the conjugation table, false otherwise. func isVerbInConjugationTable(queriedVerbToConjugate: String) -> Bool { - verbToConjugate = String(queriedVerbToConjugate.trailingSpacesTrimmed) + verbToConjugate = String(queriedVerbToConjugate.trailingSpacesTrimmed) - let firstLetter = verbToConjugate.substring(toIdx: 1) - inputWordIsCapitalized = firstLetter.isUppercase - verbToConjugate = verbToConjugate.lowercased() + let firstLetter = verbToConjugate.substring(toIdx: 1) + inputWordIsCapitalized = firstLetter.isUppercase + verbToConjugate = verbToConjugate.lowercased() - // Try to query any conjugation form to verify verb exists. - let columnName = (controllerLanguage == "Swedish") ? "verb" : "infinitive" - let results = LanguageDBManager.shared.queryVerb(of: verbToConjugate, with: [columnName]) + // Try to query any conjugation form to verify verb exists. + let columnName = (controllerLanguage == "Swedish") ? "verb" : "infinitive" + let results = LanguageDBManager.shared.queryVerb(of: verbToConjugate, with: [columnName]) - return !results.isEmpty && !results[0].isEmpty + return !results.isEmpty && !results[0].isEmpty } diff --git a/Keyboards/KeyboardsBase/ScribeFunctionality/Plural.swift b/Keyboards/KeyboardsBase/ScribeFunctionality/Plural.swift index ad66cf80..f1dd56ed 100644 --- a/Keyboards/KeyboardsBase/ScribeFunctionality/Plural.swift +++ b/Keyboards/KeyboardsBase/ScribeFunctionality/Plural.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Functions that control the plural command. */ @@ -11,51 +11,54 @@ import UIKit /// - Parameters /// - commandBar: the command bar into which an input was entered. func queryPlural(commandBar: UILabel) { - // Cancel via a return press. - if let commandBarText = commandBar.text, - commandBarText == pluralPromptAndCursor { - return - } - - var noun = "" - if let commandBarText = commandBar.text { - let startIndex = commandBarText.index(commandBarText.startIndex, offsetBy: pluralPrompt.count) - let endIndex = commandBarText.index(before: commandBarText.endIndex) - noun = String(commandBarText[startIndex ..< endIndex]) - } - - queryPluralNoun(queriedNoun: noun) + // Cancel via a return press. + if let commandBarText = commandBar.text, + commandBarText == pluralPromptAndCursor { + return + } + + var noun = "" + if let commandBarText = commandBar.text { + let startIndex = commandBarText.index( + commandBarText.startIndex, offsetBy: pluralPrompt.count + ) + let endIndex = commandBarText.index(before: commandBarText.endIndex) + noun = String(commandBarText[startIndex ..< endIndex]) + } + + queryPluralNoun(queriedNoun: noun) } func queryPluralNoun(queriedNoun: String) { - var noun = String(queriedNoun.trailingSpacesTrimmed) - - // Check to see if the input was uppercase to return an uppercase plural. - inputWordIsCapitalized = false - if !languagesWithCapitalizedNouns.contains(controllerLanguage) { - inputWordIsCapitalized = noun.substring(toIdx: 1).isUppercase - noun = noun.lowercased() - } - - // Check if the word is already plural (always use lowercase for Set lookup). - let lowercaseNoun = noun.lowercased() - let isAlreadyPlural = pluralWords?.contains(lowercaseNoun) == true - - if isAlreadyPlural { - wordToReturn = noun // Use original capitalization. - commandState = .alreadyPlural - } else { - let result = LanguageDBManager.shared.queryNounPlural(of: noun) - guard !result.isEmpty, !result[0].isEmpty else { - commandState = .invalid - return + var noun = String(queriedNoun.trailingSpacesTrimmed) + + // Check to see if the input was uppercase to return an uppercase plural. + inputWordIsCapitalized = false + if !languagesWithCapitalizedNouns.contains(controllerLanguage) { + inputWordIsCapitalized = noun.substring(toIdx: 1).isUppercase + noun = noun.lowercased() + } + + // Check if the word is already plural (always use lowercase for Set lookup). + let lowercaseNoun = noun.lowercased() + let isAlreadyPlural = pluralWords?.contains(lowercaseNoun) == true + + if isAlreadyPlural { + wordToReturn = noun // Use original capitalization. + commandState = .alreadyPlural + } else { + let result = LanguageDBManager.shared.queryNounPlural(of: noun) + guard !result.isEmpty, !result[0].isEmpty + else { + commandState = .invalid + return + } + wordToReturn = result[0] + } + + if inputWordIsCapitalized { + proxy.insertText(wordToReturn.capitalized + getOptionalSpace()) + } else { + proxy.insertText(wordToReturn + getOptionalSpace()) } - wordToReturn = result[0] - } - - if inputWordIsCapitalized { - proxy.insertText(wordToReturn.capitalized + getOptionalSpace()) - } else { - proxy.insertText(wordToReturn + getOptionalSpace()) - } } diff --git a/Keyboards/KeyboardsBase/ScribeFunctionality/ScribeKey.swift b/Keyboards/KeyboardsBase/ScribeFunctionality/ScribeKey.swift index d67a60e2..35aa2500 100644 --- a/Keyboards/KeyboardsBase/ScribeFunctionality/ScribeKey.swift +++ b/Keyboards/KeyboardsBase/ScribeFunctionality/ScribeKey.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Class defining the Scribe key that is used to access keyboard commands. */ @@ -8,92 +8,101 @@ import UIKit /// The main UI element that allows for accessing other commands and triggering annotation. class ScribeKey: UIButton { - override init(frame: CGRect) { - super.init(frame: frame) - } + override init(frame: CGRect) { + super.init(frame: frame) + } - required init?(coder: NSCoder) { - super.init(coder: coder) - } + required init?(coder: NSCoder) { + super.init(coder: coder) + } - /// Allows the class to be accessed from Keyboard.xib. - class func instanceFromNib() -> UIView { - guard let nibContents = UINib(nibName: "Keyboard", bundle: nil).instantiate(withOwner: nil, options: nil).first as? UIView else { - fatalError("Failed to instantiate view from nib.") + /// Allows the class to be accessed from Keyboard.xib. + class func instanceFromNib() -> UIView { + guard + let nibContents = UINib(nibName: "Keyboard", bundle: nil).instantiate( + withOwner: nil, options: nil + ).first as? UIView + else { + fatalError("Failed to instantiate view from nib.") + } + return nibContents } - return nibContents - } - /// Converts the Scribe key to an escape key to return to the base keyboard view. - func toEscape() { - setTitle("", for: .normal) - var selectKeyboardIconConfig = UIImage.SymbolConfiguration( - pointSize: frame.height * 0.515, - weight: .light, - scale: .medium - ) - if DeviceType.isPad { - selectKeyboardIconConfig = UIImage.SymbolConfiguration( - pointSize: frame.height * 0.6, - weight: .light, - scale: .medium - ) + /// Converts the Scribe key to an escape key to return to the base keyboard view. + func toEscape() { + setTitle("", for: .normal) + var selectKeyboardIconConfig = UIImage.SymbolConfiguration( + pointSize: frame.height * 0.515, + weight: .light, + scale: .medium + ) + if DeviceType.isPad { + selectKeyboardIconConfig = UIImage.SymbolConfiguration( + pointSize: frame.height * 0.6, + weight: .light, + scale: .medium + ) + } + setImage( + UIImage(systemName: "xmark", withConfiguration: selectKeyboardIconConfig), for: .normal + ) + tintColor = keyCharColor + layer.cornerRadius = commandKeyCornerRadius + layer.masksToBounds = true } - setImage(UIImage(systemName: "xmark", withConfiguration: selectKeyboardIconConfig), for: .normal) - tintColor = keyCharColor - layer.cornerRadius = commandKeyCornerRadius - layer.masksToBounds = true - } - /// Assigns the icon and sets up the Scribe key. - func set() { - setImage(scribeKeyIcon, for: .normal) - setBtn(btn: self, color: commandKeyColor, name: "Scribe", canBeCapitalized: false, isSpecial: false) - contentMode = .center - imageView?.contentMode = .scaleAspectFit - shadow.isUserInteractionEnabled = false - } + /// Assigns the icon and sets up the Scribe key. + func set() { + setImage(scribeKeyIcon, for: .normal) + setBtn( + btn: self, color: commandKeyColor, name: "Scribe", canBeCapitalized: false, + isSpecial: false + ) + contentMode = .center + imageView?.contentMode = .scaleAspectFit + shadow.isUserInteractionEnabled = false + } - /// Sets the corner radius for just the left side of the Scribe key. - func setPartialCornerRadius() { - layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] - } + /// Sets the corner radius for just the left side of the Scribe key. + func setPartialCornerRadius() { + layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] + } - /// Sets the corner radius for all sides of the Scribe key. - func setFullCornerRadius() { - layer.borderColor = UIColor.clear.cgColor // border is set by the shadow - layer.maskedCorners = [ - .layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMaxXMaxYCorner - ] - } + /// Sets the corner radius for all sides of the Scribe key. + func setFullCornerRadius() { + layer.borderColor = UIColor.clear.cgColor // border is set by the shadow + layer.maskedCorners = [ + .layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMaxXMaxYCorner + ] + } - var shadow: UIButton! + var shadow: UIButton! - /// Sets the shadow of the Scribe key. - func setPartialShadow() { - shadow.backgroundColor = specialKeyColor - shadow.layer.cornerRadius = commandKeyCornerRadius - shadow.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] - shadow.clipsToBounds = true - shadow.layer.masksToBounds = false - shadow.layer.shadowRadius = 0 - shadow.layer.shadowOpacity = 1.0 - shadow.layer.shadowOffset = CGSize(width: 0, height: 1) - shadow.layer.shadowColor = keyShadowColor - } + /// Sets the shadow of the Scribe key. + func setPartialShadow() { + shadow.backgroundColor = specialKeyColor + shadow.layer.cornerRadius = commandKeyCornerRadius + shadow.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner] + shadow.clipsToBounds = true + shadow.layer.masksToBounds = false + shadow.layer.shadowRadius = 0 + shadow.layer.shadowOpacity = 1.0 + shadow.layer.shadowOffset = CGSize(width: 0, height: 1) + shadow.layer.shadowColor = keyShadowColor + } - /// Sets the shadow of the Scribe key when it's an escape key. - func setFullShadow() { - shadow.backgroundColor = specialKeyColor - shadow.layer.cornerRadius = commandKeyCornerRadius - shadow.layer.maskedCorners = [ - .layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMaxXMaxYCorner - ] - shadow.clipsToBounds = true - shadow.layer.masksToBounds = false - shadow.layer.shadowRadius = 0 - shadow.layer.shadowOpacity = 1.0 - shadow.layer.shadowOffset = CGSize(width: 0, height: 1) - shadow.layer.shadowColor = keyShadowColor - } + /// Sets the shadow of the Scribe key when it's an escape key. + func setFullShadow() { + shadow.backgroundColor = specialKeyColor + shadow.layer.cornerRadius = commandKeyCornerRadius + shadow.layer.maskedCorners = [ + .layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMaxXMaxYCorner + ] + shadow.clipsToBounds = true + shadow.layer.masksToBounds = false + shadow.layer.shadowRadius = 0 + shadow.layer.shadowOpacity = 1.0 + shadow.layer.shadowOffset = CGSize(width: 0, height: 1) + shadow.layer.shadowColor = keyShadowColor + } } diff --git a/Keyboards/KeyboardsBase/ScribeFunctionality/Translate.swift b/Keyboards/KeyboardsBase/ScribeFunctionality/Translate.swift index 9894a60e..8672e868 100644 --- a/Keyboards/KeyboardsBase/ScribeFunctionality/Translate.swift +++ b/Keyboards/KeyboardsBase/ScribeFunctionality/Translate.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Functions that control the translate command. */ @@ -11,40 +11,45 @@ import UIKit /// - Parameters /// - commandBar: the command bar into which an input was entered. func queryTranslation(commandBar: UILabel) { - // Cancel via a return press. - if let commandBarText = commandBar.text, - commandBarText == translatePromptAndCursor || commandBarText == translatePromptAndPlaceholder { - return - } - - if let commandBarText = commandBar.text { - let startIndex = commandBarText.index(commandBarText.startIndex, offsetBy: translatePrompt.count) - let endIndex = commandBarText.index(commandBarText.endIndex, offsetBy: -1) - wordToTranslate = String(commandBarText[startIndex ..< endIndex]) - } - - queryWordToTranslate(queriedWordToTranslate: wordToTranslate) + // Cancel via a return press. + if let commandBarText = commandBar.text, + commandBarText == translatePromptAndCursor + || commandBarText == translatePromptAndPlaceholder { + return + } + + if let commandBarText = commandBar.text { + let startIndex = commandBarText.index( + commandBarText.startIndex, offsetBy: translatePrompt.count + ) + let endIndex = commandBarText.index(commandBarText.endIndex, offsetBy: -1) + wordToTranslate = String(commandBarText[startIndex ..< endIndex]) + } + + queryWordToTranslate(queriedWordToTranslate: wordToTranslate) } func queryWordToTranslate(queriedWordToTranslate: String) { - wordToTranslate = String(queriedWordToTranslate.trailingSpacesTrimmed) + wordToTranslate = String(queriedWordToTranslate.trailingSpacesTrimmed) - // Check to see if the input was uppercase to return an uppercase conjugation. - inputWordIsCapitalized = wordToTranslate.substring(toIdx: 1).isUppercase + // Check to see if the input was uppercase to return an uppercase conjugation. + inputWordIsCapitalized = wordToTranslate.substring(toIdx: 1).isUppercase - wordToReturn = LanguageDBManager.translations.queryTranslation(of: wordToTranslate.lowercased())[0] + wordToReturn = + LanguageDBManager.translations.queryTranslation(of: wordToTranslate.lowercased())[0] - if wordToReturn.isEmpty { - wordToReturn = LanguageDBManager.translations.queryTranslation(of: wordToTranslate)[0] - guard !wordToReturn.isEmpty else { - commandState = .invalid - return + if wordToReturn.isEmpty { + wordToReturn = LanguageDBManager.translations.queryTranslation(of: wordToTranslate)[0] + guard !wordToReturn.isEmpty + else { + commandState = .invalid + return + } } - } - if inputWordIsCapitalized { - proxy.insertText(wordToReturn.capitalized + getOptionalSpace()) - } else { - proxy.insertText(wordToReturn + getOptionalSpace()) - } + if inputWordIsCapitalized { + proxy.insertText(wordToReturn.capitalized + getOptionalSpace()) + } else { + proxy.insertText(wordToReturn + getOptionalSpace()) + } } diff --git a/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift b/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift index a7f1ad73..7fa89e3f 100644 --- a/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift +++ b/Keyboards/KeyboardsBase/ToolTip/Model/InformationToolTipData.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Data used for tooltips. */ @@ -8,58 +8,31 @@ import Foundation import UIKit enum InformationToolTipData { - static let wikiDataExplanation = NSMutableAttributedString( - string: invalidCommandTextWikidata1, - attributes: [ - NSAttributedString.Key.font: UIFont.systemFont( - ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.5 - ) - ] - ) - - static let wikiDataContationOrigin = NSMutableAttributedString( - string: invalidCommandTextWikidata2, - attributes: [ - NSAttributedString.Key.font: UIFont.systemFont( - ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.5 - ) - ] - ) - static let howToContribute = NSMutableAttributedString( - string: invalidCommandTextWikidata3, - attributes: [ - NSAttributedString.Key.font: UIFont.systemFont( - ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.5 - ) - ] - ) - - static let wiktionaryExplanation = NSMutableAttributedString( - string: invalidCommandTextWiktionary1, - attributes: [ - NSAttributedString.Key.font: UIFont.systemFont( - ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.5 - ) - ] - ) - - static let wiktionaryTranslationOrigin = NSMutableAttributedString( - string: invalidCommandTextWiktionary2, - attributes: [ - NSAttributedString.Key.font: UIFont.systemFont( - ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.5 - ) - ] - ) - - static let howToContributeWiktionary = NSMutableAttributedString( - string: invalidCommandTextWiktionary3, - attributes: [ - NSAttributedString.Key.font: UIFont.systemFont( - ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.75 - ) - ] - ) + static let wikiDataExplanation = NSMutableAttributedString( + string: invalidCommandTextWikidata1, + attributes: [ + NSAttributedString.Key.font: UIFont.systemFont( + ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.5 + ) + ] + ) + + static let wikiDataContationOrigin = NSMutableAttributedString( + string: invalidCommandTextWikidata2, + attributes: [ + NSAttributedString.Key.font: UIFont.systemFont( + ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.5 + ) + ] + ) + static let howToContribute = NSMutableAttributedString( + string: invalidCommandTextWikidata3, + attributes: [ + NSAttributedString.Key.font: UIFont.systemFont( + ofSize: DeviceType.isPhone ? letterKeyWidth / 2 : letterKeyWidth / 2.5 + ) + ] + ) static func getContent() -> [NSMutableAttributedString] { [wikiDataExplanation, wikiDataContationOrigin, howToContribute] diff --git a/Keyboards/KeyboardsBase/Utilities.swift b/Keyboards/KeyboardsBase/Utilities.swift index b49dda37..472773cb 100644 --- a/Keyboards/KeyboardsBase/Utilities.swift +++ b/Keyboards/KeyboardsBase/Utilities.swift @@ -1,14 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Simple utility functions for data extraction and language management. */ /// Checks if an extra space is needed in the text field when generated text is inserted func getOptionalSpace() -> String { - if proxy.documentContextAfterInput?.first == " " { - return "" - } else { - return " " - } + if proxy.documentContextAfterInput?.first == " " { + return "" + } else { + return " " + } } diff --git a/Keyboards/LanguageKeyboards/Danish/DAInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Danish/DAInterfaceVariables.swift index 567276f2..c9747464 100644 --- a/Keyboards/LanguageKeyboards/Danish/DAInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Danish/DAInterfaceVariables.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Constants and functions to load the Danish Scribe keyboard. */ @@ -9,129 +9,142 @@ import UIKit // MARK: Constants public enum DAKeyboardConstants { - static let defaultCurrencyKey = "kr" - static let currencyKeys = ["kr", "€", "$", "£", "¥"] - - // Alternate key vars. - static let keysWithAlternates = ["a", "e", "i", "o", "u", "y", "æ", "ø", "d", "l", "n", "s"] - static let keysWithAlternatesLeft = ["a", "e", "y", "d", "s"] - static let keysWithAlternatesRight = ["i", "o", "u", "æ", "ø", "l", "n"] - - static let aAlternateKeys = ["á", "ä", "à", "â", "ã", "ā"] - static let eAlternateKeys = ["é", "ë"] - static let iAlternateKeys = ["ï", "í"] - static let oAlternateKeys = ["ö", "ō", "œ", "õ", "ø", "ò", "ô", "ó"] - static let uAlternateKeys = ["ū", "ù", "û", "ü", "ú"] - static let yAlternateKeys = ["ÿ"] - static let æAlternateKeys = ["ä"] - static let øAlternateKeys = ["ö"] - static let dAlternateKeys = ["ð"] - static let lAlternateKeys = ["ł"] - static let nAlternateKeys = ["ń", "ñ"] - static let sAlternateKeys = ["ß", "ś", "š"] + static let defaultCurrencyKey = "kr" + static let currencyKeys = ["kr", "€", "$", "£", "¥"] + + // Alternate key vars. + static let keysWithAlternates = ["a", "e", "i", "o", "u", "y", "æ", "ø", "d", "l", "n", "s"] + static let keysWithAlternatesLeft = ["a", "e", "y", "d", "s"] + static let keysWithAlternatesRight = ["i", "o", "u", "æ", "ø", "l", "n"] + + static let aAlternateKeys = ["á", "ä", "à", "â", "ã", "ā"] + static let eAlternateKeys = ["é", "ë"] + static let iAlternateKeys = ["ï", "í"] + static let oAlternateKeys = ["ö", "ō", "œ", "õ", "ø", "ò", "ô", "ó"] + static let uAlternateKeys = ["ū", "ù", "û", "ü", "ú"] + static let yAlternateKeys = ["ÿ"] + static let æAlternateKeys = ["ä"] + static let øAlternateKeys = ["ö"] + static let dAlternateKeys = ["ð"] + static let lAlternateKeys = ["ł"] + static let nAlternateKeys = ["ń", "ñ"] + static let sAlternateKeys = ["ß", "ś", "š"] } struct DAKeyboardProvider: KeyboardProviderProtocol { - // MARK: iPhone Layouts - - static func genPhoneLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "å"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "æ", "ø"]) - .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "delete"]) - .addRow(["123", "selectKeyboard", "space", "return"]) - .build() - } - - static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) - .addRow(["-", "/", ":", ";", "(", ")", "kr", "&", "@", "\""]) - .addRow( ["#+=", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - .replaceKey(row: 1, column: 6, to: currencyKey) - .build() - } - - static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) - .addRow(["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "·"]) - .addRow(["123", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 6, to: currencyKeys[0]) - .replaceKey(row: 1, column: 7, to: currencyKeys[1]) - .replaceKey(row: 1, column: 8, to: currencyKeys[2]) - .build() + // MARK: iPhone Layouts + + static func genPhoneLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "å"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "æ", "ø"]) + .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "delete"]) + .addRow(["123", "selectKeyboard", "space", "return"]) + .build() } - } - - // MARK: iPad Layouts - - static func genPadLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) - .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "æ", "ø", "delete"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä", "return"]) - .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "`", "delete"]) - .addRow(["@", "#", "kr", "&", "*", "(", ")", "'", "\"", "+", "·", "return"]) - .addRow(["#+=", "%", "_", "-", "=", "/", ";", ":", ",", ".", "?", "#+="]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .replaceKey(row: 1, column: 2, to: currencyKey) - .build() - } - - static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "'", "delete"]) - .addRow(["€", "$", "£", "^", "[", "]", "{", "}", "―", "ᵒ", "...", "return"]) - .addRow(["123", "§", "|", "~", "≠", "≈", "\\", "<", ">", "!", "?", "123"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 0, to: currencyKeys[0]) - .replaceKey(row: 1, column: 1, to: currencyKeys[1]) - .replaceKey(row: 1, column: 2, to: currencyKeys[2]) - .build() + + static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) + .addRow(["-", "/", ":", ";", "(", ")", "kr", "&", "@", "\""]) + .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + .replaceKey(row: 1, column: 6, to: currencyKey) + .build() + } + + static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) + .addRow(["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "·"]) + .addRow(["123", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 6, to: currencyKeys[0]) + .replaceKey(row: 1, column: 7, to: currencyKeys[1]) + .replaceKey(row: 1, column: 8, to: currencyKeys[2]) + .build() + } + } + + // MARK: iPad Layouts + + static func genPadLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) + .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "æ", "ø", "delete"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä", "return"]) + .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "`", "delete"]) + .addRow(["@", "#", "kr", "&", "*", "(", ")", "'", "\"", "+", "·", "return"]) + .addRow(["#+=", "%", "_", "-", "=", "/", ";", ":", ",", ".", "?", "#+="]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .replaceKey(row: 1, column: 2, to: currencyKey) + .build() + } + + static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "'", "delete"]) + .addRow(["€", "$", "£", "^", "[", "]", "{", "}", "―", "ᵒ", "...", "return"]) + .addRow(["123", "§", "|", "~", "≠", "≈", "\\", "<", ">", "!", "?", "123"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 0, to: currencyKeys[0]) + .replaceKey(row: 1, column: 1, to: currencyKeys[1]) + .replaceKey(row: 1, column: 2, to: currencyKeys[2]) + .build() + } + } + + // MARK: Expanded iPad Layouts + + static func genPadExpandedLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["kr", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "+", "´", "delete"]) + .addRow([ + SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "å", "@", "¨" + ]) + .addRow([ + SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "æ", "ø", "'", + "return" + ]) + .addRow(["shift", "*", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadExpandedSymbolKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) + .addRow([ + SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "\"", "|", + "—" + ]) + .addRow([ + SpecialKeys.capsLock, "°", "/", ":", ";", "(", ")", "$", "&", "@", "£", "¥", "~", + "return" + ]) + .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "_", "€", ",", ".", "-", "shift"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .build() } - } - - // MARK: Expanded iPad Layouts - - static func genPadExpandedLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["kr", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "+", "´", "delete"]) - .addRow([SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "å", "@", "¨"]) - .addRow([SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "æ", "ø", "'", "return"]) - .addRow(["shift", "*", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadExpandedSymbolKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) - .addRow([SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "\"", "|", "—"]) - .addRow([SpecialKeys.capsLock, "°", "/", ":", ";", "(", ")", "$", "&", "@", "£", "¥", "~", "return"]) - .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "_", "€", ",", ".", "-", "shift"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .build() - } } // MARK: Get Keys diff --git a/Keyboards/LanguageKeyboards/Danish/DAKeyboardViewController.swift b/Keyboards/LanguageKeyboards/Danish/DAKeyboardViewController.swift index ec6d39aa..d2b2525f 100644 --- a/Keyboards/LanguageKeyboards/Danish/DAKeyboardViewController.swift +++ b/Keyboards/LanguageKeyboards/Danish/DAKeyboardViewController.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Class for the Danish Scribe keyboard. */ diff --git a/Keyboards/LanguageKeyboards/English/ENInterfaceVariables.swift b/Keyboards/LanguageKeyboards/English/ENInterfaceVariables.swift index 49114823..28f5f4fc 100644 --- a/Keyboards/LanguageKeyboards/English/ENInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/English/ENInterfaceVariables.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Constants and functions to load the English Scribe keyboard. */ @@ -9,125 +9,138 @@ import UIKit // MARK: Constants public enum ENKeyboardConstants { - static let defaultCurrencyKey = "$" - static let currencyKeys = ["$", "€", "£", "¥"] - - // Alternate key vars. - static let keysWithAlternates = ["a", "e", "i", "o", "u", "y", "c", "l", "n", "s", "z"] - static let keysWithAlternatesLeft = ["a", "e", "y", "c", "s", "z"] - static let keysWithAlternatesRight = ["i", "o", "u", "l", "n"] - - static let aAlternateKeys = ["à", "á", "â", "ä", "æ", "ã", "å", "ā"] - static let eAlternateKeys = ["è", "é", "ê", "ë", "ē", "ė", "ę"] - static let iAlternateKeys = ["ì", "į", "ī", "í", "ï", "î"] - static let oAlternateKeys = ["õ", "ō", "ø", "œ", "ó", "ò", "ö", "ô"] - static let uAlternateKeys = ["ū", "ú", "ù", "ü", "û"] - static let cAlternateKeys = ["ç", "ć", "č"] - static let lAlternateKeys = ["ł"] - static let nAlternateKeys = ["ń", "ñ"] - static let sAlternateKeys = ["ś", "š"] - static let zAlternateKeys = ["ž", "ź", "ż"] + static let defaultCurrencyKey = "$" + static let currencyKeys = ["$", "€", "£", "¥"] + + // Alternate key vars. + static let keysWithAlternates = ["a", "e", "i", "o", "u", "y", "c", "l", "n", "s", "z"] + static let keysWithAlternatesLeft = ["a", "e", "y", "c", "s", "z"] + static let keysWithAlternatesRight = ["i", "o", "u", "l", "n"] + + static let aAlternateKeys = ["à", "á", "â", "ä", "æ", "ã", "å", "ā"] + static let eAlternateKeys = ["è", "é", "ê", "ë", "ē", "ė", "ę"] + static let iAlternateKeys = ["ì", "į", "ī", "í", "ï", "î"] + static let oAlternateKeys = ["õ", "ō", "ø", "œ", "ó", "ò", "ö", "ô"] + static let uAlternateKeys = ["ū", "ú", "ù", "ü", "û"] + static let cAlternateKeys = ["ç", "ć", "č"] + static let lAlternateKeys = ["ł"] + static let nAlternateKeys = ["ń", "ñ"] + static let sAlternateKeys = ["ś", "š"] + static let zAlternateKeys = ["ž", "ź", "ż"] } struct ENKeyboardProvider: KeyboardProviderProtocol { - // MARK: iPhone Layouts - - static func genPhoneLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l"]) - .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "delete"]) - .addRow(["123", "selectKeyboard", "space", "return"]) - .build() - } - - static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) - .addRow(["-", "/", ":", ";", "(", ")", "$", "&", "@", "\""]) - .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - .replaceKey(row: 1, column: 6, to: currencyKey) - .build() - } - - static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) - .addRow(["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "·"]) - .addRow(["123", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 6, to: currencyKeys[0]) - .replaceKey(row: 1, column: 7, to: currencyKeys[1]) - .replaceKey(row: 1, column: 8, to: currencyKeys[2]) - .build() + // MARK: iPhone Layouts + + static func genPhoneLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l"]) + .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "delete"]) + .addRow(["123", "selectKeyboard", "space", "return"]) + .build() } - } - - // MARK: iPad Layouts - - static func genPadLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) - .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "delete"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "return"]) - .addRow(["shift", "w", "x", "c", "v", "b", "n", "m", ",", ".", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) - .addRow(["@", "#", "$", "&", "*", "(", ")", "'", "\"", "return"]) - .addRow(["#+=", "%", "_", "+", "=", "/", ";", ":", ",", ".", "#+="]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .replaceKey(row: 1, column: 2, to: currencyKey) - .build() - } - - static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) - .addRow(["€", "£", "¥", "_", "^", "[", "]", "{", "}", "return"]) - .addRow(["123", "§", "|", "~", "...", "\\", "<", ">", "!", "?", "123"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 0, to: currencyKeys[0]) - .replaceKey(row: 1, column: 1, to: currencyKeys[1]) - .replaceKey(row: 1, column: 2, to: currencyKeys[2]) - .build() + + static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) + .addRow(["-", "/", ":", ";", "(", ")", "$", "&", "@", "\""]) + .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + .replaceKey(row: 1, column: 6, to: currencyKey) + .build() + } + + static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) + .addRow(["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "·"]) + .addRow(["123", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 6, to: currencyKeys[0]) + .replaceKey(row: 1, column: 7, to: currencyKeys[1]) + .replaceKey(row: 1, column: 8, to: currencyKeys[2]) + .build() + } + } + + // MARK: iPad Layouts + + static func genPadLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) + .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "delete"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "return"]) + .addRow(["shift", "w", "x", "c", "v", "b", "n", "m", ",", ".", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) + .addRow(["@", "#", "$", "&", "*", "(", ")", "'", "\"", "return"]) + .addRow(["#+=", "%", "_", "+", "=", "/", ";", ":", ",", ".", "#+="]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .replaceKey(row: 1, column: 2, to: currencyKey) + .build() + } + + static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) + .addRow(["€", "£", "¥", "_", "^", "[", "]", "{", "}", "return"]) + .addRow(["123", "§", "|", "~", "...", "\\", "<", ">", "!", "?", "123"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 0, to: currencyKeys[0]) + .replaceKey(row: 1, column: 1, to: currencyKeys[1]) + .replaceKey(row: 1, column: 2, to: currencyKeys[2]) + .build() + } + } + + static func genPadExpandedLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["~", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "delete"]) + .addRow([ + SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]", + "\\" + ]) + .addRow([ + SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", ":", ";", "'", + "return" + ]) + .addRow(["shift", "-", "z", "x", "c", "v", "b", "n", "m", ",", ".", "/", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadExpandedSymbolKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) + .addRow([ + SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "—", "~", "°" + ]) + .addRow([ + SpecialKeys.capsLock, "-", "\\", ":", ";", "(", ")", "&", "@", "$", "£", "¥", "€", + "return" + ]) + .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "|", "_", ".", ",", "/", "shift"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .build() } - } - - static func genPadExpandedLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["~", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "delete"]) - .addRow([SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]", "\\"]) - .addRow([SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", ":", ";", "'", "return"]) - .addRow(["shift", "-", "z", "x", "c", "v", "b", "n", "m", ",", ".", "/", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadExpandedSymbolKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) - .addRow([SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "—", "~", "°"]) - .addRow([SpecialKeys.capsLock, "-", "\\", ":", ";", "(", ")", "&", "@", "$", "£", "¥", "€", "return"]) - .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "|", "_", ".", ",", "/", "shift"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .build() - } } // MARK: Generate and set keyboard @@ -135,124 +148,150 @@ struct ENKeyboardProvider: KeyboardProviderProtocol { // MARK: Get Keys func getENKeys() { - guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") else { - fatalError("Unable to access shared user defaults") - } - - var currencyKey = ENKeyboardConstants.defaultCurrencyKey - var currencyKeys = ENKeyboardConstants.currencyKeys - let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" - if let currencyValue = userDefaults.string(forKey: dictionaryKey) { - currencyKey = currencyValue - } else { - userDefaults.setValue(currencyKey, forKey: dictionaryKey) - } - if let index = currencyKeys.firstIndex(of: currencyKey) { - currencyKeys.remove(at: index) - } - - if DeviceType.isPhone { - letterKeys = ENKeyboardProvider.genPhoneLetterKeys() - numberKeys = ENKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) - symbolKeys = ENKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) - - leftKeyChars = ["q", "1", "-", "[", "_"] - rightKeyChars = ["p", "0", "\"", "=", "·"] - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } else { - // Use the expanded keys layout if the iPad is wide enough and has no home button. - if usingExpandedKeyboard { - letterKeys = ENKeyboardProvider.genPadExpandedLetterKeys() - symbolKeys = ENKeyboardProvider.genPadExpandedSymbolKeys() - - leftKeyChars = ["~", "`"] - rightKeyChars = ["\\", "°"] - allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) - } else { - letterKeys = ENKeyboardProvider.genPadLetterKeys() - numberKeys = ENKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) - symbolKeys = ENKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) + guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + else { + fatalError("Unable to access shared user defaults") + } - letterKeys.removeFirst(1) + var currencyKey = ENKeyboardConstants.defaultCurrencyKey + var currencyKeys = ENKeyboardConstants.currencyKeys + let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" + if let currencyValue = userDefaults.string(forKey: dictionaryKey) { + currencyKey = currencyValue + } else { + userDefaults.setValue(currencyKey, forKey: dictionaryKey) + } + if let index = currencyKeys.firstIndex(of: currencyKey) { + currencyKeys.remove(at: index) + } - leftKeyChars = ["q", "1"] - rightKeyChars = [] - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + if DeviceType.isPhone { + letterKeys = ENKeyboardProvider.genPhoneLetterKeys() + numberKeys = ENKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) + symbolKeys = ENKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + + leftKeyChars = ["q", "1", "-", "[", "_"] + rightKeyChars = ["p", "0", "\"", "=", "·"] + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } + } else { + // Use the expanded keys layout if the iPad is wide enough and has no home button. + if usingExpandedKeyboard { + letterKeys = ENKeyboardProvider.genPadExpandedLetterKeys() + symbolKeys = ENKeyboardProvider.genPadExpandedSymbolKeys() + + leftKeyChars = ["~", "`"] + rightKeyChars = ["\\", "°"] + allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) + } else { + letterKeys = ENKeyboardProvider.genPadLetterKeys() + numberKeys = ENKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) + symbolKeys = ENKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) + + letterKeys.removeFirst(1) + + leftKeyChars = ["q", "1"] + rightKeyChars = [] + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + } + + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } } - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } - - keysWithAlternates = ENKeyboardConstants.keysWithAlternates - keysWithAlternatesLeft = ENKeyboardConstants.keysWithAlternatesLeft - keysWithAlternatesRight = ENKeyboardConstants.keysWithAlternatesRight - aAlternateKeys = ENKeyboardConstants.aAlternateKeys - eAlternateKeys = ENKeyboardConstants.eAlternateKeys - iAlternateKeys = ENKeyboardConstants.iAlternateKeys - oAlternateKeys = ENKeyboardConstants.oAlternateKeys - uAlternateKeys = ENKeyboardConstants.uAlternateKeys - sAlternateKeys = ENKeyboardConstants.sAlternateKeys - lAlternateKeys = ENKeyboardConstants.lAlternateKeys - zAlternateKeys = ENKeyboardConstants.zAlternateKeys - cAlternateKeys = ENKeyboardConstants.cAlternateKeys - nAlternateKeys = ENKeyboardConstants.nAlternateKeys + keysWithAlternates = ENKeyboardConstants.keysWithAlternates + keysWithAlternatesLeft = ENKeyboardConstants.keysWithAlternatesLeft + keysWithAlternatesRight = ENKeyboardConstants.keysWithAlternatesRight + aAlternateKeys = ENKeyboardConstants.aAlternateKeys + eAlternateKeys = ENKeyboardConstants.eAlternateKeys + iAlternateKeys = ENKeyboardConstants.iAlternateKeys + oAlternateKeys = ENKeyboardConstants.oAlternateKeys + uAlternateKeys = ENKeyboardConstants.uAlternateKeys + sAlternateKeys = ENKeyboardConstants.sAlternateKeys + lAlternateKeys = ENKeyboardConstants.lAlternateKeys + zAlternateKeys = ENKeyboardConstants.zAlternateKeys + cAlternateKeys = ENKeyboardConstants.cAlternateKeys + nAlternateKeys = ENKeyboardConstants.nAlternateKeys } // MARK: Provide Layout func setENKeyboardLayout() { - getENKeys() - - currencySymbol = "$" - currencySymbolAlternates = dollarAlternateKeys - spaceBar = "space" - - invalidCommandMsgWikidata = "Not in Wikidata" - invalidCommandTextWikidata1 = "Wikidata is a collaboratively edited knowledge graph that's maintained by the Wikimedia Foundation. It serves as a source of open data for projects like Wikipedia and countless others." - invalidCommandTextWikidata2 = "Scribe uses Wikidata's language data for many of its core features. We get information like noun genders, verb conjugations and much more!" - invalidCommandTextWikidata3 = "You can make an account at wikidata.org to join the community that's supporting Scribe and so many other projects. Help us bring free information to the world!" - - invalidCommandMsgWiktionary = "Not in Wiktionary" - invalidCommandTextWiktionary1 = "Wiktionary is a collaboratively edited dictionary that's maintained by the Wikimedia Foundation. It serves as a source of free linguistic data for projects like Wikipedia and countless others." - invalidCommandTextWiktionary2 = "Scribe uses Wiktionary's data to provide translations for its Translate command. Our data is derived from the many language pairs that Wiktionary's community has created!" - invalidCommandTextWiktionary3 = "You can make an account at wiktionary.org to join the community that's supporting Scribe and so many other projects. Help us bring free information to the world!" - - baseAutosuggestions = ["I", "I'm", "we"] - numericAutosuggestions = ["is", "to", "and"] - verbsAfterPronounsArray = ["have", "be", "can"] - pronounAutosuggestionTenses = [ - "I": "presSimp", - "you": "presSimp", - "he": "presTPS", - "she": "presTPS", - "it": "presTPS", - "we": "presSimp", - "they": "presSimp" - ] - - translateKeyLbl = "Translate" - translatePlaceholder = "Enter a word" - translatePrompt = commandPromptSpacing + "en -› \(getControllerLanguageAbbr()): " - translatePromptAndCursor = translatePrompt + commandCursor - translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder - translatePromptAndColorPlaceholder = NSMutableAttributedString(string: translatePromptAndPlaceholder) - translatePromptAndColorPlaceholder.setColorForText(textForAttribute: translatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - conjugateKeyLbl = "Conjugate" - conjugatePlaceholder = "Enter a verb" - conjugatePrompt = commandPromptSpacing + "Conjugate: " - conjugatePromptAndCursor = conjugatePrompt + commandCursor - conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder - conjugatePromptAndColorPlaceholder = NSMutableAttributedString(string: conjugatePromptAndPlaceholder) - conjugatePromptAndColorPlaceholder.setColorForText(textForAttribute: conjugatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - pluralKeyLbl = "Plural" - pluralPlaceholder = "Enter a noun" - pluralPrompt = commandPromptSpacing + "Plural: " - pluralPromptAndCursor = pluralPrompt + commandCursor - pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder - pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) - pluralPromptAndColorPlaceholder.setColorForText(textForAttribute: pluralPlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - alreadyPluralMsg = "Already plural" + getENKeys() + + currencySymbol = "$" + currencySymbolAlternates = dollarAlternateKeys + spaceBar = "space" + + invalidCommandMsgWikidata = "Not in Wikidata" + invalidCommandTextWikidata1 = + "Wikidata is a collaboratively edited knowledge graph that's maintained by the Wikimedia Foundation. It serves as a source of open data for projects like Wikipedia and countless others." + invalidCommandTextWikidata2 = + "Scribe uses Wikidata's language data for many of its core features. We get information like noun genders, verb conjugations and much more!" + invalidCommandTextWikidata3 = + "You can make an account at wikidata.org to join the community that's supporting Scribe and so many other projects. Help us bring free information to the world!" + + invalidCommandMsgWiktionary = "Not in Wiktionary" + invalidCommandTextWiktionary1 = + "Wiktionary is a collaboratively edited dictionary that's maintained by the Wikimedia Foundation. It serves as a source of free linguistic data for projects like Wikipedia and countless others." + invalidCommandTextWiktionary2 = + "Scribe uses Wiktionary's data to provide translations for its Translate command. Our data is derived from the many language pairs that Wiktionary's community has created!" + invalidCommandTextWiktionary3 = + "You can make an account at wiktionary.org to join the community that's supporting Scribe and so many other projects. Help us bring free information to the world!" + + baseAutosuggestions = ["I", "I'm", "we"] + numericAutosuggestions = ["is", "to", "and"] + verbsAfterPronounsArray = ["have", "be", "can"] + pronounAutosuggestionTenses = [ + "I": "presSimp", + "you": "presSimp", + "he": "presTPS", + "she": "presTPS", + "it": "presTPS", + "we": "presSimp", + "they": "presSimp" + ] + + translateKeyLbl = "Translate" + translatePlaceholder = "Enter a word" + translatePrompt = commandPromptSpacing + "en -› \(getControllerLanguageAbbr()): " + translatePromptAndCursor = translatePrompt + commandCursor + translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder + translatePromptAndColorPlaceholder = NSMutableAttributedString( + string: translatePromptAndPlaceholder + ) + translatePromptAndColorPlaceholder.setColorForText( + textForAttribute: translatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + conjugateKeyLbl = "Conjugate" + conjugatePlaceholder = "Enter a verb" + conjugatePrompt = commandPromptSpacing + "Conjugate: " + conjugatePromptAndCursor = conjugatePrompt + commandCursor + conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder + conjugatePromptAndColorPlaceholder = NSMutableAttributedString( + string: conjugatePromptAndPlaceholder + ) + conjugatePromptAndColorPlaceholder.setColorForText( + textForAttribute: conjugatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + pluralKeyLbl = "Plural" + pluralPlaceholder = "Enter a noun" + pluralPrompt = commandPromptSpacing + "Plural: " + pluralPromptAndCursor = pluralPrompt + commandCursor + pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder + pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) + pluralPromptAndColorPlaceholder.setColorForText( + textForAttribute: pluralPlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + alreadyPluralMsg = "Already plural" } diff --git a/Keyboards/LanguageKeyboards/English/ENKeyboardViewController.swift b/Keyboards/LanguageKeyboards/English/ENKeyboardViewController.swift index 357c08ef..ce041bf4 100644 --- a/Keyboards/LanguageKeyboards/English/ENKeyboardViewController.swift +++ b/Keyboards/LanguageKeyboards/English/ENKeyboardViewController.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Class for the English Scribe keyboard. */ diff --git a/Keyboards/LanguageKeyboards/French/FR-AZERTYInterfaceVariables.swift b/Keyboards/LanguageKeyboards/French/FR-AZERTYInterfaceVariables.swift index fbea71bb..8be89a71 100644 --- a/Keyboards/LanguageKeyboards/French/FR-AZERTYInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/French/FR-AZERTYInterfaceVariables.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Constants and functions to load the AZERTY French Scribe keyboard. */ @@ -9,249 +9,288 @@ import UIKit // MARK: Constants public enum FRKeyboardConstants { - static let defaultCurrencyKey = "€" - static let currencyKeys = ["€", "$", "£", "¥"] - - // Alternate key vars. - static let keysWithAlternates = ["a", "e", "i", "o", "u", "y", "c", "n"] - static let keysWithAlternatesLeft = ["a", "e", "y", "c"] - static let keysWithAlternatesRight = ["i", "o", "u", "n"] - - static let aAlternateKeys = ["à", "â", "æ", "á", "ä", "ã", "å", "ā", "ᵃ"] - static let eAlternateKeys = ["é", "è", "ê", "ë", "ę", "ė", "ē"] - static let iAlternateKeys = ["ī", "į", "í", "ì", "ï", "î"] - static let oAlternateKeys = ["ᵒ", "ō", "ø", "õ", "ó", "ò", "ö", "œ", "ô"] - static let uAlternateKeys = ["ū", "ú", "ü", "ù", "û"] - static let yAlternateKeys = ["ÿ"] - static let cAlternateKeys = ["ç", "ć", "č"] - static let nAlternateKeys = ["ń", "ñ"] + static let defaultCurrencyKey = "€" + static let currencyKeys = ["€", "$", "£", "¥"] + + // Alternate key vars. + static let keysWithAlternates = ["a", "e", "i", "o", "u", "y", "c", "n"] + static let keysWithAlternatesLeft = ["a", "e", "y", "c"] + static let keysWithAlternatesRight = ["i", "o", "u", "n"] + + static let aAlternateKeys = ["à", "â", "æ", "á", "ä", "ã", "å", "ā", "ᵃ"] + static let eAlternateKeys = ["é", "è", "ê", "ë", "ę", "ė", "ē"] + static let iAlternateKeys = ["ī", "į", "í", "ì", "ï", "î"] + static let oAlternateKeys = ["ᵒ", "ō", "ø", "õ", "ó", "ò", "ö", "œ", "ô"] + static let uAlternateKeys = ["ū", "ú", "ü", "ù", "û"] + static let yAlternateKeys = ["ÿ"] + static let cAlternateKeys = ["ç", "ć", "č"] + static let nAlternateKeys = ["ń", "ñ"] } struct FRKeyboardProvider: KeyboardProviderProtocol { - // MARK: iPhone Layouts - - static func genPhoneLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["a", "z", "e", "r", "t", "y", "u", "i", "o", "p"]) - .addRow(["q", "s", "d", "f", "g", "h", "j", "k", "l", "m"]) - .addRow(["shift", "w", "x", "c", "v", "b", "n", "´", "delete"]) - .addRow(["123", "selectKeyboard", "space", "return"]) - .build() - } - - static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) - .addRow(["-", "/", ":", ";", "(", ")", "€", "&", "@", "\""]) - .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - .replaceKey(row: 1, column: 6, to: currencyKey) - .build() - } - - static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) - .addRow(["_", "\\", "|", "~", "<", ">", "$", "£", "¥", "·"]) - .addRow(["123", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 6, to: currencyKeys[0]) - .replaceKey(row: 1, column: 7, to: currencyKeys[1]) - .replaceKey(row: 1, column: 8, to: currencyKeys[2]) - .build() + // MARK: iPhone Layouts + + static func genPhoneLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["a", "z", "e", "r", "t", "y", "u", "i", "o", "p"]) + .addRow(["q", "s", "d", "f", "g", "h", "j", "k", "l", "m"]) + .addRow(["shift", "w", "x", "c", "v", "b", "n", "´", "delete"]) + .addRow(["123", "selectKeyboard", "space", "return"]) + .build() } - } - - // MARK: iPad Layouts - - static func genPadLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) - .addRow(["a", "z", "e", "r", "t", "y", "u", "i", "o", "p", "delete"]) - .addRow(["q", "s", "d", "f", "g", "h", "j", "k", "l", "m", "return"]) - .addRow(["shift", "w", "x", "c", "v", "b", "n", "´", ",", ".", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) - .addRow(["@", "#", "&", "\"", "€", "(", "!", ")", "-", "*", "return"]) - .addRow(["#+=", "%", "_", "+", "=", "/", ";", ":", ",", ".", "#+="]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .replaceKey(row: 1, column: 4, to: currencyKey) - .build() - } - - static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) - .addRow(["~", "ᵒ", "[", "]", "{", "}", "^", "$", "£", "¥", "return"]) - .addRow(["123", "§", "<", ">", "|", "\\", "...", "·", "?", "'", "123"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 7, to: currencyKeys[0]) - .replaceKey(row: 1, column: 8, to: currencyKeys[1]) - .replaceKey(row: 1, column: 9, to: currencyKeys[2]) - .build() + + static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) + .addRow(["-", "/", ":", ";", "(", ")", "€", "&", "@", "\""]) + .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + .replaceKey(row: 1, column: 6, to: currencyKey) + .build() + } + + static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) + .addRow(["_", "\\", "|", "~", "<", ">", "$", "£", "¥", "·"]) + .addRow(["123", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 6, to: currencyKeys[0]) + .replaceKey(row: 1, column: 7, to: currencyKeys[1]) + .replaceKey(row: 1, column: 8, to: currencyKeys[2]) + .build() + } + } + + // MARK: iPad Layouts + + static func genPadLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) + .addRow(["a", "z", "e", "r", "t", "y", "u", "i", "o", "p", "delete"]) + .addRow(["q", "s", "d", "f", "g", "h", "j", "k", "l", "m", "return"]) + .addRow(["shift", "w", "x", "c", "v", "b", "n", "´", ",", ".", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) + .addRow(["@", "#", "&", "\"", "€", "(", "!", ")", "-", "*", "return"]) + .addRow(["#+=", "%", "_", "+", "=", "/", ";", ":", ",", ".", "#+="]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .replaceKey(row: 1, column: 4, to: currencyKey) + .build() + } + + static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) + .addRow(["~", "ᵒ", "[", "]", "{", "}", "^", "$", "£", "¥", "return"]) + .addRow(["123", "§", "<", ">", "|", "\\", "...", "·", "?", "'", "123"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 7, to: currencyKeys[0]) + .replaceKey(row: 1, column: 8, to: currencyKeys[1]) + .replaceKey(row: 1, column: 9, to: currencyKeys[2]) + .build() + } + } + + // MARK: Expanded iPad Layouts + + static func genPadExpandedLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["@", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "ç", "à", "delete"]) + .addRow([ + SpecialKeys.indent, "a", "z", "e", "r", "t", "y", "u", "i", "o", "p", "^", "+", "*" + ]) + .addRow([ + SpecialKeys.capsLock, "q", "s", "d", "f", "g", "h", "j", "k", "l", "m", "ù", "#", + "return" + ]) + .addRow(["shift", "/", "w", "x", "c", "v", "b", "n", ":", "-", ",", ".", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadExpandedSymbolKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) + .addRow([ + SpecialKeys.indent, "\"", "|", "§", "[", "]", "{", "}", "-", "%", "=", "^", "+", + "*" + ]) + .addRow([ + SpecialKeys.capsLock, "/", "…", "_", "(", ")", "&", "$", "£", "¥", "€", "@", "#", + "return" + ]) + .addRow(["shift", "'", "?", "!", "~", "≠", "°", ";", ":", "-", ",", ".", "shift"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .build() } - } - - // MARK: Expanded iPad Layouts - - static func genPadExpandedLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["@", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "ç", "à", "delete"]) - .addRow([SpecialKeys.indent, "a", "z", "e", "r", "t", "y", "u", "i", "o", "p", "^", "+", "*"]) - .addRow([SpecialKeys.capsLock, "q", "s", "d", "f", "g", "h", "j", "k", "l", "m", "ù", "#", "return"]) - .addRow(["shift", "/", "w", "x", "c", "v", "b", "n", ":", "-", ",", ".", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadExpandedSymbolKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) - .addRow([SpecialKeys.indent, "\"", "|", "§", "[", "]", "{", "}", "-", "%", "=", "^", "+", "*"]) - .addRow([SpecialKeys.capsLock, "/", "…", "_", "(", ")", "&", "$", "£", "¥", "€", "@", "#", "return"]) - .addRow(["shift", "'", "?", "!", "~", "≠", "°", ";", ":", "-", ",", ".", "shift"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .build() - } } // MARK: Get Keys func getFRKeys() { - guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") else { - fatalError("Unable to access shared user defaults") - } - - var currencyKey = FRKeyboardConstants.defaultCurrencyKey - var currencyKeys = FRKeyboardConstants.currencyKeys - let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" - if let currencyValue = userDefaults.string(forKey: dictionaryKey) { - currencyKey = currencyValue - } else { - userDefaults.setValue(currencyKey, forKey: dictionaryKey) - } - if let index = currencyKeys.firstIndex(of: currencyKey) { - currencyKeys.remove(at: index) - } - - if DeviceType.isPhone { - letterKeys = FRKeyboardProvider.genPhoneLetterKeys() - numberKeys = FRKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) - symbolKeys = FRKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) - - leftKeyChars = ["a", "q", "1", "-", "[", "_"] - rightKeyChars = ["p", "m", "0", "\"", "=", "·"] - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } else { - // Use the expanded keys layout if the iPad is wide enough and has no home button. - if usingExpandedKeyboard { - letterKeys = FRKeyboardProvider.genPadExpandedLetterKeys() - symbolKeys = FRKeyboardProvider.genPadExpandedSymbolKeys() - - leftKeyChars = ["@", "`"] - rightKeyChars = ["*"] - allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) - } else { - letterKeys = FRKeyboardProvider.genPadLetterKeys() - numberKeys = FRKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) - symbolKeys = FRKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) + guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + else { + fatalError("Unable to access shared user defaults") + } - letterKeys.removeFirst(1) + var currencyKey = FRKeyboardConstants.defaultCurrencyKey + var currencyKeys = FRKeyboardConstants.currencyKeys + let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" + if let currencyValue = userDefaults.string(forKey: dictionaryKey) { + currencyKey = currencyValue + } else { + userDefaults.setValue(currencyKey, forKey: dictionaryKey) + } + if let index = currencyKeys.firstIndex(of: currencyKey) { + currencyKeys.remove(at: index) + } - leftKeyChars = ["q", "a", "1", "@", "~"] - rightKeyChars = [] - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + if DeviceType.isPhone { + letterKeys = FRKeyboardProvider.genPhoneLetterKeys() + numberKeys = FRKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) + symbolKeys = FRKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + + leftKeyChars = ["a", "q", "1", "-", "[", "_"] + rightKeyChars = ["p", "m", "0", "\"", "=", "·"] + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } + } else { + // Use the expanded keys layout if the iPad is wide enough and has no home button. + if usingExpandedKeyboard { + letterKeys = FRKeyboardProvider.genPadExpandedLetterKeys() + symbolKeys = FRKeyboardProvider.genPadExpandedSymbolKeys() + + leftKeyChars = ["@", "`"] + rightKeyChars = ["*"] + allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) + } else { + letterKeys = FRKeyboardProvider.genPadLetterKeys() + numberKeys = FRKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) + symbolKeys = FRKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) + + letterKeys.removeFirst(1) + + leftKeyChars = ["q", "a", "1", "@", "~"] + rightKeyChars = [] + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + } + + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } } - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } - - keysWithAlternates = FRKeyboardConstants.keysWithAlternates - keysWithAlternatesLeft = FRKeyboardConstants.keysWithAlternatesLeft - keysWithAlternatesRight = FRKeyboardConstants.keysWithAlternatesRight - aAlternateKeys = FRKeyboardConstants.aAlternateKeys - eAlternateKeys = FRKeyboardConstants.eAlternateKeys - iAlternateKeys = FRKeyboardConstants.iAlternateKeys - oAlternateKeys = FRKeyboardConstants.oAlternateKeys - uAlternateKeys = FRKeyboardConstants.uAlternateKeys - yAlternateKeys = FRKeyboardConstants.yAlternateKeys - cAlternateKeys = FRKeyboardConstants.cAlternateKeys - nAlternateKeys = FRKeyboardConstants.nAlternateKeys + keysWithAlternates = FRKeyboardConstants.keysWithAlternates + keysWithAlternatesLeft = FRKeyboardConstants.keysWithAlternatesLeft + keysWithAlternatesRight = FRKeyboardConstants.keysWithAlternatesRight + aAlternateKeys = FRKeyboardConstants.aAlternateKeys + eAlternateKeys = FRKeyboardConstants.eAlternateKeys + iAlternateKeys = FRKeyboardConstants.iAlternateKeys + oAlternateKeys = FRKeyboardConstants.oAlternateKeys + uAlternateKeys = FRKeyboardConstants.uAlternateKeys + yAlternateKeys = FRKeyboardConstants.yAlternateKeys + cAlternateKeys = FRKeyboardConstants.cAlternateKeys + nAlternateKeys = FRKeyboardConstants.nAlternateKeys } // MARK: Provide Layout func setFRKeyboardLayout() { - getFRKeys() - - currencySymbol = "€" - currencySymbolAlternates = euroAlternateKeys - spaceBar = "espace" - language = "Français" - - invalidCommandMsgWikidata = "Pas dans Wikidata" - invalidCommandTextWikidata1 = "Wikidata est un réseau de connaissances collaboratif géré par la fondation Wikimedia. Il sert de source de données ouvertes pour des projets tels que Wikipédia et bien d'autres." - invalidCommandTextWikidata2 = "Scribe utilise les données linguistiques de Wikidata pour un grand nombre de ses fonctionnalités de base. Nous obtenons des informations telles que le genre des noms, la conjugaison des verbes et bien plus encore !" - invalidCommandTextWikidata3 = "Vous pouvez créer un compte sur wikidata.org pour rejoindre la communauté qui soutient Scribe et bien d'autres projets. Contribuez à la diffusion d'informations gratuites dans le monde entier !" - - invalidCommandMsgWiktionary = "Pas dans Wiktionary" - invalidCommandTextWiktionary1 = "Wiktionary est un dictionnaire collaboratif maintenu par la Fondation Wikimedia. Il sert de source de données linguistiques libres pour des projets comme Wikipédia et bien d'autres." - invalidCommandTextWiktionary2 = "Scribe utilise les données de Wiktionary pour fournir des traductions à sa commande « Traduire ». Nos données proviennent des nombreuses paires de langues créées par la communauté de Wiktionary !" - invalidCommandTextWiktionary3 = "Vous pouvez créer un compte sur wiktionary.org pour rejoindre la communauté qui soutient Scribe et de nombreux autres projets. Aidez-nous à diffuser l'information libre dans le monde entier !" - - baseAutosuggestions = ["je", "il", "le"] - numericAutosuggestions = ["je", "que", "c’est"] - verbsAfterPronounsArray = ["être", "avoir", "ne"] - pronounAutosuggestionTenses = [ - "je": "presFPS", - "tu": "presSPS", - "il": "presTPS", - "elle": "presTPS", - "on": "presTPS", - "nous": "presFPP", - "vous": "presSPP", - "ils": "presTPP", - "elles": "presTPP" - ] - - translateKeyLbl = "Traduire" - translatePlaceholder = "Entrez un mot" - translatePrompt = commandPromptSpacing + "fr -› \(getControllerLanguageAbbr()): " - translatePromptAndCursor = translatePrompt + commandCursor - translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder - translatePromptAndColorPlaceholder = NSMutableAttributedString(string: translatePromptAndPlaceholder) - translatePromptAndColorPlaceholder.setColorForText(textForAttribute: translatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - conjugateKeyLbl = "Conjuguer" - conjugatePlaceholder = "Entrez un verbe" - conjugatePrompt = commandPromptSpacing + "Conjuguer: " - conjugatePromptAndCursor = conjugatePrompt + commandCursor - conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder - conjugatePromptAndColorPlaceholder = NSMutableAttributedString(string: conjugatePromptAndPlaceholder) - conjugatePromptAndColorPlaceholder.setColorForText(textForAttribute: conjugatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - pluralKeyLbl = "Pluriel" - pluralPlaceholder = "Entrez un nom" - pluralPrompt = commandPromptSpacing + "Pluriel: " - pluralPromptAndCursor = pluralPrompt + commandCursor - pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder - pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) - pluralPromptAndColorPlaceholder.setColorForText(textForAttribute: pluralPlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - alreadyPluralMsg = "Déjà pluriel" + getFRKeys() + + currencySymbol = "€" + currencySymbolAlternates = euroAlternateKeys + spaceBar = "espace" + language = "Français" + + invalidCommandMsgWikidata = "Pas dans Wikidata" + invalidCommandTextWikidata1 = + "Wikidata est un réseau de connaissances collaboratif géré par la fondation Wikimedia. Il sert de source de données ouvertes pour des projets tels que Wikipédia et bien d'autres." + invalidCommandTextWikidata2 = + "Scribe utilise les données linguistiques de Wikidata pour un grand nombre de ses fonctionnalités de base. Nous obtenons des informations telles que le genre des noms, la conjugaison des verbes et bien plus encore !" + invalidCommandTextWikidata3 = + "Vous pouvez créer un compte sur wikidata.org pour rejoindre la communauté qui soutient Scribe et bien d'autres projets. Contribuez à la diffusion d'informations gratuites dans le monde entier !" + + invalidCommandMsgWiktionary = "Pas dans Wiktionary" + invalidCommandTextWiktionary1 = + "Wiktionary est un dictionnaire collaboratif maintenu par la Fondation Wikimedia. Il sert de source de données linguistiques libres pour des projets comme Wikipédia et bien d'autres." + invalidCommandTextWiktionary2 = + "Scribe utilise les données de Wiktionary pour fournir des traductions à sa commande « Traduire ». Nos données proviennent des nombreuses paires de langues créées par la communauté de Wiktionary !" + invalidCommandTextWiktionary3 = + "Vous pouvez créer un compte sur wiktionary.org pour rejoindre la communauté qui soutient Scribe et de nombreux autres projets. Aidez-nous à diffuser l'information libre dans le monde entier !" + + baseAutosuggestions = ["je", "il", "le"] + numericAutosuggestions = ["je", "que", "c’est"] + verbsAfterPronounsArray = ["être", "avoir", "ne"] + pronounAutosuggestionTenses = [ + "je": "presFPS", + "tu": "presSPS", + "il": "presTPS", + "elle": "presTPS", + "on": "presTPS", + "nous": "presFPP", + "vous": "presSPP", + "ils": "presTPP", + "elles": "presTPP" + ] + + translateKeyLbl = "Traduire" + translatePlaceholder = "Entrez un mot" + translatePrompt = commandPromptSpacing + "fr -› \(getControllerLanguageAbbr()): " + translatePromptAndCursor = translatePrompt + commandCursor + translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder + translatePromptAndColorPlaceholder = NSMutableAttributedString( + string: translatePromptAndPlaceholder + ) + translatePromptAndColorPlaceholder.setColorForText( + textForAttribute: translatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + conjugateKeyLbl = "Conjuguer" + conjugatePlaceholder = "Entrez un verbe" + conjugatePrompt = commandPromptSpacing + "Conjuguer: " + conjugatePromptAndCursor = conjugatePrompt + commandCursor + conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder + conjugatePromptAndColorPlaceholder = NSMutableAttributedString( + string: conjugatePromptAndPlaceholder + ) + conjugatePromptAndColorPlaceholder.setColorForText( + textForAttribute: conjugatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + pluralKeyLbl = "Pluriel" + pluralPlaceholder = "Entrez un nom" + pluralPrompt = commandPromptSpacing + "Pluriel: " + pluralPromptAndCursor = pluralPrompt + commandCursor + pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder + pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) + pluralPromptAndColorPlaceholder.setColorForText( + textForAttribute: pluralPlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + alreadyPluralMsg = "Déjà pluriel" } diff --git a/Keyboards/LanguageKeyboards/French/FR-QWERTYInterfaceVariables.swift b/Keyboards/LanguageKeyboards/French/FR-QWERTYInterfaceVariables.swift index 604f6b4e..0c8bb79a 100644 --- a/Keyboards/LanguageKeyboards/French/FR-QWERTYInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/French/FR-QWERTYInterfaceVariables.swift @@ -1,132 +1,144 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Constants and functions to load the QWERTY French Scribe keyboard. */ public enum FRQWERTYKeyboardConstants { - // iPhone keyboard layouts. - static let letterKeysPhone = [ - ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"], - ["a", "s", "d", "f", "g", "h", "j", "k", "l", "´"], - ["shift", "z", "x", "c", "v", "b", "n", "m", "delete"], - ["123", "selectKeyboard", "space", "return"] - ] - - static let numberKeysPhone = [ - ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], - ["-", "/", ":", ";", "(", ")", "$", "&", "@", "\""], - ["#+=", ".", ",", "?", "!", "'", "delete"], - ["ABC", "selectKeyboard", "space", "return"] - ] - - static let symbolKeysPhone = [ - ["[", "]", "{", "}", "#", "%", "^", "*", "+", "="], - ["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "·"], - ["123", ".", ",", "?", "!", "'", "delete"], - ["ABC", "selectKeyboard", "space", "return"] - ] - - // MARK: iPad Layouts - - static let letterKeysPad = [ - ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"], - ["a", "z", "e", "r", "t", "y", "u", "i", "o", "p", "delete"], - ["q", "s", "d", "f", "g", "h", "j", "k", "l", "m", "return"], - ["shift", "w", "x", "c", "v", "b", "n", "´", ",", ".", "shift"], - ["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"] - ] - - static let numberKeysPad = [ - ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"], - ["@", "#", "&", "\"", "€", "(", "!", ")", "-", "*", "return"], - ["#+=", "%", "_", "+", "=", "/", ";", ":", ",", ".", "#+="], - ["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"] - ] - - static let symbolKeysPad = [ - ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"], - ["~", "ᵒ", "[", "]", "{", "}", "^", "$", "£", "¥", "return"], - ["123", "§", "<", ">", "|", "\\", "...", "·", "?", "'", "123"], - ["ABC", "selectKeyboard", "space", "ABC", "hideKeyboard"] - ] - - // MARK: Expanded iPad Layouts - - static let letterKeysPadExpanded = [ - ["/", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "delete"], - [SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "^", "ç", ":"], - [SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "è", "à", "return"], - ["shift", "ù", "z", "x", "c", "v", "b", "n", "m", ",", ".", "é", "shift"], - ["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"] - ] - - static let symbolKeysPadExpanded = [ - ["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"], - [SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "\"", "|", "~"], - [SpecialKeys.capsLock, "-", "/", ":", ";", "(", ")", "$", "&", "@", "£", "¥", "~", "return"], - ["shift", "…", "?", "!", "≠", "°", "'", "\"", "_", ",", ".", "€", "shift"], - ["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"] - ] - - // Alternate key vars. - static let keysWithAlternates = ["a", "e", "i", "o", "u", "y", "c", "n"] - static let keysWithAlternatesLeft = ["a", "e", "y", "c"] - static let keysWithAlternatesRight = ["i", "o", "u", "n"] - - static let aAlternateKeys = ["à", "â", "æ", "á", "ä", "ã", "å", "ā", "ᵃ"] - static let eAlternateKeys = ["é", "è", "ê", "ë", "ę", "ė", "ē"] - static let iAlternateKeys = ["ī", "į", "í", "ì", "ï", "î"] - static let oAlternateKeys = ["ᵒ", "ō", "ø", "õ", "ó", "ò", "ö", "œ", "ô"] - static let uAlternateKeys = ["ū", "ú", "ü", "ù", "û"] - static let yAlternateKeys = ["ÿ"] - static let cAlternateKeys = ["ç", "ć", "č"] - static let nAlternateKeys = ["ń", "ñ"] + /// iPhone keyboard layouts. + static let letterKeysPhone = [ + ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"], + ["a", "s", "d", "f", "g", "h", "j", "k", "l", "´"], + ["shift", "z", "x", "c", "v", "b", "n", "m", "delete"], + ["123", "selectKeyboard", "space", "return"] + ] + + static let numberKeysPhone = [ + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], + ["-", "/", ":", ";", "(", ")", "$", "&", "@", "\""], + ["#+=", ".", ",", "?", "!", "'", "delete"], + ["ABC", "selectKeyboard", "space", "return"] + ] + + static let symbolKeysPhone = [ + ["[", "]", "{", "}", "#", "%", "^", "*", "+", "="], + ["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "·"], + ["123", ".", ",", "?", "!", "'", "delete"], + ["ABC", "selectKeyboard", "space", "return"] + ] + + // MARK: iPad Layouts + + static let letterKeysPad = [ + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"], + ["a", "z", "e", "r", "t", "y", "u", "i", "o", "p", "delete"], + ["q", "s", "d", "f", "g", "h", "j", "k", "l", "m", "return"], + ["shift", "w", "x", "c", "v", "b", "n", "´", ",", ".", "shift"], + ["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"] + ] + + static let numberKeysPad = [ + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"], + ["@", "#", "&", "\"", "€", "(", "!", ")", "-", "*", "return"], + ["#+=", "%", "_", "+", "=", "/", ";", ":", ",", ".", "#+="], + ["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"] + ] + + static let symbolKeysPad = [ + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"], + ["~", "ᵒ", "[", "]", "{", "}", "^", "$", "£", "¥", "return"], + ["123", "§", "<", ">", "|", "\\", "...", "·", "?", "'", "123"], + ["ABC", "selectKeyboard", "space", "ABC", "hideKeyboard"] + ] + + // MARK: Expanded iPad Layouts + + static let letterKeysPadExpanded = [ + ["/", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "delete"], + [SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "^", "ç", ":"], + [ + SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "è", "à", + "return" + ], + ["shift", "ù", "z", "x", "c", "v", "b", "n", "m", ",", ".", "é", "shift"], + ["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"] + ] + + static let symbolKeysPadExpanded = [ + ["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"], + [SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "\"", "|", "~"], + [ + SpecialKeys.capsLock, "-", "/", ":", ";", "(", ")", "$", "&", "@", "£", "¥", "~", + "return" + ], + ["shift", "…", "?", "!", "≠", "°", "'", "\"", "_", ",", ".", "€", "shift"], + ["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"] + ] + + // Alternate key vars. + static let keysWithAlternates = ["a", "e", "i", "o", "u", "y", "c", "n"] + static let keysWithAlternatesLeft = ["a", "e", "y", "c"] + static let keysWithAlternatesRight = ["i", "o", "u", "n"] + + static let aAlternateKeys = ["à", "â", "æ", "á", "ä", "ã", "å", "ā", "ᵃ"] + static let eAlternateKeys = ["é", "è", "ê", "ë", "ę", "ė", "ē"] + static let iAlternateKeys = ["ī", "į", "í", "ì", "ï", "î"] + static let oAlternateKeys = ["ᵒ", "ō", "ø", "õ", "ó", "ò", "ö", "œ", "ô"] + static let uAlternateKeys = ["ū", "ú", "ü", "ù", "û"] + static let yAlternateKeys = ["ÿ"] + static let cAlternateKeys = ["ç", "ć", "č"] + static let nAlternateKeys = ["ń", "ñ"] } // MARK: Get Keys func getFRQWERTYKeys() { - if DeviceType.isPhone { - letterKeys = FRQWERTYKeyboardConstants.letterKeysPhone - numberKeys = FRQWERTYKeyboardConstants.numberKeysPhone - symbolKeys = FRQWERTYKeyboardConstants.symbolKeysPhone - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) - - leftKeyChars = ["a", "q", "1", "-", "[", "_"] - rightKeyChars = ["p", "m", "0", "\"", "=", "·"] - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } else { - // Use the expanded keys layout if the iPad is wide enough and has no home button. - if usingExpandedKeyboard { - letterKeys = FRQWERTYKeyboardConstants.letterKeysPadExpanded - symbolKeys = FRQWERTYKeyboardConstants.symbolKeysPadExpanded - - allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) + if DeviceType.isPhone { + letterKeys = FRQWERTYKeyboardConstants.letterKeysPhone + numberKeys = FRQWERTYKeyboardConstants.numberKeysPhone + symbolKeys = FRQWERTYKeyboardConstants.symbolKeysPhone + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + + leftKeyChars = ["a", "q", "1", "-", "[", "_"] + rightKeyChars = ["p", "m", "0", "\"", "=", "·"] + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } } else { - letterKeys = FRQWERTYKeyboardConstants.letterKeysPad - numberKeys = FRQWERTYKeyboardConstants.numberKeysPad - symbolKeys = FRQWERTYKeyboardConstants.symbolKeysPad - - letterKeys.removeFirst(1) - - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + // Use the expanded keys layout if the iPad is wide enough and has no home button. + if usingExpandedKeyboard { + letterKeys = FRQWERTYKeyboardConstants.letterKeysPadExpanded + symbolKeys = FRQWERTYKeyboardConstants.symbolKeysPadExpanded + + allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) + } else { + letterKeys = FRQWERTYKeyboardConstants.letterKeysPad + numberKeys = FRQWERTYKeyboardConstants.numberKeysPad + symbolKeys = FRQWERTYKeyboardConstants.symbolKeysPad + + letterKeys.removeFirst(1) + + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + } + + leftKeyChars = ["q", "a", "1", "@", "~"] + rightKeyChars = [] + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } } - leftKeyChars = ["q", "a", "1", "@", "~"] - rightKeyChars = [] - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } - - keysWithAlternates = FRQWERTYKeyboardConstants.keysWithAlternates - keysWithAlternatesLeft = FRQWERTYKeyboardConstants.keysWithAlternatesLeft - keysWithAlternatesRight = FRQWERTYKeyboardConstants.keysWithAlternatesRight - aAlternateKeys = FRQWERTYKeyboardConstants.aAlternateKeys - eAlternateKeys = FRQWERTYKeyboardConstants.eAlternateKeys - iAlternateKeys = FRQWERTYKeyboardConstants.iAlternateKeys - oAlternateKeys = FRQWERTYKeyboardConstants.oAlternateKeys - uAlternateKeys = FRQWERTYKeyboardConstants.uAlternateKeys - yAlternateKeys = FRQWERTYKeyboardConstants.yAlternateKeys - cAlternateKeys = FRQWERTYKeyboardConstants.cAlternateKeys - nAlternateKeys = FRQWERTYKeyboardConstants.nAlternateKeys + keysWithAlternates = FRQWERTYKeyboardConstants.keysWithAlternates + keysWithAlternatesLeft = FRQWERTYKeyboardConstants.keysWithAlternatesLeft + keysWithAlternatesRight = FRQWERTYKeyboardConstants.keysWithAlternatesRight + aAlternateKeys = FRQWERTYKeyboardConstants.aAlternateKeys + eAlternateKeys = FRQWERTYKeyboardConstants.eAlternateKeys + iAlternateKeys = FRQWERTYKeyboardConstants.iAlternateKeys + oAlternateKeys = FRQWERTYKeyboardConstants.oAlternateKeys + uAlternateKeys = FRQWERTYKeyboardConstants.uAlternateKeys + yAlternateKeys = FRQWERTYKeyboardConstants.yAlternateKeys + cAlternateKeys = FRQWERTYKeyboardConstants.cAlternateKeys + nAlternateKeys = FRQWERTYKeyboardConstants.nAlternateKeys } diff --git a/Keyboards/LanguageKeyboards/French/FRKeyboardViewController.swift b/Keyboards/LanguageKeyboards/French/FRKeyboardViewController.swift index 5e23d37e..703d7ca8 100644 --- a/Keyboards/LanguageKeyboards/French/FRKeyboardViewController.swift +++ b/Keyboards/LanguageKeyboards/French/FRKeyboardViewController.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Class for the French Scribe keyboard. */ diff --git a/Keyboards/LanguageKeyboards/German/DEInterfaceVariables.swift b/Keyboards/LanguageKeyboards/German/DEInterfaceVariables.swift index 6f875c74..de063eb9 100644 --- a/Keyboards/LanguageKeyboards/German/DEInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/German/DEInterfaceVariables.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Constants and functions to load the German Scribe keyboard. */ @@ -9,307 +9,351 @@ import UIKit // MARK: Constants public enum DEKeyboardConstants { - static let defaultCurrencyKey = "€" - static let currencyKeys = ["€", "$", "£", "¥"] - - // Alternate key vars. - static let keysWithAlternates = ["a", "e", "i", "o", "u", "y", "c", "l", "n", "s", "z"] - static let keysWithAlternatesLeft = ["a", "e", "y", "c", "s", "z"] - static let keysWithAlternatesRight = ["i", "o", "u", "l", "n"] - - static let aAlternateKeys = ["à", "á", "â", "æ", "ã", "å", "ā", "ą"] - static let aAlternateKeysDisableAccents = ["à", "á", "â", "æ", "ã", "å", "ā", "ą", "ä"] - static let eAlternateKeys = ["é", "è", "ê", "ë", "ė", "ę"] - static let iAlternateKeys = ["ì", "ī", "í", "î", "ï"] - static let oAlternateKeys = ["ō", "ø", "œ", "õ", "ó", "ò", "ô"] - static let oAlternateKeysDisableAccents = ["ō", "ø", "œ", "õ", "ó", "ò", "ô", "ö"] - static let uAlternateKeys = ["ū", "ú", "ù", "û"] - static let uAlternateKeysDisableAccents = ["ū", "ú", "ù", "û", "ü"] - static let yAlternateKeys = ["ÿ"] - static let cAlternateKeys = ["ç", "ć", "č"] - static let lAlternateKeys = ["ł"] - static let nAlternateKeys = ["ń", "ñ"] - static let sAlternateKeys = ["ß", "ś", "š"] - static let zAlternateKeys = ["ź", "ż"] + static let defaultCurrencyKey = "€" + static let currencyKeys = ["€", "$", "£", "¥"] + + // Alternate key vars. + static let keysWithAlternates = ["a", "e", "i", "o", "u", "y", "c", "l", "n", "s", "z"] + static let keysWithAlternatesLeft = ["a", "e", "y", "c", "s", "z"] + static let keysWithAlternatesRight = ["i", "o", "u", "l", "n"] + + static let aAlternateKeys = ["à", "á", "â", "æ", "ã", "å", "ā", "ą"] + static let aAlternateKeysDisableAccents = ["à", "á", "â", "æ", "ã", "å", "ā", "ą", "ä"] + static let eAlternateKeys = ["é", "è", "ê", "ë", "ė", "ę"] + static let iAlternateKeys = ["ì", "ī", "í", "î", "ï"] + static let oAlternateKeys = ["ō", "ø", "œ", "õ", "ó", "ò", "ô"] + static let oAlternateKeysDisableAccents = ["ō", "ø", "œ", "õ", "ó", "ò", "ô", "ö"] + static let uAlternateKeys = ["ū", "ú", "ù", "û"] + static let uAlternateKeysDisableAccents = ["ū", "ú", "ù", "û", "ü"] + static let yAlternateKeys = ["ÿ"] + static let cAlternateKeys = ["ç", "ć", "č"] + static let lAlternateKeys = ["ł"] + static let nAlternateKeys = ["ń", "ñ"] + static let sAlternateKeys = ["ß", "ś", "š"] + static let zAlternateKeys = ["ź", "ż"] } struct DEKeyboardProvider: KeyboardProviderProtocol, KeyboardProviderDisableAccentsProtocol { - // MARK: iPhone Layouts - - static func genPhoneLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "ü"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä"]) - .addRow(["shift", "y", "x", "c", "v", "b", "n", "m", "delete"]) - .addRow(["123", "selectKeyboard", "space", "return"]) - .build() - } - - static func genPhoneDisableAccentsLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["q", "w", "e", "r", "t", "z", "u", "i", "o", "p"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l"]) - .addRow(["shift", "y", "x", "c", "v", "b", "n", "m", "delete"]) - .addRow(["123", "selectKeyboard", "space", "return"]) - .build() - } - - static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) - .addRow(["-", "/", ":", ";", "(", ")", "€", "&", "@", "\""]) - .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - .replaceKey(row: 1, column: 6, to: currencyKey) - .build() - } - - static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) - .addRow(["_", "\\", "|", "~", "<", ">", "$", "£", "¥", "·"]) - .addRow(["123", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 6, to: currencyKeys[0]) - .replaceKey(row: 1, column: 7, to: currencyKeys[1]) - .replaceKey(row: 1, column: 8, to: currencyKeys[2]) - .build() + // MARK: iPhone Layouts + + static func genPhoneLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "ü"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä"]) + .addRow(["shift", "y", "x", "c", "v", "b", "n", "m", "delete"]) + .addRow(["123", "selectKeyboard", "space", "return"]) + .build() } - } - - // MARK: iPad Layouts - - static func genPadLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) - .addRow(["q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "ü", "delete"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä", "return"]) - .addRow(["shift", "y", "x", "c", "v", "b", "n", "m", ",", ".", "ß", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadDisableAccentsLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) - .addRow(["q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "delete"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "return"]) - .addRow(["shift", "y", "x", "c", "v", "b", "n", "m", ",", ".", "ß", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "+", "delete"]) - .addRow(["\"", "§", "€", "%", "&", "/", "(", ")", "=", "'", "#", "return"]) - .addRow(["#+=", "—", "`", "'", "...", "@", ";", ":", ",", ".", "-", "#+="]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .replaceKey(row: 1, column: 2, to: currencyKey) - .build() - } - - static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "*", "delete"]) - .addRow(["$", "£", "¥", "¿", "―", "\\", "[", "]", "{", "}", "|", "return"]) - .addRow(["123", "¡", "<", ">", "≠", "·", "^", "~", "!", "?", "_", "123"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 0, to: currencyKeys[0]) - .replaceKey(row: 1, column: 1, to: currencyKeys[1]) - .replaceKey(row: 1, column: 2, to: currencyKeys[2]) - .build() + + static func genPhoneDisableAccentsLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["q", "w", "e", "r", "t", "z", "u", "i", "o", "p"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l"]) + .addRow(["shift", "y", "x", "c", "v", "b", "n", "m", "delete"]) + .addRow(["123", "selectKeyboard", "space", "return"]) + .build() + } + + static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) + .addRow(["-", "/", ":", ";", "(", ")", "€", "&", "@", "\""]) + .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + .replaceKey(row: 1, column: 6, to: currencyKey) + .build() + } + + static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) + .addRow(["_", "\\", "|", "~", "<", ">", "$", "£", "¥", "·"]) + .addRow(["123", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 6, to: currencyKeys[0]) + .replaceKey(row: 1, column: 7, to: currencyKeys[1]) + .replaceKey(row: 1, column: 8, to: currencyKeys[2]) + .build() + } + } + + // MARK: iPad Layouts + + static func genPadLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) + .addRow(["q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "ü", "delete"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä", "return"]) + .addRow(["shift", "y", "x", "c", "v", "b", "n", "m", ",", ".", "ß", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadDisableAccentsLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) + .addRow(["q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "delete"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "return"]) + .addRow(["shift", "y", "x", "c", "v", "b", "n", "m", ",", ".", "ß", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "+", "delete"]) + .addRow(["\"", "§", "€", "%", "&", "/", "(", ")", "=", "'", "#", "return"]) + .addRow(["#+=", "—", "`", "'", "...", "@", ";", ":", ",", ".", "-", "#+="]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .replaceKey(row: 1, column: 2, to: currencyKey) + .build() + } + + static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "*", "delete"]) + .addRow(["$", "£", "¥", "¿", "―", "\\", "[", "]", "{", "}", "|", "return"]) + .addRow(["123", "¡", "<", ">", "≠", "·", "^", "~", "!", "?", "_", "123"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 0, to: currencyKeys[0]) + .replaceKey(row: 1, column: 1, to: currencyKeys[1]) + .replaceKey(row: 1, column: 2, to: currencyKeys[2]) + .build() + } + } + + // MARK: Expanded iPad Layouts + + static func genPadExpandedLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["^", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "ß", "´", "delete"]) + .addRow([ + SpecialKeys.indent, "q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "ü", "+", "*" + ]) + .addRow([ + SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä", "#", + "return" + ]) + .addRow(["shift", "'", "y", "x", "c", "v", "b", "n", "m", "-", ",", ".", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadExpandedDisableAccentsLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["^", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "ß", "´", "delete"]) + .addRow([ + SpecialKeys.indent, "q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "=", "+", "*" + ]) + .addRow([ + SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "/", "@", "#", + "return" + ]) + .addRow(["shift", "'", "y", "x", "c", "v", "b", "n", "m", "-", ",", ".", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadExpandedSymbolKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) + .addRow([ + SpecialKeys.indent, "\"", "|", "§", "[", "]", "{", "}", "—", "%", "^", "=", "+", + "*" + ]) + .addRow([ + SpecialKeys.capsLock, ":", ";", "(", ")", "&", "$", "£", "¥", "€", "/", "@", "#", + "return" + ]) + .addRow(["shift", "'", "?", "!", "~", "≠", "°", "…", "_", "-", ",", ".", "shift"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .build() } - } - - // MARK: Expanded iPad Layouts - - static func genPadExpandedLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["^", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "ß", "´", "delete"]) - .addRow([SpecialKeys.indent, "q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "ü", "+", "*"]) - .addRow([SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä", "#", "return"]) - .addRow(["shift", "'", "y", "x", "c", "v", "b", "n", "m", "-", ",", ".", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadExpandedDisableAccentsLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["^", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "ß", "´", "delete"]) - .addRow([SpecialKeys.indent, "q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "=", "+", "*"]) - .addRow([SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "/", "@", "#", "return"]) - .addRow(["shift", "'", "y", "x", "c", "v", "b", "n", "m", "-", ",", ".", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadExpandedSymbolKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) - .addRow([SpecialKeys.indent, "\"", "|", "§", "[", "]", "{", "}", "—", "%", "^", "=", "+", "*"]) - .addRow([SpecialKeys.capsLock, ":", ";", "(", ")", "&", "$", "£", "¥", "€", "/", "@", "#", "return"]) - .addRow(["shift", "'", "?", "!", "~", "≠", "°", "…", "_", "-", ",", ".", "shift"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .build() - } } // MARK: Get Keys func getDEKeys() { - guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") else { - fatalError("Unable to access shared user defaults") - } - - var currencyKey = DEKeyboardConstants.defaultCurrencyKey - var currencyKeys = DEKeyboardConstants.currencyKeys - let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" - if let currencyValue = userDefaults.string(forKey: dictionaryKey) { - currencyKey = currencyValue - } else { - userDefaults.setValue(currencyKey, forKey: dictionaryKey) - } - if let index = currencyKeys.firstIndex(of: currencyKey) { - currencyKeys.remove(at: index) - } - - if DeviceType.isPhone { - if userDefaults.bool(forKey: "deAccentCharacters") { - letterKeys = DEKeyboardProvider.genPhoneDisableAccentsLetterKeys() - } else { - letterKeys = DEKeyboardProvider.genPhoneLetterKeys() + guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + else { + fatalError("Unable to access shared user defaults") } - numberKeys = DEKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) - symbolKeys = DEKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) - leftKeyChars = ["q", "a", "1", "-", "[", "_"] - if userDefaults.bool(forKey: "deAccentCharacters") { - rightKeyChars = ["p", "l", "0", "\"", "=", "·"] + var currencyKey = DEKeyboardConstants.defaultCurrencyKey + var currencyKeys = DEKeyboardConstants.currencyKeys + let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" + if let currencyValue = userDefaults.string(forKey: dictionaryKey) { + currencyKey = currencyValue } else { - rightKeyChars = ["ü", "ä", "0", "\"", "=", "·"] + userDefaults.setValue(currencyKey, forKey: dictionaryKey) + } + if let index = currencyKeys.firstIndex(of: currencyKey) { + currencyKeys.remove(at: index) } - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } else { - // Use the expanded keys layout if the iPad is wide enough and has no home button. - if usingExpandedKeyboard { - if userDefaults.bool(forKey: "deAccentCharacters") { - letterKeys = DEKeyboardProvider.genPadExpandedDisableAccentsLetterKeys() - } else { - letterKeys = DEKeyboardProvider.genPadExpandedLetterKeys() - } - symbolKeys = DEKeyboardProvider.genPadExpandedSymbolKeys() - leftKeyChars = ["^", "`"] - rightKeyChars = ["*"] - allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) + + if DeviceType.isPhone { + if userDefaults.bool(forKey: "deAccentCharacters") { + letterKeys = DEKeyboardProvider.genPhoneDisableAccentsLetterKeys() + } else { + letterKeys = DEKeyboardProvider.genPhoneLetterKeys() + } + numberKeys = DEKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) + symbolKeys = DEKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + + leftKeyChars = ["q", "a", "1", "-", "[", "_"] + if userDefaults.bool(forKey: "deAccentCharacters") { + rightKeyChars = ["p", "l", "0", "\"", "=", "·"] + } else { + rightKeyChars = ["ü", "ä", "0", "\"", "=", "·"] + } + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } } else { - if userDefaults.bool(forKey: "deAccentCharacters") { - letterKeys = DEKeyboardProvider.genPadDisableAccentsLetterKeys() - } else { - letterKeys = DEKeyboardProvider.genPadLetterKeys() - } - numberKeys = DEKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) - symbolKeys = DEKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) - - letterKeys.removeFirst(1) - leftKeyChars = ["q", "a", "1", "\"", "$"] - rightKeyChars = [] - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + // Use the expanded keys layout if the iPad is wide enough and has no home button. + if usingExpandedKeyboard { + if userDefaults.bool(forKey: "deAccentCharacters") { + letterKeys = DEKeyboardProvider.genPadExpandedDisableAccentsLetterKeys() + } else { + letterKeys = DEKeyboardProvider.genPadExpandedLetterKeys() + } + symbolKeys = DEKeyboardProvider.genPadExpandedSymbolKeys() + leftKeyChars = ["^", "`"] + rightKeyChars = ["*"] + allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) + } else { + if userDefaults.bool(forKey: "deAccentCharacters") { + letterKeys = DEKeyboardProvider.genPadDisableAccentsLetterKeys() + } else { + letterKeys = DEKeyboardProvider.genPadLetterKeys() + } + numberKeys = DEKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) + symbolKeys = DEKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) + + letterKeys.removeFirst(1) + leftKeyChars = ["q", "a", "1", "\"", "$"] + rightKeyChars = [] + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + } + + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } } - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } - - keysWithAlternates = DEKeyboardConstants.keysWithAlternates - keysWithAlternatesLeft = DEKeyboardConstants.keysWithAlternatesLeft - keysWithAlternatesRight = DEKeyboardConstants.keysWithAlternatesRight - eAlternateKeys = DEKeyboardConstants.eAlternateKeys - iAlternateKeys = DEKeyboardConstants.iAlternateKeys - yAlternateKeys = DEKeyboardConstants.yAlternateKeys - sAlternateKeys = DEKeyboardConstants.sAlternateKeys - lAlternateKeys = DEKeyboardConstants.lAlternateKeys - zAlternateKeys = DEKeyboardConstants.zAlternateKeys - cAlternateKeys = DEKeyboardConstants.cAlternateKeys - nAlternateKeys = DEKeyboardConstants.nAlternateKeys - - if userDefaults.bool(forKey: "deAccentCharacters") { - aAlternateKeys = DEKeyboardConstants.aAlternateKeysDisableAccents - oAlternateKeys = DEKeyboardConstants.oAlternateKeysDisableAccents - uAlternateKeys = DEKeyboardConstants.uAlternateKeysDisableAccents - } else { - aAlternateKeys = DEKeyboardConstants.aAlternateKeys - oAlternateKeys = DEKeyboardConstants.oAlternateKeys - uAlternateKeys = DEKeyboardConstants.uAlternateKeys - } + keysWithAlternates = DEKeyboardConstants.keysWithAlternates + keysWithAlternatesLeft = DEKeyboardConstants.keysWithAlternatesLeft + keysWithAlternatesRight = DEKeyboardConstants.keysWithAlternatesRight + eAlternateKeys = DEKeyboardConstants.eAlternateKeys + iAlternateKeys = DEKeyboardConstants.iAlternateKeys + yAlternateKeys = DEKeyboardConstants.yAlternateKeys + sAlternateKeys = DEKeyboardConstants.sAlternateKeys + lAlternateKeys = DEKeyboardConstants.lAlternateKeys + zAlternateKeys = DEKeyboardConstants.zAlternateKeys + cAlternateKeys = DEKeyboardConstants.cAlternateKeys + nAlternateKeys = DEKeyboardConstants.nAlternateKeys + + if userDefaults.bool(forKey: "deAccentCharacters") { + aAlternateKeys = DEKeyboardConstants.aAlternateKeysDisableAccents + oAlternateKeys = DEKeyboardConstants.oAlternateKeysDisableAccents + uAlternateKeys = DEKeyboardConstants.uAlternateKeysDisableAccents + } else { + aAlternateKeys = DEKeyboardConstants.aAlternateKeys + oAlternateKeys = DEKeyboardConstants.oAlternateKeys + uAlternateKeys = DEKeyboardConstants.uAlternateKeys + } } // MARK: Provide Layout func setDEKeyboardLayout() { - getDEKeys() - - currencySymbol = "€" - currencySymbolAlternates = euroAlternateKeys - spaceBar = "Leerzeichen" - language = "Deutsch" - - invalidCommandMsgWikidata = "Nicht in Wikidata" - invalidCommandTextWikidata1 = "Wikidata ist ein kollaborativ gestalteter, mehrsprachiger Wissensgraf, der von der Wikimedia Foundation gehostet wird. Sie dient als Quelle für offene Daten für unzählige Projekte, beispielsweise Wikipedia." - invalidCommandTextWikidata2 = "Scribe nutzt Sprachdaten von Wikidata für viele Kernfunktionen. Von dort erhalten wir Informationen wie Genera, Verbkonjugationen und viele mehr!" - invalidCommandTextWikidata3 = "Du kannst auf wikidata.org einen Account erstellen, um der Community, die Scribe und viele andere Projekte unterstützt, beizutreten. Hilf uns dabei, der Welt freie Informationen zu geben!" - - invalidCommandMsgWiktionary = "Nicht in Wiktionary" - invalidCommandTextWiktionary1 = "Wiktionary ist ein gemeinschaftlich bearbeitetes Wörterbuch, das von der Wikimedia Foundation gepflegt wird. Es dient als Quelle freier Sprachdaten für Projekte wie Wikipedia und unzählige andere." - invalidCommandTextWiktionary2 = "Scribe verwendet die Daten von Wiktionary, um Übersetzungen für den Befehl „Übersetzen“ bereitzustellen. Unsere Daten stammen aus den vielen Sprachpaaren, die die Wiktionary-Community erstellt hat!" - invalidCommandTextWiktionary3 = "Erstellen Sie ein Konto auf wiktionary.org, um der Community beizutreten, die Scribe und viele andere Projekte unterstützt. Helfen Sie uns, freie Informationen in die Welt zu bringen!" - - baseAutosuggestions = ["ich", "die", "das"] - numericAutosuggestions = ["Prozent", "Milionen", "Meter"] - verbsAfterPronounsArray = ["haben", "sein", "können"] - pronounAutosuggestionTenses = [ - "ich": "presFPS", - "du": "presSPS", - "er": "presTPS", - "sie": "presTPS", - "es": "presTPS", - "wir": "presFPP", - "ihr": "presSPP", - "Sie": "presTPP" - ] - - translateKeyLbl = "Übersetzen" - translatePlaceholder = "Wort eingeben" - translatePrompt = commandPromptSpacing + "de -› \(getControllerLanguageAbbr()): " - translatePromptAndCursor = translatePrompt + commandCursor - translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder - translatePromptAndColorPlaceholder = NSMutableAttributedString(string: translatePromptAndPlaceholder) - translatePromptAndColorPlaceholder.setColorForText(textForAttribute: translatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - conjugateKeyLbl = "Konjugieren" - conjugatePlaceholder = "Verb eingeben" - conjugatePrompt = commandPromptSpacing + "Konjugieren: " - conjugatePromptAndCursor = conjugatePrompt + commandCursor - conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder - conjugatePromptAndColorPlaceholder = NSMutableAttributedString(string: conjugatePromptAndPlaceholder) - conjugatePromptAndColorPlaceholder.setColorForText(textForAttribute: conjugatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - pluralKeyLbl = "Plural" - pluralPlaceholder = "Nomen eingeben" - pluralPrompt = commandPromptSpacing + "Plural: " - pluralPromptAndCursor = pluralPrompt + commandCursor - pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder - pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) - pluralPromptAndColorPlaceholder.setColorForText(textForAttribute: pluralPlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - alreadyPluralMsg = "Schon Plural" + getDEKeys() + + currencySymbol = "€" + currencySymbolAlternates = euroAlternateKeys + spaceBar = "Leerzeichen" + language = "Deutsch" + + invalidCommandMsgWikidata = "Nicht in Wikidata" + invalidCommandTextWikidata1 = + "Wikidata ist ein kollaborativ gestalteter, mehrsprachiger Wissensgraf, der von der Wikimedia Foundation gehostet wird. Sie dient als Quelle für offene Daten für unzählige Projekte, beispielsweise Wikipedia." + invalidCommandTextWikidata2 = + "Scribe nutzt Sprachdaten von Wikidata für viele Kernfunktionen. Von dort erhalten wir Informationen wie Genera, Verbkonjugationen und viele mehr!" + invalidCommandTextWikidata3 = + "Du kannst auf wikidata.org einen Account erstellen, um der Community, die Scribe und viele andere Projekte unterstützt, beizutreten. Hilf uns dabei, der Welt freie Informationen zu geben!" + + invalidCommandMsgWiktionary = "Nicht in Wiktionary" + invalidCommandTextWiktionary1 = + "Wiktionary ist ein gemeinschaftlich bearbeitetes Wörterbuch, das von der Wikimedia Foundation gepflegt wird. Es dient als Quelle freier Sprachdaten für Projekte wie Wikipedia und unzählige andere." + invalidCommandTextWiktionary2 = + "Scribe verwendet die Daten von Wiktionary, um Übersetzungen für den Befehl „Übersetzen“ bereitzustellen. Unsere Daten stammen aus den vielen Sprachpaaren, die die Wiktionary-Community erstellt hat!" + invalidCommandTextWiktionary3 = + "Erstellen Sie ein Konto auf wiktionary.org, um der Community beizutreten, die Scribe und viele andere Projekte unterstützt. Helfen Sie uns, freie Informationen in die Welt zu bringen!" + + baseAutosuggestions = ["ich", "die", "das"] + numericAutosuggestions = ["Prozent", "Milionen", "Meter"] + verbsAfterPronounsArray = ["haben", "sein", "können"] + pronounAutosuggestionTenses = [ + "ich": "presFPS", + "du": "presSPS", + "er": "presTPS", + "sie": "presTPS", + "es": "presTPS", + "wir": "presFPP", + "ihr": "presSPP", + "Sie": "presTPP" + ] + + translateKeyLbl = "Übersetzen" + translatePlaceholder = "Wort eingeben" + translatePrompt = commandPromptSpacing + "de -› \(getControllerLanguageAbbr()): " + translatePromptAndCursor = translatePrompt + commandCursor + translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder + translatePromptAndColorPlaceholder = NSMutableAttributedString( + string: translatePromptAndPlaceholder + ) + translatePromptAndColorPlaceholder.setColorForText( + textForAttribute: translatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + conjugateKeyLbl = "Konjugieren" + conjugatePlaceholder = "Verb eingeben" + conjugatePrompt = commandPromptSpacing + "Konjugieren: " + conjugatePromptAndCursor = conjugatePrompt + commandCursor + conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder + conjugatePromptAndColorPlaceholder = NSMutableAttributedString( + string: conjugatePromptAndPlaceholder + ) + conjugatePromptAndColorPlaceholder.setColorForText( + textForAttribute: conjugatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + pluralKeyLbl = "Plural" + pluralPlaceholder = "Nomen eingeben" + pluralPrompt = commandPromptSpacing + "Plural: " + pluralPromptAndCursor = pluralPrompt + commandCursor + pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder + pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) + pluralPromptAndColorPlaceholder.setColorForText( + textForAttribute: pluralPlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + alreadyPluralMsg = "Schon Plural" } diff --git a/Keyboards/LanguageKeyboards/German/DEKeyboardViewController.swift b/Keyboards/LanguageKeyboards/German/DEKeyboardViewController.swift index eb20b6b5..e1144347 100644 --- a/Keyboards/LanguageKeyboards/German/DEKeyboardViewController.swift +++ b/Keyboards/LanguageKeyboards/German/DEKeyboardViewController.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Class for the German Scribe keyboard. */ diff --git a/Keyboards/LanguageKeyboards/Hebrew/HEInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Hebrew/HEInterfaceVariables.swift index 9fada892..3e3f6319 100644 --- a/Keyboards/LanguageKeyboards/Hebrew/HEInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Hebrew/HEInterfaceVariables.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Constants and functions to load the Hebrew Scribe keyboard. */ @@ -9,210 +9,249 @@ import UIKit // MARK: Constants public enum HEKeyboardConstants { - static let defaultCurrencyKey = "₪" - static let currencyKeys = ["₪", "€", "£", "$"] + static let defaultCurrencyKey = "₪" + static let currencyKeys = ["₪", "€", "£", "$"] } struct HEKeyboardProvider: KeyboardProviderProtocol { - // MARK: iPhone Layouts - - static func genPhoneLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["פ", "ם", "ן", "ו", "ט", "א", "ר", "ק", "delete"]) - .addRow(["ף", "ך", "ל", "ח", "י", "ע", "כ", "ג", "ד", "ש"]) - .addRow(["ץ", "ת", "צ", "מ", "נ", "ה", "ב", "ס", "ז"]) - .addRow(["123", "selectKeyboard", "space", "return"]) - .build() - } - - static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) - .addRow(["-", "/", ":", ";", "(", ")", "$", "&", "@", "\""]) - .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) - .addRow(["אבג", "selectKeyboard", "space", "return"]) - .replaceKey(row: 1, column: 6, to: currencyKey) - .build() - } - - static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) - .addRow(["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "·"]) - .addRow(["123", ".", ",", "?", "!", "'", "delete"]) - .addRow(["אבג", "selectKeyboard", "space", "return"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 6, to: currencyKeys[0]) - .replaceKey(row: 1, column: 7, to: currencyKeys[1]) - .replaceKey(row: 1, column: 8, to: currencyKeys[2]) - .build() + // MARK: iPhone Layouts + + static func genPhoneLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["פ", "ם", "ן", "ו", "ט", "א", "ר", "ק", "delete"]) + .addRow(["ף", "ך", "ל", "ח", "י", "ע", "כ", "ג", "ד", "ש"]) + .addRow(["ץ", "ת", "צ", "מ", "נ", "ה", "ב", "ס", "ז"]) + .addRow(["123", "selectKeyboard", "space", "return"]) + .build() } - } - - // MARK: iPad Layouts - - static func genPadLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) - .addRow([",", ".", "פ", "ם", "ן", "ו", "ט", "א", "ר", "ק", "delete"]) - .addRow(["ף", "ך", "ל", "ח", "י", "ע", "כ", "ג", "ד", "ש"]) - .addRow(["ץ", "ת", "צ", "מ", "נ", "ה", "ב", "ס", "ז", "return"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) - .addRow(["!", "@", "#", "&", "_", "-", "'", "\"", "(", ")", "return"]) - .addRow(["#+=", "%", "...", "&", ";", ":", "=", "+", "/", "?", "#+="]) - .addRow(["selectKeyboard", "אבג", "space", "אבג", "hideKeyboard"]) - // .replaceKey(row: 1, column: 4, to: currencyKey) - .build() - } - - static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "*", "delete"]) - .addRow(["^", "€", "$", "£", "[", "]", "'", "\"", "<", ">", "return"]) - .addRow(["123", "§", "|", "~", "*", "·", "{", "}", "\\", "~", "123"]) - .addRow(["selectKeyboard", "אבג", "space", "אבג", "hideKeyboard"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 1, to: currencyKeys[0]) - .replaceKey(row: 1, column: 2, to: currencyKeys[1]) - .replaceKey(row: 1, column: 3, to: currencyKeys[2]) - .build() + + static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) + .addRow(["-", "/", ":", ";", "(", ")", "$", "&", "@", "\""]) + .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) + .addRow(["אבג", "selectKeyboard", "space", "return"]) + .replaceKey(row: 1, column: 6, to: currencyKey) + .build() + } + + static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) + .addRow(["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "·"]) + .addRow(["123", ".", ",", "?", "!", "'", "delete"]) + .addRow(["אבג", "selectKeyboard", "space", "return"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 6, to: currencyKeys[0]) + .replaceKey(row: 1, column: 7, to: currencyKeys[1]) + .replaceKey(row: 1, column: 8, to: currencyKeys[2]) + .build() + } + } + + // MARK: iPad Layouts + + static func genPadLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) + .addRow([",", ".", "פ", "ם", "ן", "ו", "ט", "א", "ר", "ק", "delete"]) + .addRow(["ף", "ך", "ל", "ח", "י", "ע", "כ", "ג", "ד", "ש"]) + .addRow(["ץ", "ת", "צ", "מ", "נ", "ה", "ב", "ס", "ז", "return"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadNumberKeys(currencyKey _: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) + .addRow(["!", "@", "#", "&", "_", "-", "'", "\"", "(", ")", "return"]) + .addRow(["#+=", "%", "...", "&", ";", ":", "=", "+", "/", "?", "#+="]) + .addRow(["selectKeyboard", "אבג", "space", "אבג", "hideKeyboard"]) + // .replaceKey(row: 1, column: 4, to: currencyKey) + .build() + } + + static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "*", "delete"]) + .addRow(["^", "€", "$", "£", "[", "]", "'", "\"", "<", ">", "return"]) + .addRow(["123", "§", "|", "~", "*", "·", "{", "}", "\\", "~", "123"]) + .addRow(["selectKeyboard", "אבג", "space", "אבג", "hideKeyboard"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 1, to: currencyKeys[0]) + .replaceKey(row: 1, column: 2, to: currencyKeys[1]) + .replaceKey(row: 1, column: 3, to: currencyKeys[2]) + .build() + } + } + + // MARK: Expanded iPad Layouts + + static func genPadExpandedLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["§", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "delete"]) + .addRow([ + SpecialKeys.indent, "/", "'", "פ", "ם", "ן", "ו", "ט", "א", "ר", "ק", "[", "]", "+" + ]) + .addRow([ + SpecialKeys.capsLock, "ף", "ך", "ל", "ח", "י", "ע", "כ", "ג", "ד", "ש", ",", "\"", + "return" + ]) + .addRow(["⇧", ";", "ץ", "ת", "צ", "מ", "נ", "ה", "ב", "ס", "ז", ".", "⇧"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadExpandedSymbolKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) + .addRow([ + SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "\"", "|", + "—" + ]) + .addRow([ + SpecialKeys.capsLock, "°", "/", ":", ";", "(", ")", "$", "&", "@", "£", "¥", "€", + "return" + ]) + .addRow(["⇧", "…", "?", "!", "~", "≠", "'", "\"", "_", ",", ".", "-", "⇧"]) + .addRow(["selectKeyboard", "אבג", "space", "אבג", "hideKeyboard"]) + .build() } - } - - // MARK: Expanded iPad Layouts - - static func genPadExpandedLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["§", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "delete"]) - .addRow([SpecialKeys.indent, "/", "'", "פ", "ם", "ן", "ו", "ט", "א", "ר", "ק", "[", "]", "+"]) - .addRow([SpecialKeys.capsLock, "ף", "ך", "ל", "ח", "י", "ע", "כ", "ג", "ד", "ש", ",", "\"", "return"]) - .addRow(["⇧", ";", "ץ", "ת", "צ", "מ", "נ", "ה", "ב", "ס", "ז", ".", "⇧"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadExpandedSymbolKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) - .addRow([SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "\"", "|", "—"]) - .addRow([SpecialKeys.capsLock, "°", "/", ":", ";", "(", ")", "$", "&", "@", "£", "¥", "€", "return"]) - .addRow(["⇧", "…", "?", "!", "~", "≠", "'", "\"", "_", ",", ".", "-", "⇧"]) - .addRow(["selectKeyboard", "אבג", "space", "אבג", "hideKeyboard"]) - .build() - } } // MARK: Get Keys func getHEKeys() { - guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") else { - fatalError("Unable to access shared user defaults") - } - - var currencyKey = HEKeyboardConstants.defaultCurrencyKey - var currencyKeys = HEKeyboardConstants.currencyKeys - let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" - - if let currencyValue = userDefaults.string(forKey: dictionaryKey) { - currencyKey = currencyValue - } else { - userDefaults.setValue(currencyKey, forKey: dictionaryKey) - } - if let index = currencyKeys.firstIndex(of: currencyKey) { - currencyKeys.remove(at: index) - } - - if DeviceType.isPhone { - letterKeys = HEKeyboardProvider.genPhoneLetterKeys() - numberKeys = HEKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) - symbolKeys = HEKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) - - leftKeyChars = ["ק", "1", "-", "[", "_"] - rightKeyChars = ["פ", "0", "\"", "=", "·"] - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } else { - // Use the expanded keys layout if the iPad is wide enough and has no home button. - if usingExpandedKeyboard { - letterKeys = HEKeyboardProvider.genPadExpandedLetterKeys() - symbolKeys = HEKeyboardProvider.genPadExpandedSymbolKeys() - - allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) - } else { - letterKeys = HEKeyboardProvider.genPadLetterKeys() - numberKeys = HEKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) - symbolKeys = HEKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) + guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + else { + fatalError("Unable to access shared user defaults") + } - letterKeys.removeFirst(1) + var currencyKey = HEKeyboardConstants.defaultCurrencyKey + var currencyKeys = HEKeyboardConstants.currencyKeys + let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + if let currencyValue = userDefaults.string(forKey: dictionaryKey) { + currencyKey = currencyValue + } else { + userDefaults.setValue(currencyKey, forKey: dictionaryKey) + } + if let index = currencyKeys.firstIndex(of: currencyKey) { + currencyKeys.remove(at: index) } - leftKeyChars = ["1", "ק"] - rightKeyChars = [] - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } + if DeviceType.isPhone { + letterKeys = HEKeyboardProvider.genPhoneLetterKeys() + numberKeys = HEKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) + symbolKeys = HEKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + + leftKeyChars = ["ק", "1", "-", "[", "_"] + rightKeyChars = ["פ", "0", "\"", "=", "·"] + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } + } else { + // Use the expanded keys layout if the iPad is wide enough and has no home button. + if usingExpandedKeyboard { + letterKeys = HEKeyboardProvider.genPadExpandedLetterKeys() + symbolKeys = HEKeyboardProvider.genPadExpandedSymbolKeys() + + allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) + } else { + letterKeys = HEKeyboardProvider.genPadLetterKeys() + numberKeys = HEKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) + symbolKeys = HEKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) + + letterKeys.removeFirst(1) + + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + } + + leftKeyChars = ["1", "ק"] + rightKeyChars = [] + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } + } } // MARK: Provide Layout func setHEKeyboardLayout() { - getHEKeys() - - currencySymbol = "" - currencySymbolAlternates = roubleAlternateKeys - spaceBar = "רווח" - language = "עברית" - - invalidCommandMsgWikidata = "אין מידע" - invalidCommandTextWikidata1 = "ויקידאטים הוא גרף ידע שנערך בשיתוף פעולה ומתוחזק על ידי קרן ויקימדיה. הוא משמש כמקור לנתונים פתוחים עבור פרויקטים כמו ויקיפדיה ועוד רבים אחרים." - invalidCommandTextWikidata2 = "סקריב משתמש בנתוני השפה של ויקידאטים עבור רבים מתכונות הליבה שלו. אנו מקבלים מידע כמו מגדרים של שמות עצם, צימוד פעלים ועוד!" - invalidCommandTextWikidata3 = "ניתן ליצור חשבון ב-wikidata.org כדי להצטרף לקהילה התומכת בסקריב ובפרויקטים רבים אחרים. עזרו לנו להביא מידע חופשי לעולם!" - - invalidCommandMsgWiktionary = "אין מידע" - invalidCommandTextWiktionary1 = "ויקימיילון הוא מילון שנערך בשיתוף פעולה ומתוחזק על ידי קרן ויקימדיה. הוא משמש כמקור לנתונים לשוניים בחינם עבור פרויקטים כמו ויקיפדיה ועוד רבים אחרים." - invalidCommandTextWiktionary2 = "סקריב משתמש בנתוני ויקימיילון כדי לספק תרגומים לפקודת התרגום שלו. הנתונים שלנו נגזרים מזוגות השפות הרבים שיצרה קהילת ויקימיילון!" - invalidCommandTextWiktionary3 = "ניתן ליצור חשבון ב-wiktionary.org כדי להצטרף לקהילה התומכת בסקריב ובפרויקטים רבים אחרים. עזרו לנו להביא מידע חופשי לעולם!" - - baseAutosuggestions = ["אתמ", "אני", "היי"] - numericAutosuggestions = ["", "", ""] - - translateKeyLbl = "לְתַרְגֵם" - translatePlaceholder = "לְתַרְגֵם" - translatePrompt = commandPromptSpacing + "he -› \(getControllerLanguageAbbr()): " - translatePromptAndCursor = translatePrompt + commandCursor - translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder - translatePromptAndColorPlaceholder = NSMutableAttributedString(string: translatePromptAndPlaceholder) - translatePromptAndColorPlaceholder.setColorForText(textForAttribute: translatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - conjugateKeyLbl = "לְהַטוֹת" - conjugatePlaceholder = "לְהַטוֹת" - conjugatePrompt = commandPromptSpacing + " :נְטִיָה" - conjugatePromptAndCursor = conjugatePrompt + commandCursor - conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder - conjugatePromptAndColorPlaceholder = NSMutableAttributedString(string: conjugatePromptAndPlaceholder) - conjugatePromptAndColorPlaceholder.setColorForText(textForAttribute: conjugatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - pluralKeyLbl = "רַבִּים" - pluralPlaceholder = "רַבִּים" - pluralPrompt = commandPromptSpacing + " :רַבִּים" - pluralPromptAndCursor = pluralPrompt + commandCursor - pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder - pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) - pluralPromptAndColorPlaceholder.setColorForText(textForAttribute: pluralPlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - alreadyPluralMsg = "כבר בצורת רבים" + getHEKeys() + + currencySymbol = "" + currencySymbolAlternates = roubleAlternateKeys + spaceBar = "רווח" + language = "עברית" + + invalidCommandMsgWikidata = "אין מידע" + invalidCommandTextWikidata1 = + "ויקידאטים הוא גרף ידע שנערך בשיתוף פעולה ומתוחזק על ידי קרן ויקימדיה. הוא משמש כמקור לנתונים פתוחים עבור פרויקטים כמו ויקיפדיה ועוד רבים אחרים." + invalidCommandTextWikidata2 = + "סקריב משתמש בנתוני השפה של ויקידאטים עבור רבים מתכונות הליבה שלו. אנו מקבלים מידע כמו מגדרים של שמות עצם, צימוד פעלים ועוד!" + invalidCommandTextWikidata3 = + "ניתן ליצור חשבון ב-wikidata.org כדי להצטרף לקהילה התומכת בסקריב ובפרויקטים רבים אחרים. עזרו לנו להביא מידע חופשי לעולם!" + + invalidCommandMsgWiktionary = "אין מידע" + invalidCommandTextWiktionary1 = + "ויקימיילון הוא מילון שנערך בשיתוף פעולה ומתוחזק על ידי קרן ויקימדיה. הוא משמש כמקור לנתונים לשוניים בחינם עבור פרויקטים כמו ויקיפדיה ועוד רבים אחרים." + invalidCommandTextWiktionary2 = + "סקריב משתמש בנתוני ויקימיילון כדי לספק תרגומים לפקודת התרגום שלו. הנתונים שלנו נגזרים מזוגות השפות הרבים שיצרה קהילת ויקימיילון!" + invalidCommandTextWiktionary3 = + "ניתן ליצור חשבון ב-wiktionary.org כדי להצטרף לקהילה התומכת בסקריב ובפרויקטים רבים אחרים. עזרו לנו להביא מידע חופשי לעולם!" + + baseAutosuggestions = ["אתמ", "אני", "היי"] + numericAutosuggestions = ["", "", ""] + + translateKeyLbl = "לְתַרְגֵם" + translatePlaceholder = "לְתַרְגֵם" + translatePrompt = commandPromptSpacing + "he -› \(getControllerLanguageAbbr()): " + translatePromptAndCursor = translatePrompt + commandCursor + translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder + translatePromptAndColorPlaceholder = NSMutableAttributedString( + string: translatePromptAndPlaceholder + ) + translatePromptAndColorPlaceholder.setColorForText( + textForAttribute: translatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + conjugateKeyLbl = "לְהַטוֹת" + conjugatePlaceholder = "לְהַטוֹת" + conjugatePrompt = commandPromptSpacing + " :נְטִיָה" + conjugatePromptAndCursor = conjugatePrompt + commandCursor + conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder + conjugatePromptAndColorPlaceholder = NSMutableAttributedString( + string: conjugatePromptAndPlaceholder + ) + conjugatePromptAndColorPlaceholder.setColorForText( + textForAttribute: conjugatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + pluralKeyLbl = "רַבִּים" + pluralPlaceholder = "רַבִּים" + pluralPrompt = commandPromptSpacing + " :רַבִּים" + pluralPromptAndCursor = pluralPrompt + commandCursor + pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder + pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) + pluralPromptAndColorPlaceholder.setColorForText( + textForAttribute: pluralPlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + alreadyPluralMsg = "כבר בצורת רבים" } diff --git a/Keyboards/LanguageKeyboards/Hebrew/HEKeyboardViewController.swift b/Keyboards/LanguageKeyboards/Hebrew/HEKeyboardViewController.swift index 6f57caa2..5c82c006 100644 --- a/Keyboards/LanguageKeyboards/Hebrew/HEKeyboardViewController.swift +++ b/Keyboards/LanguageKeyboards/Hebrew/HEKeyboardViewController.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Class for the Hebrew Scribe keyboard. */ diff --git a/Keyboards/LanguageKeyboards/Indonesian/IDInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Indonesian/IDInterfaceVariables.swift index 87eff48b..8acfbe95 100644 --- a/Keyboards/LanguageKeyboards/Indonesian/IDInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Indonesian/IDInterfaceVariables.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Constants and functions to load the Indonesian Scribe keyboard. */ @@ -9,109 +9,122 @@ import UIKit // MARK: Constants public enum IDKeyboardConstants { - static let defaultCurrencyKey = "$" - static let currencyKeys = ["$", "€", "£", "¥"] + static let defaultCurrencyKey = "$" + static let currencyKeys = ["$", "€", "£", "¥"] } struct IDKeyboardProvider: KeyboardProviderProtocol { - // MARK: iPhone Layouts - - static func genPhoneLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l"]) - .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "delete"]) - .addRow(["123", "selectKeyboard", "space", "return"]) - .build() - } - - static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) - .addRow(["-", "/", ":", ";", "(", ")", "$", "&", "@", "\""]) - .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - .replaceKey(row: 1, column: 6, to: currencyKey) - .build() - } - - static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) - .addRow(["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "·"]) - .addRow(["123", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 6, to: currencyKeys[0]) - .replaceKey(row: 1, column: 7, to: currencyKeys[1]) - .replaceKey(row: 1, column: 8, to: currencyKeys[2]) - .build() + // MARK: iPhone Layouts + + static func genPhoneLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l"]) + .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "delete"]) + .addRow(["123", "selectKeyboard", "space", "return"]) + .build() } - } - - // MARK: iPad Layouts - - static func genPadLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) - .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "delete"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "return"]) - .addRow(["shift", "w", "x", "c", "v", "b", "n", "m", ",", ".", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) - .addRow(["@", "#", "$", "&", "*", "(", ")", "'", "\"", "return"]) - .addRow(["#+=", "%", "_", "+", "=", "/", ";", ":", ",", ".", "#+="]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .replaceKey(row: 1, column: 2, to: currencyKey) - .build() - } - - static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) - .addRow(["€", "£", "¥", "_", "^", "[", "]", "{", "}", "return"]) - .addRow(["123", "§", "|", "~", "...", "\\", "<", ">", "!", "?", "123"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 0, to: currencyKeys[0]) - .replaceKey(row: 1, column: 1, to: currencyKeys[1]) - .replaceKey(row: 1, column: 2, to: currencyKeys[2]) - .build() + + static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) + .addRow(["-", "/", ":", ";", "(", ")", "$", "&", "@", "\""]) + .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + .replaceKey(row: 1, column: 6, to: currencyKey) + .build() + } + + static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) + .addRow(["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "·"]) + .addRow(["123", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 6, to: currencyKeys[0]) + .replaceKey(row: 1, column: 7, to: currencyKeys[1]) + .replaceKey(row: 1, column: 8, to: currencyKeys[2]) + .build() + } + } + + // MARK: iPad Layouts + + static func genPadLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) + .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "delete"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "return"]) + .addRow(["shift", "w", "x", "c", "v", "b", "n", "m", ",", ".", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) + .addRow(["@", "#", "$", "&", "*", "(", ")", "'", "\"", "return"]) + .addRow(["#+=", "%", "_", "+", "=", "/", ";", ":", ",", ".", "#+="]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .replaceKey(row: 1, column: 2, to: currencyKey) + .build() + } + + static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) + .addRow(["€", "£", "¥", "_", "^", "[", "]", "{", "}", "return"]) + .addRow(["123", "§", "|", "~", "...", "\\", "<", ">", "!", "?", "123"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 0, to: currencyKeys[0]) + .replaceKey(row: 1, column: 1, to: currencyKeys[1]) + .replaceKey(row: 1, column: 2, to: currencyKeys[2]) + .build() + } + } + + static func genPadExpandedLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["~", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "delete"]) + .addRow([ + SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]", + "\\" + ]) + .addRow([ + SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", ":", ";", "'", + "return" + ]) + .addRow(["shift", "-", "z", "x", "c", "v", "b", "n", "m", ",", ".", "/", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadExpandedSymbolKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) + .addRow([ + SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "—", "~", "°" + ]) + .addRow([ + SpecialKeys.capsLock, "-", "\\", ":", ";", "(", ")", "&", "@", "$", "£", "¥", "€", + "return" + ]) + .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "|", "_", ".", ",", "/", "shift"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .build() } - } - - static func genPadExpandedLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["~", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "delete"]) - .addRow([SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]", "\\"]) - .addRow([SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", ":", ";", "'", "return"]) - .addRow(["shift", "-", "z", "x", "c", "v", "b", "n", "m", ",", ".", "/", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadExpandedSymbolKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) - .addRow([SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "—", "~", "°"]) - .addRow([SpecialKeys.capsLock, "-", "\\", ":", ";", "(", ")", "&", "@", "$", "£", "¥", "€", "return"]) - .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "|", "_", ".", ",", "/", "shift"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .build() - } } // MARK: Generate and set keyboard @@ -119,98 +132,116 @@ struct IDKeyboardProvider: KeyboardProviderProtocol { // MARK: Get Keys func getIDKeys() { - guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") else { - fatalError("Unable to access shared user defaults") - } - - var currencyKey = IDKeyboardConstants.defaultCurrencyKey - var currencyKeys = IDKeyboardConstants.currencyKeys - let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" - if let currencyValue = userDefaults.string(forKey: dictionaryKey) { - currencyKey = currencyValue - } else { - userDefaults.setValue(currencyKey, forKey: dictionaryKey) - } - if let index = currencyKeys.firstIndex(of: currencyKey) { - currencyKeys.remove(at: index) - } - - if DeviceType.isPhone { - letterKeys = IDKeyboardProvider.genPhoneLetterKeys() - numberKeys = IDKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) - symbolKeys = IDKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) - - leftKeyChars = ["q", "1", "-", "[", "_"] - rightKeyChars = ["p", "0", "\"", "=", "·"] - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } else { - // Use the expanded keys layout if the iPad is wide enough and has no home button. - if usingExpandedKeyboard { - letterKeys = IDKeyboardProvider.genPadExpandedLetterKeys() - symbolKeys = IDKeyboardProvider.genPadExpandedSymbolKeys() - - leftKeyChars = ["~", "`"] - rightKeyChars = ["\\", "°"] - allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) - } else { - letterKeys = IDKeyboardProvider.genPadLetterKeys() - numberKeys = IDKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) - symbolKeys = IDKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) + guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + else { + fatalError("Unable to access shared user defaults") + } - letterKeys.removeFirst(1) + var currencyKey = IDKeyboardConstants.defaultCurrencyKey + var currencyKeys = IDKeyboardConstants.currencyKeys + let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" + if let currencyValue = userDefaults.string(forKey: dictionaryKey) { + currencyKey = currencyValue + } else { + userDefaults.setValue(currencyKey, forKey: dictionaryKey) + } + if let index = currencyKeys.firstIndex(of: currencyKey) { + currencyKeys.remove(at: index) + } - leftKeyChars = ["q", "1"] - rightKeyChars = [] - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + if DeviceType.isPhone { + letterKeys = IDKeyboardProvider.genPhoneLetterKeys() + numberKeys = IDKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) + symbolKeys = IDKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + + leftKeyChars = ["q", "1", "-", "[", "_"] + rightKeyChars = ["p", "0", "\"", "=", "·"] + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } + } else { + // Use the expanded keys layout if the iPad is wide enough and has no home button. + if usingExpandedKeyboard { + letterKeys = IDKeyboardProvider.genPadExpandedLetterKeys() + symbolKeys = IDKeyboardProvider.genPadExpandedSymbolKeys() + + leftKeyChars = ["~", "`"] + rightKeyChars = ["\\", "°"] + allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) + } else { + letterKeys = IDKeyboardProvider.genPadLetterKeys() + numberKeys = IDKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) + symbolKeys = IDKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) + + letterKeys.removeFirst(1) + + leftKeyChars = ["q", "1"] + rightKeyChars = [] + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + } + + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } } - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } - - keysWithAlternates = [] - keysWithAlternatesLeft = [] - keysWithAlternatesRight = [] - aAlternateKeys = [] - eAlternateKeys = [] - iAlternateKeys = [] - oAlternateKeys = [] - uAlternateKeys = [] - cAlternateKeys = [] - lAlternateKeys = [] - nAlternateKeys = [] - sAlternateKeys = [] - zAlternateKeys = [] + keysWithAlternates = [] + keysWithAlternatesLeft = [] + keysWithAlternatesRight = [] + aAlternateKeys = [] + eAlternateKeys = [] + iAlternateKeys = [] + oAlternateKeys = [] + uAlternateKeys = [] + cAlternateKeys = [] + lAlternateKeys = [] + nAlternateKeys = [] + sAlternateKeys = [] + zAlternateKeys = [] } // MARK: Provide Layout func setIDKeyboardLayout() { - getIDKeys() - - currencySymbol = "$" - currencySymbolAlternates = dollarAlternateKeys - spaceBar = "spasi" - - invalidCommandMsgWikidata = "Tidak ada di Wikidata" - invalidCommandTextWikidata1 = "Wikidata adalah knowledge graph yang diedit secara kolaboratif dan dikelola oleh Wikimedia Foundation. Wikidata berfungsi sebagai sumber data terbuka untuk proyek-proyek seperti Wikipedia dan banyak proyek lainnya." - invalidCommandTextWikidata2 = "Scribe menggunakan data bahasa Wikidata untuk banyak fitur intinya. Kami mendapatkan informasi seperti jenis kelamin kata benda, konjugasi kata kerja, dan banyak lagi!" - invalidCommandTextWikidata3 = "Anda dapat membuat akun di wikidata.org untuk bergabung dengan komunitas yang mendukung Scribe dan banyak proyek lainnya. Bantu kami menghadirkan informasi gratis ke dunia!" - - invalidCommandMsgWiktionary = "Tidak ada di Wiktionary" - invalidCommandTextWiktionary1 = "Wiktionary adalah kamus yang diedit secara kolaboratif dan dikelola oleh Wikimedia Foundation. Wiktionary berfungsi sebagai sumber data linguistik gratis untuk proyek-proyek seperti Wikipedia dan banyak proyek lainnya." - invalidCommandTextWiktionary2 = "Scribe menggunakan data Wiktionary untuk menyediakan terjemahan untuk perintah Terjemahannya. Data kami berasal dari banyak pasangan bahasa yang telah dibuat oleh komunitas Wiktionary!" - invalidCommandTextWiktionary3 = "Anda dapat membuat akun di wiktionary.org untuk bergabung dengan komunitas yang mendukung Scribe dan banyak proyek lainnya. Bantu kami menghadirkan informasi gratis ke dunia!" - - baseAutosuggestions = ["aku", "saya", "itu"] - numericAutosuggestions = ["adalah", "untuk", "dan"] - verbsAfterPronounsArray = ["sudah", "sedang", "bisa"] - - translateKeyLbl = "Terjemahkan" - translatePlaceholder = "Masukkan kata" - translatePrompt = commandPromptSpacing + "id -› \(getControllerLanguageAbbr()): " - translatePromptAndCursor = translatePrompt + commandCursor - translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder - translatePromptAndColorPlaceholder = NSMutableAttributedString(string: translatePromptAndPlaceholder) - translatePromptAndColorPlaceholder.setColorForText(textForAttribute: translatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) + getIDKeys() + + currencySymbol = "$" + currencySymbolAlternates = dollarAlternateKeys + spaceBar = "spasi" + + invalidCommandMsgWikidata = "Tidak ada di Wikidata" + invalidCommandTextWikidata1 = + "Wikidata adalah knowledge graph yang diedit secara kolaboratif dan dikelola oleh Wikimedia Foundation. Wikidata berfungsi sebagai sumber data terbuka untuk proyek-proyek seperti Wikipedia dan banyak proyek lainnya." + invalidCommandTextWikidata2 = + "Scribe menggunakan data bahasa Wikidata untuk banyak fitur intinya. Kami mendapatkan informasi seperti jenis kelamin kata benda, konjugasi kata kerja, dan banyak lagi!" + invalidCommandTextWikidata3 = + "Anda dapat membuat akun di wikidata.org untuk bergabung dengan komunitas yang mendukung Scribe dan banyak proyek lainnya. Bantu kami menghadirkan informasi gratis ke dunia!" + + invalidCommandMsgWiktionary = "Tidak ada di Wiktionary" + invalidCommandTextWiktionary1 = + "Wiktionary adalah kamus yang diedit secara kolaboratif dan dikelola oleh Wikimedia Foundation. Wiktionary berfungsi sebagai sumber data linguistik gratis untuk proyek-proyek seperti Wikipedia dan banyak proyek lainnya." + invalidCommandTextWiktionary2 = + "Scribe menggunakan data Wiktionary untuk menyediakan terjemahan untuk perintah Terjemahannya. Data kami berasal dari banyak pasangan bahasa yang telah dibuat oleh komunitas Wiktionary!" + invalidCommandTextWiktionary3 = + "Anda dapat membuat akun di wiktionary.org untuk bergabung dengan komunitas yang mendukung Scribe dan banyak proyek lainnya. Bantu kami menghadirkan informasi gratis ke dunia!" + + baseAutosuggestions = ["aku", "saya", "itu"] + numericAutosuggestions = ["adalah", "untuk", "dan"] + verbsAfterPronounsArray = ["sudah", "sedang", "bisa"] + + translateKeyLbl = "Terjemahkan" + translatePlaceholder = "Masukkan kata" + translatePrompt = commandPromptSpacing + "id -› \(getControllerLanguageAbbr()): " + translatePromptAndCursor = translatePrompt + commandCursor + translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder + translatePromptAndColorPlaceholder = NSMutableAttributedString( + string: translatePromptAndPlaceholder + ) + translatePromptAndColorPlaceholder.setColorForText( + textForAttribute: translatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) } diff --git a/Keyboards/LanguageKeyboards/Indonesian/IDKeyboardViewController.swift b/Keyboards/LanguageKeyboards/Indonesian/IDKeyboardViewController.swift index 3d8de2e1..b9f56b57 100644 --- a/Keyboards/LanguageKeyboards/Indonesian/IDKeyboardViewController.swift +++ b/Keyboards/LanguageKeyboards/Indonesian/IDKeyboardViewController.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Class for the Indonesian Scribe keyboard. */ diff --git a/Keyboards/LanguageKeyboards/Italian/ITInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Italian/ITInterfaceVariables.swift index c1f1b7fd..8337b9f7 100644 --- a/Keyboards/LanguageKeyboards/Italian/ITInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Italian/ITInterfaceVariables.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Constants and functions to load the Italian Scribe keyboard. */ @@ -9,237 +9,276 @@ import UIKit // MARK: Constants public enum ITKeyboardConstants { - static let defaultCurrencyKey = "€" - static let currencyKeys = ["€", "$", "£", "¥"] - - // Alternate key vars. - static let keysWithAlternates = ["a", "e", "i", "o", "u", "c", "n", "s"] - static let keysWithAlternatesLeft = ["a", "e", "s", "c"] - static let keysWithAlternatesRight = ["i", "o", "u", "n"] - - static let aAlternateKeys = ["à", "á", "ä", "â", "æ", "ã", "å", "ā", "ᵃ"] - static let eAlternateKeys = ["é", "è", "ə", "ê", "ë", "ę", "ė", "ē"] - static let iAlternateKeys = ["ī", "į", "ï", "î", "í", "ì"] - static let oAlternateKeys = ["ᵒ", "ō", "ø", "œ", "õ", "ö", "ô", "ó", "ò"] - static let uAlternateKeys = ["ū", "ü", "û", "ú", "ù"] - static let cAlternateKeys = ["ç", "ć", "č"] - static let nAlternateKeys = ["ñ"] - static let sAlternateKeys = ["ß", "ś", "š"] + static let defaultCurrencyKey = "€" + static let currencyKeys = ["€", "$", "£", "¥"] + + // Alternate key vars. + static let keysWithAlternates = ["a", "e", "i", "o", "u", "c", "n", "s"] + static let keysWithAlternatesLeft = ["a", "e", "s", "c"] + static let keysWithAlternatesRight = ["i", "o", "u", "n"] + + static let aAlternateKeys = ["à", "á", "ä", "â", "æ", "ã", "å", "ā", "ᵃ"] + static let eAlternateKeys = ["é", "è", "ə", "ê", "ë", "ę", "ė", "ē"] + static let iAlternateKeys = ["ī", "į", "ï", "î", "í", "ì"] + static let oAlternateKeys = ["ᵒ", "ō", "ø", "œ", "õ", "ö", "ô", "ó", "ò"] + static let uAlternateKeys = ["ū", "ü", "û", "ú", "ù"] + static let cAlternateKeys = ["ç", "ć", "č"] + static let nAlternateKeys = ["ñ"] + static let sAlternateKeys = ["ß", "ś", "š"] } struct ITKeyboardProvider: KeyboardProviderProtocol { - // MARK: iPhone Layouts - - static func genPhoneLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l"]) - .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "delete"]) - .addRow(["123", "selectKeyboard", "space", "return"]) - .build() - } - - static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) - .addRow(["-", "/", ":", ";", "(", ")", "€", "&", "@", "\""]) - .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - .replaceKey(row: 1, column: 6, to: currencyKey) - .build() - } - - static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) - .addRow(["_", "\\", "|", "~", "<", ">", "$", "£", "¥", "·"]) - .addRow(["123", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 6, to: currencyKeys[0]) - .replaceKey(row: 1, column: 7, to: currencyKeys[1]) - .replaceKey(row: 1, column: 8, to: currencyKeys[2]) - .build() + // MARK: iPhone Layouts + + static func genPhoneLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l"]) + .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "delete"]) + .addRow(["123", "selectKeyboard", "space", "return"]) + .build() } - } - - // MARK: iPad Layouts - - static func genPadLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) - .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "delete"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "return"]) - .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) - .addRow(["@", "#", "€", "&", "*", "(", ")", "'", "\"", "return"]) - .addRow(["#+=", "%", "-", "+", "=", "/", ";", ":", ",", ".", "#+="]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .replaceKey(row: 1, column: 2, to: currencyKey) - .build() - } - - static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) - .addRow(["$", "£", "¥", "_", "^", "[", "]", "{", "}", "return"]) - .addRow(["123", "§", "|", "~", "...", "\\", "<", ">", "!", "?", "123"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 0, to: currencyKeys[0]) - .replaceKey(row: 1, column: 1, to: currencyKeys[1]) - .replaceKey(row: 1, column: 2, to: currencyKeys[2]) - .build() + + static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) + .addRow(["-", "/", ":", ";", "(", ")", "€", "&", "@", "\""]) + .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + .replaceKey(row: 1, column: 6, to: currencyKey) + .build() + } + + static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) + .addRow(["_", "\\", "|", "~", "<", ">", "$", "£", "¥", "·"]) + .addRow(["123", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 6, to: currencyKeys[0]) + .replaceKey(row: 1, column: 7, to: currencyKeys[1]) + .replaceKey(row: 1, column: 8, to: currencyKeys[2]) + .build() + } + } + + // MARK: iPad Layouts + + static func genPadLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) + .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "delete"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "return"]) + .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) + .addRow(["@", "#", "€", "&", "*", "(", ")", "'", "\"", "return"]) + .addRow(["#+=", "%", "-", "+", "=", "/", ";", ":", ",", ".", "#+="]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .replaceKey(row: 1, column: 2, to: currencyKey) + .build() + } + + static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) + .addRow(["$", "£", "¥", "_", "^", "[", "]", "{", "}", "return"]) + .addRow(["123", "§", "|", "~", "...", "\\", "<", ">", "!", "?", "123"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 0, to: currencyKeys[0]) + .replaceKey(row: 1, column: 1, to: currencyKeys[1]) + .replaceKey(row: 1, column: 2, to: currencyKeys[2]) + .build() + } + } + + // MARK: Expanded iPad Layouts + + static func genPadExpandedLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["\\", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "‘", "ì", "delete"]) + .addRow([ + SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "è", "+", "*" + ]) + .addRow([ + SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "ò", "à", "ù", + "return" + ]) + .addRow(["shift", "'", "z", "x", "c", "v", "b", "n", "m", "-", ",", ".", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadExpandedSymbolKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) + .addRow([ + SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "\"", "|", + "§" + ]) + .addRow([ + SpecialKeys.capsLock, "°", "/", ":", ";", "(", ")", "&", "@", "$", "£", "¥", "~", + "return" + ]) + .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "_", "€", "-", ",", ".", "shift"]) // "shift" + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .build() } - } - - // MARK: Expanded iPad Layouts - - static func genPadExpandedLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["\\", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "‘", "ì", "delete"]) - .addRow([SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "è", "+", "*"]) - .addRow([SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "ò", "à", "ù", "return"]) - .addRow(["shift", "'", "z", "x", "c", "v", "b", "n", "m", "-", ",", ".", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadExpandedSymbolKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) - .addRow([SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "\"", "|", "§"]) - .addRow([SpecialKeys.capsLock, "°", "/", ":", ";", "(", ")", "&", "@", "$", "£", "¥", "~", "return"]) - .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "_", "€", "-", ",", ".", "shift"]) // "shift" - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .build() - } } // MARK: Get Keys func getITKeys() { - guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") else { - fatalError("Unable to access shared user defaults") - } - - var currencyKey = ITKeyboardConstants.defaultCurrencyKey - var currencyKeys = ITKeyboardConstants.currencyKeys - let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" - if let currencyValue = userDefaults.string(forKey: dictionaryKey) { - currencyKey = currencyValue - } else { - userDefaults.setValue(currencyKey, forKey: dictionaryKey) - } - if let index = currencyKeys.firstIndex(of: currencyKey) { - currencyKeys.remove(at: index) - } - - if DeviceType.isPhone { - letterKeys = ITKeyboardProvider.genPhoneLetterKeys() - numberKeys = ITKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) - symbolKeys = ITKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) - - leftKeyChars = ["q", "1", "-", "[", "_"] - rightKeyChars = ["p", "0", "\"", "=", "·"] - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } else { - // Use the expanded keys layout if the iPad is wide enough and has no home button. - if usingExpandedKeyboard { - letterKeys = ITKeyboardProvider.genPadExpandedLetterKeys() - symbolKeys = ITKeyboardProvider.genPadExpandedSymbolKeys() - - leftKeyChars = ["\\", "`"] - rightKeyChars = ["*", "§"] - allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) - } else { - letterKeys = ITKeyboardProvider.genPadLetterKeys() - numberKeys = ITKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) - symbolKeys = ITKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) + guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + else { + fatalError("Unable to access shared user defaults") + } - letterKeys.removeFirst(1) + var currencyKey = ITKeyboardConstants.defaultCurrencyKey + var currencyKeys = ITKeyboardConstants.currencyKeys + let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" + if let currencyValue = userDefaults.string(forKey: dictionaryKey) { + currencyKey = currencyValue + } else { + userDefaults.setValue(currencyKey, forKey: dictionaryKey) + } + if let index = currencyKeys.firstIndex(of: currencyKey) { + currencyKeys.remove(at: index) + } - leftKeyChars = ["q", "1"] - rightKeyChars = [] - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + if DeviceType.isPhone { + letterKeys = ITKeyboardProvider.genPhoneLetterKeys() + numberKeys = ITKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) + symbolKeys = ITKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + + leftKeyChars = ["q", "1", "-", "[", "_"] + rightKeyChars = ["p", "0", "\"", "=", "·"] + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } + } else { + // Use the expanded keys layout if the iPad is wide enough and has no home button. + if usingExpandedKeyboard { + letterKeys = ITKeyboardProvider.genPadExpandedLetterKeys() + symbolKeys = ITKeyboardProvider.genPadExpandedSymbolKeys() + + leftKeyChars = ["\\", "`"] + rightKeyChars = ["*", "§"] + allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) + } else { + letterKeys = ITKeyboardProvider.genPadLetterKeys() + numberKeys = ITKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) + symbolKeys = ITKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) + + letterKeys.removeFirst(1) + + leftKeyChars = ["q", "1"] + rightKeyChars = [] + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + } + + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } } - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } - - keysWithAlternates = ITKeyboardConstants.keysWithAlternates - keysWithAlternatesLeft = ITKeyboardConstants.keysWithAlternatesLeft - keysWithAlternatesRight = ITKeyboardConstants.keysWithAlternatesRight - aAlternateKeys = ITKeyboardConstants.aAlternateKeys - eAlternateKeys = ITKeyboardConstants.eAlternateKeys - iAlternateKeys = ITKeyboardConstants.iAlternateKeys - oAlternateKeys = ITKeyboardConstants.oAlternateKeys - uAlternateKeys = ITKeyboardConstants.uAlternateKeys - sAlternateKeys = ITKeyboardConstants.sAlternateKeys - cAlternateKeys = ITKeyboardConstants.cAlternateKeys - nAlternateKeys = ITKeyboardConstants.nAlternateKeys + keysWithAlternates = ITKeyboardConstants.keysWithAlternates + keysWithAlternatesLeft = ITKeyboardConstants.keysWithAlternatesLeft + keysWithAlternatesRight = ITKeyboardConstants.keysWithAlternatesRight + aAlternateKeys = ITKeyboardConstants.aAlternateKeys + eAlternateKeys = ITKeyboardConstants.eAlternateKeys + iAlternateKeys = ITKeyboardConstants.iAlternateKeys + oAlternateKeys = ITKeyboardConstants.oAlternateKeys + uAlternateKeys = ITKeyboardConstants.uAlternateKeys + sAlternateKeys = ITKeyboardConstants.sAlternateKeys + cAlternateKeys = ITKeyboardConstants.cAlternateKeys + nAlternateKeys = ITKeyboardConstants.nAlternateKeys } // MARK: Provide Layout func setITKeyboardLayout() { - getITKeys() - - currencySymbol = "€" - currencySymbolAlternates = euroAlternateKeys - spaceBar = "spazio" - language = "Italiano" - - invalidCommandMsgWikidata = "Non in Wikidata" - invalidCommandTextWikidata1 = "Wikidata è un grafo della conoscenza modificabile in modo collaborativo, gestito dalla Wikimedia Foundation. Serve come fonte di dati aperti per progetti come Wikipedia e innumerevoli altri." - invalidCommandTextWikidata2 = "Scribe utilizza i dati linguistici di Wikidata per molte delle sue funzionalità principali. Otteniamo informazioni come il genere dei sostantivi, la coniugazione dei verbi e molto altro!" - invalidCommandTextWikidata3 = "Puoi creare un account su wikidata.org per unirti alla comunità che supporta Scribe e tanti altri progetti. Aiutaci a diffondere informazioni libere in tutto il mondo!" - - invalidCommandMsgWiktionary = "Non in Wiktionary" - invalidCommandTextWiktionary1 = "Wiktionary è un dizionario modificato in modo collaborativo e gestito dalla Wikimedia Foundation. Serve come fonte di dati linguistici liberi per progetti come Wikipedia e innumerevoli altri." - invalidCommandTextWiktionary2 = "Scribe utilizza i dati di Wiktionary per fornire traduzioni per il suo comando Traduci. I nostri dati derivano dalle numerose coppie linguistiche create dalla community di Wiktionary!" - invalidCommandTextWiktionary3 = "Puoi creare un account su wiktionary.org per unirti alla community che supporta Scribe e tanti altri progetti. Aiutaci a diffondere informazioni libere in tutto il mondo!" - - baseAutosuggestions = ["ho", "non", "ma"] - numericAutosuggestions = ["utenti", "anni", "e"] - - translateKeyLbl = "Tradurre" - translatePlaceholder = "Inserisci una parola" - translatePrompt = commandPromptSpacing + "it -› \(getControllerLanguageAbbr()): " - translatePromptAndCursor = translatePrompt + commandCursor - translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder - translatePromptAndColorPlaceholder = NSMutableAttributedString(string: translatePromptAndPlaceholder) - translatePromptAndColorPlaceholder.setColorForText(textForAttribute: translatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - conjugateKeyLbl = "Coniugare" - conjugatePlaceholder = "Inserisci un verbo" - conjugatePrompt = commandPromptSpacing + "Coniugare: " - conjugatePromptAndCursor = conjugatePrompt + commandCursor - conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder - conjugatePromptAndColorPlaceholder = NSMutableAttributedString(string: conjugatePromptAndPlaceholder) - conjugatePromptAndColorPlaceholder.setColorForText(textForAttribute: conjugatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - pluralKeyLbl = "Plurale" - pluralPlaceholder = "Inserisci un nome" - pluralPrompt = commandPromptSpacing + "Plurale: " - pluralPromptAndCursor = pluralPrompt + commandCursor - pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder - pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) - pluralPromptAndColorPlaceholder.setColorForText(textForAttribute: pluralPlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - alreadyPluralMsg = "Già plurale" + getITKeys() + + currencySymbol = "€" + currencySymbolAlternates = euroAlternateKeys + spaceBar = "spazio" + language = "Italiano" + + invalidCommandMsgWikidata = "Non in Wikidata" + invalidCommandTextWikidata1 = + "Wikidata è un grafo della conoscenza modificabile in modo collaborativo, gestito dalla Wikimedia Foundation. Serve come fonte di dati aperti per progetti come Wikipedia e innumerevoli altri." + invalidCommandTextWikidata2 = + "Scribe utilizza i dati linguistici di Wikidata per molte delle sue funzionalità principali. Otteniamo informazioni come il genere dei sostantivi, la coniugazione dei verbi e molto altro!" + invalidCommandTextWikidata3 = + "Puoi creare un account su wikidata.org per unirti alla comunità che supporta Scribe e tanti altri progetti. Aiutaci a diffondere informazioni libere in tutto il mondo!" + + invalidCommandMsgWiktionary = "Non in Wiktionary" + invalidCommandTextWiktionary1 = + "Wiktionary è un dizionario modificato in modo collaborativo e gestito dalla Wikimedia Foundation. Serve come fonte di dati linguistici liberi per progetti come Wikipedia e innumerevoli altri." + invalidCommandTextWiktionary2 = + "Scribe utilizza i dati di Wiktionary per fornire traduzioni per il suo comando Traduci. I nostri dati derivano dalle numerose coppie linguistiche create dalla community di Wiktionary!" + invalidCommandTextWiktionary3 = + "Puoi creare un account su wiktionary.org per unirti alla community che supporta Scribe e tanti altri progetti. Aiutaci a diffondere informazioni libere in tutto il mondo!" + + baseAutosuggestions = ["ho", "non", "ma"] + numericAutosuggestions = ["utenti", "anni", "e"] + + translateKeyLbl = "Tradurre" + translatePlaceholder = "Inserisci una parola" + translatePrompt = commandPromptSpacing + "it -› \(getControllerLanguageAbbr()): " + translatePromptAndCursor = translatePrompt + commandCursor + translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder + translatePromptAndColorPlaceholder = NSMutableAttributedString( + string: translatePromptAndPlaceholder + ) + translatePromptAndColorPlaceholder.setColorForText( + textForAttribute: translatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + conjugateKeyLbl = "Coniugare" + conjugatePlaceholder = "Inserisci un verbo" + conjugatePrompt = commandPromptSpacing + "Coniugare: " + conjugatePromptAndCursor = conjugatePrompt + commandCursor + conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder + conjugatePromptAndColorPlaceholder = NSMutableAttributedString( + string: conjugatePromptAndPlaceholder + ) + conjugatePromptAndColorPlaceholder.setColorForText( + textForAttribute: conjugatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + pluralKeyLbl = "Plurale" + pluralPlaceholder = "Inserisci un nome" + pluralPrompt = commandPromptSpacing + "Plurale: " + pluralPromptAndCursor = pluralPrompt + commandCursor + pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder + pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) + pluralPromptAndColorPlaceholder.setColorForText( + textForAttribute: pluralPlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + alreadyPluralMsg = "Già plurale" } diff --git a/Keyboards/LanguageKeyboards/Italian/ITKeyboardViewController.swift b/Keyboards/LanguageKeyboards/Italian/ITKeyboardViewController.swift index 91b7d63e..749b42fe 100644 --- a/Keyboards/LanguageKeyboards/Italian/ITKeyboardViewController.swift +++ b/Keyboards/LanguageKeyboards/Italian/ITKeyboardViewController.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Class for the Italian Scribe keyboard. */ diff --git a/Keyboards/LanguageKeyboards/Norwegian/NBCommandVariables.swift b/Keyboards/LanguageKeyboards/Norwegian/NBCommandVariables.swift index fee79c8b..d8ffa635 100644 --- a/Keyboards/LanguageKeyboards/Norwegian/NBCommandVariables.swift +++ b/Keyboards/LanguageKeyboards/Norwegian/NBCommandVariables.swift @@ -1,61 +1,61 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Variables associated with commands for the Norwegian Bokmål Scribe keyboard. */ func nbSetConjugationLabels() { - // Reset all form labels prior to assignment. - for k in formLabelsDict.keys { - formLabelsDict[k] = "" - } - - formLabelsDict["FPS"] = "jeg" // I - formLabelsDict["SPS"] = "du" // you (informal) - formLabelsDict["TPS"] = "han/henne/det" // he/she/it - formLabelsDict["FPP"] = "vi" // we - formLabelsDict["SPP"] = "dere" // you (plural) - formLabelsDict["TPP"] = "de" // they + // Reset all form labels prior to assignment. + for k in formLabelsDict.keys { + formLabelsDict[k] = "" + } + + formLabelsDict["FPS"] = "jeg" // I + formLabelsDict["SPS"] = "du" // you (informal) + formLabelsDict["TPS"] = "han/henne/det" // he/she/it + formLabelsDict["FPP"] = "vi" // we + formLabelsDict["SPP"] = "dere" // you (plural) + formLabelsDict["TPP"] = "de" // they } /// What the conjugation state is for the conjugate feature. enum NBConjugationState { - case present + case present } var nbConjugationState: NBConjugationState = .present /// Sets the title of the command bar when the keyboard is in conjugate mode. func nbGetConjugationTitle() -> String { - let verbToDisplay = verbToConjugate - switch nbConjugationState { - case .present: - return commandPromptSpacing + "Presens: " + verbToDisplay - } + let verbToDisplay = verbToConjugate + switch nbConjugationState { + case .present: + return commandPromptSpacing + "Presens: " + verbToDisplay + } } /// Returns the appropriate key in the verbs dictionary to access conjugations. func nbGetConjugationState() -> String { - switch nbConjugationState { - case .present: - return "pres" - } + switch nbConjugationState { + case .present: + return "pres" + } } /// Action associated with the left view switch button of the conjugation state. func nbConjugationStateLeft() { - switch nbConjugationState { - case .present: - // Note: Transition to other states once verb data is added. - break - } + switch nbConjugationState { + case .present: + // Note: Transition to other states once verb data is added. + break + } } /// Action associated with the right view switch button of the conjugation state. func nbConjugationStateRight() { - switch nbConjugationState { - case .present: - // Note: Transition to other states once verb data is added. - break - } + switch nbConjugationState { + case .present: + // Note: Transition to other states once verb data is added. + break + } } diff --git a/Keyboards/LanguageKeyboards/Norwegian/NBInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Norwegian/NBInterfaceVariables.swift index e78e8e61..2d7e8a74 100644 --- a/Keyboards/LanguageKeyboards/Norwegian/NBInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Norwegian/NBInterfaceVariables.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Constants and functions to load the Norwegian Bokmål Scribe keyboard. */ @@ -9,253 +9,295 @@ import UIKit // MARK: Constants public enum NBKeyboardConstants { - static let defaultCurrencyKey = "kr" - static let currencyKeys = ["kr", "€", "$", "£", "¥"] - - // Alternate key vars. - static let keysWithAlternates = ["a", "e", "i", "o", "u", "y", "å", "æ", "ø", "d", "l", "n", "s"] - static let keysWithAlternatesLeft = ["a", "e", "y", "å", "d", "s"] - static let keysWithAlternatesRight = ["i", "o", "u", "æ", "ø", "l", "n"] - - static let aAlternateKeys = ["á", "ä", "à", "â", "ã", "ā"] - static let eAlternateKeys = ["é", "ë", "è", "ê", "ę", "ė", "ē"] - static let iAlternateKeys = ["ï", "í", "ì", "î", "į", "ī"] - static let oAlternateKeys = ["ö", "ō", "œ", "õ", "ò", "ô", "ó"] - static let uAlternateKeys = ["ū", "ù", "û", "ü", "ú"] - static let yAlternateKeys = ["ÿ"] - static let åAlternateKeys = ["ä", "ā"] - static let æAlternateKeys = ["ä"] - static let øAlternateKeys = ["ö", "ô"] - static let dAlternateKeys = ["ð"] - static let lAlternateKeys = ["ł"] - static let nAlternateKeys = ["ń", "ñ"] - static let sAlternateKeys = ["ß", "ś", "š"] + static let defaultCurrencyKey = "kr" + static let currencyKeys = ["kr", "€", "$", "£", "¥"] + + /// Alternate key vars. + static let keysWithAlternates = [ + "a", "e", "i", "o", "u", "y", "å", "æ", "ø", "d", "l", "n", "s" + ] + static let keysWithAlternatesLeft = ["a", "e", "y", "å", "d", "s"] + static let keysWithAlternatesRight = ["i", "o", "u", "æ", "ø", "l", "n"] + + static let aAlternateKeys = ["á", "ä", "à", "â", "ã", "ā"] + static let eAlternateKeys = ["é", "ë", "è", "ê", "ę", "ė", "ē"] + static let iAlternateKeys = ["ï", "í", "ì", "î", "į", "ī"] + static let oAlternateKeys = ["ö", "ō", "œ", "õ", "ò", "ô", "ó"] + static let uAlternateKeys = ["ū", "ù", "û", "ü", "ú"] + static let yAlternateKeys = ["ÿ"] + static let åAlternateKeys = ["ä", "ā"] + static let æAlternateKeys = ["ä"] + static let øAlternateKeys = ["ö", "ô"] + static let dAlternateKeys = ["ð"] + static let lAlternateKeys = ["ł"] + static let nAlternateKeys = ["ń", "ñ"] + static let sAlternateKeys = ["ß", "ś", "š"] } struct NBKeyboardProvider: KeyboardProviderProtocol { - // MARK: iPhone Layouts - - static func genPhoneLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "å"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ø", "æ"]) - .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "delete"]) - .addRow(["123", "selectKeyboard", "space", "return"]) - .build() - } - - static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) - .addRow(["-", "/", ":", ";", "(", ")", "kr", "&", "@", "\""]) - .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - .replaceKey(row: 1, column: 6, to: currencyKey) - .build() - } - - static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) - .addRow(["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "·"]) - .addRow(["123", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 6, to: currencyKeys[0]) - .replaceKey(row: 1, column: 7, to: currencyKeys[1]) - .replaceKey(row: 1, column: 8, to: currencyKeys[2]) - .build() + // MARK: iPhone Layouts + + static func genPhoneLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "å"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ø", "æ"]) + .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "delete"]) + .addRow(["123", "selectKeyboard", "space", "return"]) + .build() } - } - - // MARK: iPad Layouts - - static func genPadLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) - .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "å", "delete"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ø", "æ", "return"]) - .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "`", "delete"]) - .addRow(["@", "#", "kr", "&", "*", "(", ")", "'", "\"", "+", "·", "return"]) - .addRow(["#+=", "%", "_", "-", "=", "/", ";", ":", ",", ".", "?", "#+="]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .replaceKey(row: 1, column: 2, to: currencyKey) - .build() - } - - static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "'", "delete"]) - .addRow(["€", "$", "£", "^", "[", "]", "{", "}", "―", "ᵒ", "...", "return"]) - .addRow(["123", "§", "|", "~", "≠", "≈", "\\", "<", ">", "!", "?", "123"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 0, to: currencyKeys[0]) - .replaceKey(row: 1, column: 1, to: currencyKeys[1]) - .replaceKey(row: 1, column: 2, to: currencyKeys[2]) - .build() + + static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) + .addRow(["-", "/", ":", ";", "(", ")", "kr", "&", "@", "\""]) + .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + .replaceKey(row: 1, column: 6, to: currencyKey) + .build() + } + + static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) + .addRow(["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "·"]) + .addRow(["123", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 6, to: currencyKeys[0]) + .replaceKey(row: 1, column: 7, to: currencyKeys[1]) + .replaceKey(row: 1, column: 8, to: currencyKeys[2]) + .build() + } + } + + // MARK: iPad Layouts + + static func genPadLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) + .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "å", "delete"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ø", "æ", "return"]) + .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "`", "delete"]) + .addRow(["@", "#", "kr", "&", "*", "(", ")", "'", "\"", "+", "·", "return"]) + .addRow(["#+=", "%", "_", "-", "=", "/", ";", ":", ",", ".", "?", "#+="]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .replaceKey(row: 1, column: 2, to: currencyKey) + .build() + } + + static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "'", "delete"]) + .addRow(["€", "$", "£", "^", "[", "]", "{", "}", "―", "ᵒ", "...", "return"]) + .addRow(["123", "§", "|", "~", "≠", "≈", "\\", "<", ">", "!", "?", "123"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 0, to: currencyKeys[0]) + .replaceKey(row: 1, column: 1, to: currencyKeys[1]) + .replaceKey(row: 1, column: 2, to: currencyKeys[2]) + .build() + } + } + + // MARK: Expanded iPad Layouts + + static func genPadExpandedLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["kr", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "+", "´", "delete"]) + .addRow([ + SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "å", "@", "¨" + ]) + .addRow([ + SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "ø", "æ", "'", + "return" + ]) + .addRow(["shift", "*", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadExpandedSymbolKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) + .addRow([ + SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "\"", "|", + "—" + ]) + .addRow([ + SpecialKeys.capsLock, "°", "/", ":", ";", "(", ")", "$", "&", "@", "£", "¥", "~", + "return" + ]) + .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "_", "€", ",", ".", "-", "shift"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .build() } - } - - // MARK: Expanded iPad Layouts - - static func genPadExpandedLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["kr", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "+", "´", "delete"]) - .addRow([SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "å", "@", "¨"]) - .addRow([SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "ø", "æ", "'", "return"]) - .addRow(["shift", "*", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadExpandedSymbolKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) - .addRow([SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "\"", "|", "—"]) - .addRow([SpecialKeys.capsLock, "°", "/", ":", ";", "(", ")", "$", "&", "@", "£", "¥", "~", "return"]) - .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "_", "€", ",", ".", "-", "shift"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .build() - } } // MARK: Get Keys func getNBKeys() { - guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") else { - fatalError("Unable to access shared user defaults") - } - - var currencyKey = NBKeyboardConstants.defaultCurrencyKey - var currencyKeys = NBKeyboardConstants.currencyKeys - let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" - if let currencyValue = userDefaults.string(forKey: dictionaryKey) { - currencyKey = currencyValue - } else { - userDefaults.setValue(currencyKey, forKey: dictionaryKey) - } - if let index = currencyKeys.firstIndex(of: currencyKey) { - currencyKeys.remove(at: index) - } - - if DeviceType.isPhone { - letterKeys = NBKeyboardProvider.genPhoneLetterKeys() - numberKeys = NBKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) - symbolKeys = NBKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) - - leftKeyChars = ["q", "a", "1", "-", "[", "_"] - rightKeyChars = ["å", "æ", "0", "\"", "=", "·"] - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } else { - // Use the expanded keys layout if the iPad is wide enough and has no home button. - if usingExpandedKeyboard { - letterKeys = NBKeyboardProvider.genPadExpandedLetterKeys() - symbolKeys = NBKeyboardProvider.genPadExpandedSymbolKeys() - leftKeyChars = ["kr", "`"] - rightKeyChars = ["¨", "—"] - allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) + guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + else { + fatalError("Unable to access shared user defaults") + } + + var currencyKey = NBKeyboardConstants.defaultCurrencyKey + var currencyKeys = NBKeyboardConstants.currencyKeys + let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" + if let currencyValue = userDefaults.string(forKey: dictionaryKey) { + currencyKey = currencyValue } else { - letterKeys = NBKeyboardProvider.genPadLetterKeys() - numberKeys = NBKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) - symbolKeys = NBKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) - - letterKeys.removeFirst(1) - leftKeyChars = ["q", "a", "1", "@", "€"] - rightKeyChars = ["delete", "return"] - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + userDefaults.setValue(currencyKey, forKey: dictionaryKey) + } + if let index = currencyKeys.firstIndex(of: currencyKey) { + currencyKeys.remove(at: index) } - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } - - keysWithAlternates = NBKeyboardConstants.keysWithAlternates - keysWithAlternatesLeft = NBKeyboardConstants.keysWithAlternatesLeft - keysWithAlternatesRight = NBKeyboardConstants.keysWithAlternatesRight - aAlternateKeys = NBKeyboardConstants.aAlternateKeys - eAlternateKeys = NBKeyboardConstants.eAlternateKeys - iAlternateKeys = NBKeyboardConstants.iAlternateKeys - oAlternateKeys = NBKeyboardConstants.oAlternateKeys - uAlternateKeys = NBKeyboardConstants.uAlternateKeys - yAlternateKeys = NBKeyboardConstants.yAlternateKeys - dAlternateKeys = NBKeyboardConstants.dAlternateKeys - lAlternateKeys = NBKeyboardConstants.lAlternateKeys - nAlternateKeys = NBKeyboardConstants.nAlternateKeys - sAlternateKeys = NBKeyboardConstants.sAlternateKeys + if DeviceType.isPhone { + letterKeys = NBKeyboardProvider.genPhoneLetterKeys() + numberKeys = NBKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) + symbolKeys = NBKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + + leftKeyChars = ["q", "a", "1", "-", "[", "_"] + rightKeyChars = ["å", "æ", "0", "\"", "=", "·"] + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } + } else { + // Use the expanded keys layout if the iPad is wide enough and has no home button. + if usingExpandedKeyboard { + letterKeys = NBKeyboardProvider.genPadExpandedLetterKeys() + symbolKeys = NBKeyboardProvider.genPadExpandedSymbolKeys() + leftKeyChars = ["kr", "`"] + rightKeyChars = ["¨", "—"] + allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) + } else { + letterKeys = NBKeyboardProvider.genPadLetterKeys() + numberKeys = NBKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) + symbolKeys = NBKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) + + letterKeys.removeFirst(1) + leftKeyChars = ["q", "a", "1", "@", "€"] + rightKeyChars = ["delete", "return"] + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + } + + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } + } + + keysWithAlternates = NBKeyboardConstants.keysWithAlternates + keysWithAlternatesLeft = NBKeyboardConstants.keysWithAlternatesLeft + keysWithAlternatesRight = NBKeyboardConstants.keysWithAlternatesRight + aAlternateKeys = NBKeyboardConstants.aAlternateKeys + eAlternateKeys = NBKeyboardConstants.eAlternateKeys + iAlternateKeys = NBKeyboardConstants.iAlternateKeys + oAlternateKeys = NBKeyboardConstants.oAlternateKeys + uAlternateKeys = NBKeyboardConstants.uAlternateKeys + yAlternateKeys = NBKeyboardConstants.yAlternateKeys + dAlternateKeys = NBKeyboardConstants.dAlternateKeys + lAlternateKeys = NBKeyboardConstants.lAlternateKeys + nAlternateKeys = NBKeyboardConstants.nAlternateKeys + sAlternateKeys = NBKeyboardConstants.sAlternateKeys } + // MARK: Provide Layout func setNBKeyboardLayout() { - getNBKeys() - - currencySymbol = "kr" - currencySymbolAlternates = kronaAlternateKeys - spaceBar = "mellomrom" - language = "Norsk" - - invalidCommandMsgWikidata = "Ikke i Wikidata" - invalidCommandTextWikidata1 = "Wikidata er en samarbeidsredigert kunnskapsgraf som vedlikeholdes av Wikimedia Foundation. Den fungerer som en kilde til åpne data for prosjekter som Wikipedia og utallige andre." - invalidCommandTextWikidata2 = "Scribe bruker Wikidatas språkdata for mange av kjernefunksjonene. Vi får informasjon som substantivkjønn, verbkonjugasjoner og mye mer!" - invalidCommandTextWikidata3 = "Du kan opprette en konto på wikidata.org for å bli med i fellesskapet som støtter Scribe og mange andre prosjekter. Hjelp oss å bringe gratis informasjon til verden!" - - invalidCommandMsgWiktionary = "Ikke i Wiktionary" - invalidCommandTextWiktionary1 = "Wiktionary er en samarbeidsredigert ordbok som vedlikeholdes av Wikimedia Foundation. Den fungerer som en kilde til gratis språkdata for prosjekter som Wikipedia og utallige andre." - invalidCommandTextWiktionary2 = "Scribe bruker Wiktionarys data for å tilby oversettelser for sin Oversett-kommando. Dataene våre er hentet fra de mange språkparene som Wiktionarys fellesskap har laget!" - invalidCommandTextWiktionary3 = "Du kan opprette en konto på wiktionary.org for å bli med i fellesskapet som støtter Scribe og mange andre prosjekter. Hjelp oss å bringe gratis informasjon til verden!" - - baseAutosuggestions = ["jeg", "det", "er"] - numericAutosuggestions = ["prosent", "millioner", "meter"] - verbsAfterPronounsArray = ["har", "være", "kan"] - pronounAutosuggestionTenses = [ - "jeg": "presFPS", - "du": "presSPS", - "han": "presTPS", - "hun": "presTPS", - "den": "presTPS", - "det": "presTPS", - "vi": "presFPP", - "dere": "presSPP", - "de": "presTPP" - ] - - translateKeyLbl = "Oversett" - translatePlaceholder = "Skriv inn et ord" - translatePrompt = commandPromptSpacing + "nb → \(getControllerLanguageAbbr()): " - translatePromptAndCursor = translatePrompt + commandCursor - translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder - translatePromptAndColorPlaceholder = NSMutableAttributedString(string: translatePromptAndPlaceholder) - translatePromptAndColorPlaceholder.setColorForText(textForAttribute: translatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - conjugateKeyLbl = "Bøy" - conjugatePlaceholder = "Skriv inn et verb" - conjugatePrompt = commandPromptSpacing + "Bøy: " - conjugatePromptAndCursor = conjugatePrompt + commandCursor - conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder - conjugatePromptAndColorPlaceholder = NSMutableAttributedString(string: conjugatePromptAndPlaceholder) - conjugatePromptAndColorPlaceholder.setColorForText(textForAttribute: conjugatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - pluralKeyLbl = "Flertall" - pluralPlaceholder = "Skriv inn et substantiv" - pluralPrompt = commandPromptSpacing + "Flertall: " - pluralPromptAndCursor = pluralPrompt + commandCursor - pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder - pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) - pluralPromptAndColorPlaceholder.setColorForText(textForAttribute: pluralPlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - alreadyPluralMsg = "Allerede flertall" + getNBKeys() + + currencySymbol = "kr" + currencySymbolAlternates = kronaAlternateKeys + spaceBar = "mellomrom" + language = "Norsk" + + invalidCommandMsgWikidata = "Ikke i Wikidata" + invalidCommandTextWikidata1 = + "Wikidata er en samarbeidsredigert kunnskapsgraf som vedlikeholdes av Wikimedia Foundation. Den fungerer som en kilde til åpne data for prosjekter som Wikipedia og utallige andre." + invalidCommandTextWikidata2 = + "Scribe bruker Wikidatas språkdata for mange av kjernefunksjonene. Vi får informasjon som substantivkjønn, verbkonjugasjoner og mye mer!" + invalidCommandTextWikidata3 = + "Du kan opprette en konto på wikidata.org for å bli med i fellesskapet som støtter Scribe og mange andre prosjekter. Hjelp oss å bringe gratis informasjon til verden!" + + invalidCommandMsgWiktionary = "Ikke i Wiktionary" + invalidCommandTextWiktionary1 = + "Wiktionary er en samarbeidsredigert ordbok som vedlikeholdes av Wikimedia Foundation. Den fungerer som en kilde til gratis språkdata for prosjekter som Wikipedia og utallige andre." + invalidCommandTextWiktionary2 = + "Scribe bruker Wiktionarys data for å tilby oversettelser for sin Oversett-kommando. Dataene våre er hentet fra de mange språkparene som Wiktionarys fellesskap har laget!" + invalidCommandTextWiktionary3 = + "Du kan opprette en konto på wiktionary.org for å bli med i fellesskapet som støtter Scribe og mange andre prosjekter. Hjelp oss å bringe gratis informasjon til verden!" + + baseAutosuggestions = ["jeg", "det", "er"] + numericAutosuggestions = ["prosent", "millioner", "meter"] + verbsAfterPronounsArray = ["har", "være", "kan"] + pronounAutosuggestionTenses = [ + "jeg": "presFPS", + "du": "presSPS", + "han": "presTPS", + "hun": "presTPS", + "den": "presTPS", + "det": "presTPS", + "vi": "presFPP", + "dere": "presSPP", + "de": "presTPP" + ] + + translateKeyLbl = "Oversett" + translatePlaceholder = "Skriv inn et ord" + translatePrompt = commandPromptSpacing + "nb → \(getControllerLanguageAbbr()): " + translatePromptAndCursor = translatePrompt + commandCursor + translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder + translatePromptAndColorPlaceholder = NSMutableAttributedString( + string: translatePromptAndPlaceholder + ) + translatePromptAndColorPlaceholder.setColorForText( + textForAttribute: translatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + conjugateKeyLbl = "Bøy" + conjugatePlaceholder = "Skriv inn et verb" + conjugatePrompt = commandPromptSpacing + "Bøy: " + conjugatePromptAndCursor = conjugatePrompt + commandCursor + conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder + conjugatePromptAndColorPlaceholder = NSMutableAttributedString( + string: conjugatePromptAndPlaceholder + ) + conjugatePromptAndColorPlaceholder.setColorForText( + textForAttribute: conjugatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + pluralKeyLbl = "Flertall" + pluralPlaceholder = "Skriv inn et substantiv" + pluralPrompt = commandPromptSpacing + "Flertall: " + pluralPromptAndCursor = pluralPrompt + commandCursor + pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder + pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) + pluralPromptAndColorPlaceholder.setColorForText( + textForAttribute: pluralPlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + alreadyPluralMsg = "Allerede flertall" } diff --git a/Keyboards/LanguageKeyboards/Norwegian/NBKeyboardViewController.swift b/Keyboards/LanguageKeyboards/Norwegian/NBKeyboardViewController.swift index c72d816a..52fae0c4 100644 --- a/Keyboards/LanguageKeyboards/Norwegian/NBKeyboardViewController.swift +++ b/Keyboards/LanguageKeyboards/Norwegian/NBKeyboardViewController.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Class for the Norwegian Bokmål Scribe keyboard. */ diff --git a/Keyboards/LanguageKeyboards/Portuguese/PTInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Portuguese/PTInterfaceVariables.swift index d48f85ea..eeee3642 100644 --- a/Keyboards/LanguageKeyboards/Portuguese/PTInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Portuguese/PTInterfaceVariables.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Constants and functions to load the Portuguese Scribe keyboard. */ @@ -9,235 +9,274 @@ import UIKit // MARK: Constants public enum PTKeyboardConstants { - static let defaultCurrencyKey = "€" - static let currencyKeys = ["€", "$", "£", "¥"] - - // Alternate key vars. - static let keysWithAlternates = ["a", "e", "i", "o", "u", "c", "n"] - static let keysWithAlternatesLeft = ["a", "e", "c"] - static let keysWithAlternatesRight = ["i", "o", "u", "n"] - - static let aAlternateKeys = ["á", "ã", "à", "â", "ä", "å", "æ", "ᵃ"] - static let eAlternateKeys = ["é", "ê", "è", "ę", "ė", "ē", "ë"] - static let iAlternateKeys = ["ī", "į", "ï", "ì", "î", "í"] - static let oAlternateKeys = ["ᵒ", "ō", "ø", "œ", "ö", "ò", "ô", "õ", "ó"] - static let uAlternateKeys = ["ū", "û", "ù", "ü", "ú"] - static let cAlternateKeys = ["ç"] - static let nAlternateKeys = ["ñ"] + static let defaultCurrencyKey = "€" + static let currencyKeys = ["€", "$", "£", "¥"] + + // Alternate key vars. + static let keysWithAlternates = ["a", "e", "i", "o", "u", "c", "n"] + static let keysWithAlternatesLeft = ["a", "e", "c"] + static let keysWithAlternatesRight = ["i", "o", "u", "n"] + + static let aAlternateKeys = ["á", "ã", "à", "â", "ä", "å", "æ", "ᵃ"] + static let eAlternateKeys = ["é", "ê", "è", "ę", "ė", "ē", "ë"] + static let iAlternateKeys = ["ī", "į", "ï", "ì", "î", "í"] + static let oAlternateKeys = ["ᵒ", "ō", "ø", "œ", "ö", "ò", "ô", "õ", "ó"] + static let uAlternateKeys = ["ū", "û", "ù", "ü", "ú"] + static let cAlternateKeys = ["ç"] + static let nAlternateKeys = ["ñ"] } struct PTKeyboardProvider: KeyboardProviderProtocol { - // MARK: iPhone Layouts - - static func genPhoneLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l"]) - .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "delete"]) - .addRow(["123", "selectKeyboard", "space", "return"]) - .build() - } - - static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) - .addRow(["-", "/", ":", ";", "(", ")", "€", "&", "@", "\""]) - .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - .replaceKey(row: 1, column: 6, to: currencyKey) - .build() - } - - static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) - .addRow(["_", "\\", "|", "~", "<", ">", "$", "£", "¥", "·"]) - .addRow(["123", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 6, to: currencyKeys[0]) - .replaceKey(row: 1, column: 7, to: currencyKeys[1]) - .replaceKey(row: 1, column: 8, to: currencyKeys[2]) - .build() + // MARK: iPhone Layouts + + static func genPhoneLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l"]) + .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "delete"]) + .addRow(["123", "selectKeyboard", "space", "return"]) + .build() } - } - - // MARK: iPad Layouts - - static func genPadLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) - .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "delete"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "return"]) - .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "!", "?", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) - .addRow(["@", "#", "$", "&", "*", "(", ")", "'", "\"", "return"]) - .addRow(["#+=", "%", "-", "+", "=", "/", ";", ":", ",", ".", "#+="]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .replaceKey(row: 1, column: 2, to: currencyKey) - .build() - } - - static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) - .addRow(["€", "£", "¥", "_", "^", "[", "]", "{", "}", "return"]) - .addRow(["123", "§", "|", "~", "...", "\\", "<", ">", "!", "?", "123"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 0, to: currencyKeys[0]) - .replaceKey(row: 1, column: 1, to: currencyKeys[1]) - .replaceKey(row: 1, column: 2, to: currencyKeys[2]) - .build() + + static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) + .addRow(["-", "/", ":", ";", "(", ")", "€", "&", "@", "\""]) + .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + .replaceKey(row: 1, column: 6, to: currencyKey) + .build() + } + + static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) + .addRow(["_", "\\", "|", "~", "<", ">", "$", "£", "¥", "·"]) + .addRow(["123", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 6, to: currencyKeys[0]) + .replaceKey(row: 1, column: 7, to: currencyKeys[1]) + .replaceKey(row: 1, column: 8, to: currencyKeys[2]) + .build() + } + } + + // MARK: iPad Layouts + + static func genPadLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) + .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "delete"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "return"]) + .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "!", "?", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) + .addRow(["@", "#", "$", "&", "*", "(", ")", "'", "\"", "return"]) + .addRow(["#+=", "%", "-", "+", "=", "/", ";", ":", ",", ".", "#+="]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .replaceKey(row: 1, column: 2, to: currencyKey) + .build() + } + + static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) + .addRow(["€", "£", "¥", "_", "^", "[", "]", "{", "}", "return"]) + .addRow(["123", "§", "|", "~", "...", "\\", "<", ">", "!", "?", "123"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 0, to: currencyKeys[0]) + .replaceKey(row: 1, column: 1, to: currencyKeys[1]) + .replaceKey(row: 1, column: 2, to: currencyKeys[2]) + .build() + } + } + + // MARK: Expanded iPad Layouts + + static func genPadExpandedLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["~", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "delete"]) + .addRow([ + SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]", + "\\" + ]) + .addRow([ + SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", ":", ";", "ç", + "return" + ]) + .addRow(["shift", "'", "z", "x", "c", "v", "b", "n", "m", ",", ".", "/", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadExpandedSymbolKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) + .addRow([ + SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "—", "|", "~" + ]) + .addRow([ + SpecialKeys.capsLock, "°", "\\", ":", ";", "(", ")", "&", "@", "$", "£", "¥", "€", + "return" + ]) + .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "_", "-", ",", ".", "/", "shift"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .build() } - } - - // MARK: Expanded iPad Layouts - - static func genPadExpandedLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["~", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "delete"]) - .addRow([SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]", "\\"]) - .addRow([SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", ":", ";", "ç", "return"]) - .addRow(["shift", "'", "z", "x", "c", "v", "b", "n", "m", ",", ".", "/", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadExpandedSymbolKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) - .addRow([SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "—", "|", "~"]) - .addRow([SpecialKeys.capsLock, "°", "\\", ":", ";", "(", ")", "&", "@", "$", "£", "¥", "€", "return"]) - .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "_", "-", ",", ".", "/", "shift"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .build() - } } // MARK: Get Keys func getPTKeys() { - guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") else { - fatalError("Unable to access shared user defaults") - } - - var currencyKey = PTKeyboardConstants.defaultCurrencyKey - var currencyKeys = PTKeyboardConstants.currencyKeys - let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" - if let currencyValue = userDefaults.string(forKey: dictionaryKey) { - currencyKey = currencyValue - } else { - userDefaults.setValue(currencyKey, forKey: dictionaryKey) - } - if let index = currencyKeys.firstIndex(of: currencyKey) { - currencyKeys.remove(at: index) - } - - if DeviceType.isPhone { - letterKeys = PTKeyboardProvider.genPhoneLetterKeys() - numberKeys = PTKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) - symbolKeys = PTKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) - - leftKeyChars = ["q", "1", "-", "[", "_"] - rightKeyChars = ["p", "0", "\"", "=", "·"] - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } else { - // Use the expanded keys layout if the iPad is wide enough and has no home button. - if usingExpandedKeyboard { - letterKeys = PTKeyboardProvider.genPadExpandedLetterKeys() - symbolKeys = PTKeyboardProvider.genPadExpandedSymbolKeys() - - leftKeyChars = ["~", "`"] - rightKeyChars = ["\\", "~"] - allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) - } else { - letterKeys = PTKeyboardProvider.genPadLetterKeys() - numberKeys = PTKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) - symbolKeys = PTKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) + guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + else { + fatalError("Unable to access shared user defaults") + } - letterKeys.removeFirst(1) + var currencyKey = PTKeyboardConstants.defaultCurrencyKey + var currencyKeys = PTKeyboardConstants.currencyKeys + let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" + if let currencyValue = userDefaults.string(forKey: dictionaryKey) { + currencyKey = currencyValue + } else { + userDefaults.setValue(currencyKey, forKey: dictionaryKey) + } + if let index = currencyKeys.firstIndex(of: currencyKey) { + currencyKeys.remove(at: index) + } - leftKeyChars = ["q", "1", "$"] - rightKeyChars = [] - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + if DeviceType.isPhone { + letterKeys = PTKeyboardProvider.genPhoneLetterKeys() + numberKeys = PTKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) + symbolKeys = PTKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + + leftKeyChars = ["q", "1", "-", "[", "_"] + rightKeyChars = ["p", "0", "\"", "=", "·"] + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } + } else { + // Use the expanded keys layout if the iPad is wide enough and has no home button. + if usingExpandedKeyboard { + letterKeys = PTKeyboardProvider.genPadExpandedLetterKeys() + symbolKeys = PTKeyboardProvider.genPadExpandedSymbolKeys() + + leftKeyChars = ["~", "`"] + rightKeyChars = ["\\", "~"] + allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) + } else { + letterKeys = PTKeyboardProvider.genPadLetterKeys() + numberKeys = PTKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) + symbolKeys = PTKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) + + letterKeys.removeFirst(1) + + leftKeyChars = ["q", "1", "$"] + rightKeyChars = [] + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + } + + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } } - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } - - keysWithAlternates = PTKeyboardConstants.keysWithAlternates - keysWithAlternatesLeft = PTKeyboardConstants.keysWithAlternatesLeft - keysWithAlternatesRight = PTKeyboardConstants.keysWithAlternatesRight - aAlternateKeys = PTKeyboardConstants.aAlternateKeys - eAlternateKeys = PTKeyboardConstants.eAlternateKeys - iAlternateKeys = PTKeyboardConstants.iAlternateKeys - oAlternateKeys = PTKeyboardConstants.oAlternateKeys - uAlternateKeys = PTKeyboardConstants.uAlternateKeys - cAlternateKeys = PTKeyboardConstants.cAlternateKeys - nAlternateKeys = PTKeyboardConstants.nAlternateKeys + keysWithAlternates = PTKeyboardConstants.keysWithAlternates + keysWithAlternatesLeft = PTKeyboardConstants.keysWithAlternatesLeft + keysWithAlternatesRight = PTKeyboardConstants.keysWithAlternatesRight + aAlternateKeys = PTKeyboardConstants.aAlternateKeys + eAlternateKeys = PTKeyboardConstants.eAlternateKeys + iAlternateKeys = PTKeyboardConstants.iAlternateKeys + oAlternateKeys = PTKeyboardConstants.oAlternateKeys + uAlternateKeys = PTKeyboardConstants.uAlternateKeys + cAlternateKeys = PTKeyboardConstants.cAlternateKeys + nAlternateKeys = PTKeyboardConstants.nAlternateKeys } // MARK: Provide Layout func setPTKeyboardLayout() { - getPTKeys() - - currencySymbol = "$" - currencySymbolAlternates = dollarAlternateKeys - spaceBar = "espaço" - language = "Português" - - invalidCommandMsgWikidata = "Não está no Wikidata" - invalidCommandTextWikidata1 = "A Wikidata é um grafo de conhecimento editado colaborativamente e mantivo pela Fundação Wikimedia. A Wikidata é uma fonte de dados públicos para projetos como a Wikipédia e muitos outros." - invalidCommandTextWikidata2 = "O Scribe usa dados linguísticos da Wikidata para muitas de suas funcionalidades. Temos informações sobre gêneros de substantivos, conjugações de verbos, e muito mais!" - invalidCommandTextWikidata3 = "Você pode criar uma conta em wikidata.org e se juntar à comunidade que apoia o Scribe e muitos outros projetos. Ajude-nos a fornecer dados gratuitos para o mundo!" - - invalidCommandMsgWiktionary = "Não está no Wiktionary" - invalidCommandTextWiktionary1 = "O Wikcionário é um dicionário editado colaborativamente e mantido pela Fundação Wikimedia. Ele serve como fonte de dados linguísticos gratuitos para projetos como a Wikipédia e inúmeros outros." - invalidCommandTextWiktionary2 = "O Scribe utiliza os dados do Wikcionário para fornecer traduções para o seu comando Traduzir. Nossos dados são derivados dos diversos pares de idiomas criados pela comunidade do Wikcionário!" - invalidCommandTextWiktionary3 = "Você pode criar uma conta em wiktionary.org para se juntar à comunidade que apoia o Scribe e muitos outros projetos. Ajude-nos a levar informação gratuita para o mundo!" - - baseAutosuggestions = ["o", "a", "eu"] - numericAutosuggestions = ["de", "que", "a"] - - translateKeyLbl = "Traduzir" - translatePlaceholder = "Digite uma palavra" - translatePrompt = commandPromptSpacing + "pt -› \(getControllerLanguageAbbr()): " - translatePromptAndCursor = translatePrompt + commandCursor - translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder - translatePromptAndColorPlaceholder = NSMutableAttributedString(string: translatePromptAndPlaceholder) - translatePromptAndColorPlaceholder.setColorForText(textForAttribute: translatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - conjugateKeyLbl = "Conjugar" - conjugatePlaceholder = "Digite um verbo" - conjugatePrompt = commandPromptSpacing + "Conjugar: " - conjugatePromptAndCursor = conjugatePrompt + commandCursor - conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder - conjugatePromptAndColorPlaceholder = NSMutableAttributedString(string: conjugatePromptAndPlaceholder) - conjugatePromptAndColorPlaceholder.setColorForText(textForAttribute: conjugatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - pluralKeyLbl = "Plural" - pluralPlaceholder = "Digite um substantivo" - pluralPrompt = commandPromptSpacing + "Plural: " - pluralPromptAndCursor = pluralPrompt + commandCursor - pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder - pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) - pluralPromptAndColorPlaceholder.setColorForText(textForAttribute: pluralPlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - alreadyPluralMsg = "Já plural" + getPTKeys() + + currencySymbol = "$" + currencySymbolAlternates = dollarAlternateKeys + spaceBar = "espaço" + language = "Português" + + invalidCommandMsgWikidata = "Não está no Wikidata" + invalidCommandTextWikidata1 = + "A Wikidata é um grafo de conhecimento editado colaborativamente e mantivo pela Fundação Wikimedia. A Wikidata é uma fonte de dados públicos para projetos como a Wikipédia e muitos outros." + invalidCommandTextWikidata2 = + "O Scribe usa dados linguísticos da Wikidata para muitas de suas funcionalidades. Temos informações sobre gêneros de substantivos, conjugações de verbos, e muito mais!" + invalidCommandTextWikidata3 = + "Você pode criar uma conta em wikidata.org e se juntar à comunidade que apoia o Scribe e muitos outros projetos. Ajude-nos a fornecer dados gratuitos para o mundo!" + + invalidCommandMsgWiktionary = "Não está no Wiktionary" + invalidCommandTextWiktionary1 = + "O Wikcionário é um dicionário editado colaborativamente e mantido pela Fundação Wikimedia. Ele serve como fonte de dados linguísticos gratuitos para projetos como a Wikipédia e inúmeros outros." + invalidCommandTextWiktionary2 = + "O Scribe utiliza os dados do Wikcionário para fornecer traduções para o seu comando Traduzir. Nossos dados são derivados dos diversos pares de idiomas criados pela comunidade do Wikcionário!" + invalidCommandTextWiktionary3 = + "Você pode criar uma conta em wiktionary.org para se juntar à comunidade que apoia o Scribe e muitos outros projetos. Ajude-nos a levar informação gratuita para o mundo!" + + baseAutosuggestions = ["o", "a", "eu"] + numericAutosuggestions = ["de", "que", "a"] + + translateKeyLbl = "Traduzir" + translatePlaceholder = "Digite uma palavra" + translatePrompt = commandPromptSpacing + "pt -› \(getControllerLanguageAbbr()): " + translatePromptAndCursor = translatePrompt + commandCursor + translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder + translatePromptAndColorPlaceholder = NSMutableAttributedString( + string: translatePromptAndPlaceholder + ) + translatePromptAndColorPlaceholder.setColorForText( + textForAttribute: translatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + conjugateKeyLbl = "Conjugar" + conjugatePlaceholder = "Digite um verbo" + conjugatePrompt = commandPromptSpacing + "Conjugar: " + conjugatePromptAndCursor = conjugatePrompt + commandCursor + conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder + conjugatePromptAndColorPlaceholder = NSMutableAttributedString( + string: conjugatePromptAndPlaceholder + ) + conjugatePromptAndColorPlaceholder.setColorForText( + textForAttribute: conjugatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + pluralKeyLbl = "Plural" + pluralPlaceholder = "Digite um substantivo" + pluralPrompt = commandPromptSpacing + "Plural: " + pluralPromptAndCursor = pluralPrompt + commandCursor + pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder + pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) + pluralPromptAndColorPlaceholder.setColorForText( + textForAttribute: pluralPlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + alreadyPluralMsg = "Já plural" } diff --git a/Keyboards/LanguageKeyboards/Portuguese/PTKeyboardViewController.swift b/Keyboards/LanguageKeyboards/Portuguese/PTKeyboardViewController.swift index a912e4e8..b7ae9dc0 100644 --- a/Keyboards/LanguageKeyboards/Portuguese/PTKeyboardViewController.swift +++ b/Keyboards/LanguageKeyboards/Portuguese/PTKeyboardViewController.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Class for the Portuguese Scribe keyboard. */ diff --git a/Keyboards/LanguageKeyboards/Russian/RUInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Russian/RUInterfaceVariables.swift index 88b35866..40e17c4c 100644 --- a/Keyboards/LanguageKeyboards/Russian/RUInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Russian/RUInterfaceVariables.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Commands and functions to load the Russian Scribe keyboard. */ @@ -9,225 +9,264 @@ import UIKit // MARK: Constants public enum RUKeyboardConstants { - static let defaultCurrencyKey = "₽" - static let currencyKeys = ["₽", "$", "€", "£"] + static let defaultCurrencyKey = "₽" + static let currencyKeys = ["₽", "$", "€", "£"] - // Alternate key vars. - static let keysWithAlternates = ["е", "ь"] - static let keysWithAlternatesLeft = ["е"] - static let keysWithAlternatesRight = ["ь"] + // Alternate key vars. + static let keysWithAlternates = ["е", "ь"] + static let keysWithAlternatesLeft = ["е"] + static let keysWithAlternatesRight = ["ь"] - static let еAlternateKeys = ["ë"] - static let ьAlternateKeys = ["Ъ"] + static let еAlternateKeys = ["ë"] + static let ьAlternateKeys = ["Ъ"] } struct RUKeyboardProvider: KeyboardProviderProtocol { - // MARK: iPhone Layouts - - static func genPhoneLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["й", "ц", "у", "к", "е", "н", "г", "ш", "щ", "з", "х"]) - .addRow(["ф", "ы", "в", "а", "п", "р", "о", "л", "д", "ж", "э"]) - .addRow(["shift", "я", "ч", "с", "м", "и", "т", "ь", "б", "ю", "delete"]) - .addRow(["123", "selectKeyboard", "space", "return"]) - .build() - } - - static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) - .addRow(["-", "/", ":", ";", "(", ")", "₽", "&", "@", "\""]) - .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) - .addRow(["АБВ", "selectKeyboard", "space", "return"]) - .replaceKey(row: 1, column: 6, to: currencyKey) - .build() - } - - static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) - .addRow(["_", "\\", "|", "~", "<", ">", "$", "€", "£", "·"]) - .addRow(["123", ".", ",", "?", "!", "'", "delete"]) - .addRow(["АБВ", "selectKeyboard", "space", "return"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 6, to: currencyKeys[0]) - .replaceKey(row: 1, column: 7, to: currencyKeys[1]) - .replaceKey(row: 1, column: 8, to: currencyKeys[2]) - .build() + // MARK: iPhone Layouts + + static func genPhoneLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["й", "ц", "у", "к", "е", "н", "г", "ш", "щ", "з", "х"]) + .addRow(["ф", "ы", "в", "а", "п", "р", "о", "л", "д", "ж", "э"]) + .addRow(["shift", "я", "ч", "с", "м", "и", "т", "ь", "б", "ю", "delete"]) + .addRow(["123", "selectKeyboard", "space", "return"]) + .build() } - } - - // MARK: iPad Layouts - - static func genPadLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) - .addRow(["й", "ц", "у", "к", "е", "н", "г", "ш", "щ", "з", "х", "delete"]) - .addRow(["ф", "ы", "в", "а", "п", "р", "о", "л", "д", "ж", "э", "return"]) - .addRow(["shift", "я", "ч", "с", "м", "и", "т", "ь", "б", "ю", ".", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "—", "delete"]) - .addRow(["@", "#", "№", "₽", "ʼ", "&", "*", "(", ")", "'", "\"", "return"]) - .addRow(["#+=", "%", "_", "-", "+", "=", "≠", ";", ":", ",", ".", "#+="]) - .addRow(["selectKeyboard", "АБВ", "space", "АБВ", "hideKeyboard"]) - .replaceKey(row: 1, column: 3, to: currencyKey) - .build() - } - - static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "—", "delete"]) - .addRow(["$", "€", "£", "¥", "±", "·", "`", "[", "]", "{", "}", "return"]) - .addRow(["123", "§", "|", "~", "...", "^", "\\", "<", ">", "!", "?", "123"]) - .addRow(["selectKeyboard", "АБВ", "space", "АБВ", "hideKeyboard"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 0, to: currencyKeys[0]) - .replaceKey(row: 1, column: 1, to: currencyKeys[1]) - .replaceKey(row: 1, column: 2, to: currencyKeys[2]) - .build() + + static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) + .addRow(["-", "/", ":", ";", "(", ")", "₽", "&", "@", "\""]) + .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) + .addRow(["АБВ", "selectKeyboard", "space", "return"]) + .replaceKey(row: 1, column: 6, to: currencyKey) + .build() + } + + static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) + .addRow(["_", "\\", "|", "~", "<", ">", "$", "€", "£", "·"]) + .addRow(["123", ".", ",", "?", "!", "'", "delete"]) + .addRow(["АБВ", "selectKeyboard", "space", "return"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 6, to: currencyKeys[0]) + .replaceKey(row: 1, column: 7, to: currencyKeys[1]) + .replaceKey(row: 1, column: 8, to: currencyKeys[2]) + .build() + } + } + + // MARK: iPad Layouts + + static func genPadLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) + .addRow(["й", "ц", "у", "к", "е", "н", "г", "ш", "щ", "з", "х", "delete"]) + .addRow(["ф", "ы", "в", "а", "п", "р", "о", "л", "д", "ж", "э", "return"]) + .addRow(["shift", "я", "ч", "с", "м", "и", "т", "ь", "б", "ю", ".", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "—", "delete"]) + .addRow(["@", "#", "№", "₽", "ʼ", "&", "*", "(", ")", "'", "\"", "return"]) + .addRow(["#+=", "%", "_", "-", "+", "=", "≠", ";", ":", ",", ".", "#+="]) + .addRow(["selectKeyboard", "АБВ", "space", "АБВ", "hideKeyboard"]) + .replaceKey(row: 1, column: 3, to: currencyKey) + .build() + } + + static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "—", "delete"]) + .addRow(["$", "€", "£", "¥", "±", "·", "`", "[", "]", "{", "}", "return"]) + .addRow(["123", "§", "|", "~", "...", "^", "\\", "<", ">", "!", "?", "123"]) + .addRow(["selectKeyboard", "АБВ", "space", "АБВ", "hideKeyboard"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 0, to: currencyKeys[0]) + .replaceKey(row: 1, column: 1, to: currencyKeys[1]) + .replaceKey(row: 1, column: 2, to: currencyKeys[2]) + .build() + } + } + + // MARK: Expanded iPad Layouts + + static func genPadExpandedLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["§", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "delete"]) + .addRow([ + SpecialKeys.indent, "й", "ц", "у", "к", "е", "н", "г", "ш", "щ", "з", "х", "ъ", "+" + ]) + .addRow([ + SpecialKeys.capsLock, "ф", "ы", "в", "а", "п", "р", "о", "л", "д", "ж", "э", "ё", + "return" + ]) + .addRow(["shift", "'", "я", "ч", "с", "м", "и", "т", "ь", "б", "ю", "/", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadExpandedSymbolKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) + .addRow([ + SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "\\", "|", + "₽" + ]) + .addRow([ + SpecialKeys.capsLock, "—", "/", ":", ";", "(", ")", "&", "@", "$", "£", "¥", "~", + "return" + ]) + .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "_", "€", "-", ",", ".", "shift"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .build() } - } - - // MARK: Expanded iPad Layouts - - static func genPadExpandedLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["§", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "delete"]) - .addRow([SpecialKeys.indent, "й", "ц", "у", "к", "е", "н", "г", "ш", "щ", "з", "х", "ъ", "+"]) - .addRow([SpecialKeys.capsLock, "ф", "ы", "в", "а", "п", "р", "о", "л", "д", "ж", "э", "ё", "return"]) - .addRow(["shift", "'", "я", "ч", "с", "м", "и", "т", "ь", "б", "ю", "/", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadExpandedSymbolKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) - .addRow([SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "\\", "|", "₽"]) - .addRow([SpecialKeys.capsLock, "—", "/", ":", ";", "(", ")", "&", "@", "$", "£", "¥", "~", "return"]) - .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "_", "€", "-", ",", ".", "shift"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .build() - } } // MARK: Get Keys func getRUKeys() { - guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") else { - fatalError("Unable to access shared user defaults") - } - - var currencyKey = RUKeyboardConstants.defaultCurrencyKey - var currencyKeys = RUKeyboardConstants.currencyKeys - let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" - if let currencyValue = userDefaults.string(forKey: dictionaryKey) { - currencyKey = currencyValue - } else { - userDefaults.setValue(currencyKey, forKey: dictionaryKey) - } - if let index = currencyKeys.firstIndex(of: currencyKey) { - currencyKeys.remove(at: index) - } - - if DeviceType.isPhone { - letterKeys = RUKeyboardProvider.genPhoneLetterKeys() - numberKeys = RUKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) - symbolKeys = RUKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) - - leftKeyChars = ["й", "ф", "1", "-", "[", "_"] - rightKeyChars = ["х", "э", "0", "\"", "=", "·"] - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } else { - // Use the expanded keys layout if the iPad is wide enough and has no home button. - if usingExpandedKeyboard { - letterKeys = RUKeyboardProvider.genPadExpandedLetterKeys() - symbolKeys = RUKeyboardProvider.genPadExpandedSymbolKeys() - - leftKeyChars = ["§", "`"] - rightKeyChars = ["+", "₽"] - allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) - } else { - letterKeys = RUKeyboardProvider.genPadLetterKeys() - numberKeys = RUKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) - symbolKeys = RUKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) - - letterKeys.removeFirst(1) + guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + else { + fatalError("Unable to access shared user defaults") + } - leftKeyChars = ["й", "ф", "1", "@", "$"] - rightKeyChars = [] - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + var currencyKey = RUKeyboardConstants.defaultCurrencyKey + var currencyKeys = RUKeyboardConstants.currencyKeys + let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" + if let currencyValue = userDefaults.string(forKey: dictionaryKey) { + currencyKey = currencyValue + } else { + userDefaults.setValue(currencyKey, forKey: dictionaryKey) + } + if let index = currencyKeys.firstIndex(of: currencyKey) { + currencyKeys.remove(at: index) } - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } + if DeviceType.isPhone { + letterKeys = RUKeyboardProvider.genPhoneLetterKeys() + numberKeys = RUKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) + symbolKeys = RUKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + + leftKeyChars = ["й", "ф", "1", "-", "[", "_"] + rightKeyChars = ["х", "э", "0", "\"", "=", "·"] + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } + } else { + // Use the expanded keys layout if the iPad is wide enough and has no home button. + if usingExpandedKeyboard { + letterKeys = RUKeyboardProvider.genPadExpandedLetterKeys() + symbolKeys = RUKeyboardProvider.genPadExpandedSymbolKeys() + + leftKeyChars = ["§", "`"] + rightKeyChars = ["+", "₽"] + allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) + } else { + letterKeys = RUKeyboardProvider.genPadLetterKeys() + numberKeys = RUKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) + symbolKeys = RUKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) + + letterKeys.removeFirst(1) + + leftKeyChars = ["й", "ф", "1", "@", "$"] + rightKeyChars = [] + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + } + + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } + } - keysWithAlternates = RUKeyboardConstants.keysWithAlternates - keysWithAlternatesLeft = RUKeyboardConstants.keysWithAlternatesLeft - keysWithAlternatesRight = RUKeyboardConstants.keysWithAlternatesRight - еAlternateKeys = RUKeyboardConstants.еAlternateKeys - ьAlternateKeys = RUKeyboardConstants.ьAlternateKeys + keysWithAlternates = RUKeyboardConstants.keysWithAlternates + keysWithAlternatesLeft = RUKeyboardConstants.keysWithAlternatesLeft + keysWithAlternatesRight = RUKeyboardConstants.keysWithAlternatesRight + еAlternateKeys = RUKeyboardConstants.еAlternateKeys + ьAlternateKeys = RUKeyboardConstants.ьAlternateKeys } // MARK: Provide Layout func setRUKeyboardLayout() { - getRUKeys() - - currencySymbol = "₽" - currencySymbolAlternates = roubleAlternateKeys - spaceBar = "Пробел" - language = "Pусский" - - invalidCommandMsgWikidata = "Нет в Викиданных" - invalidCommandTextWikidata1 = "Wikidata — это совместно редактируемый граф знаний, поддерживаемый Фондом Викимедиа. Он служит источником открытых данных для таких проектов, как Википедия и бесчисленное множество других." - invalidCommandTextWikidata2 = "Scribe использует языковые данные Wikidata для многих своих основных функций. Мы получаем информацию о родах существительных, спряжениях глаголов и многом другом!" - invalidCommandTextWikidata3 = "Вы можете создать учетную запись на wikidata.org, чтобы присоединиться к сообществу, поддерживающему Scribe и многие другие проекты. Помогите нам предоставлять бесплатную информацию миру!" - - invalidCommandMsgWiktionary = "Нет в Викисловаре" - invalidCommandTextWiktionary1 = "Wiktionary — это совместно редактируемый словарь, поддерживаемый Фондом Викимедиа. Он служит источником бесплатных лингвистических данных для таких проектов, как Википедия и бесчисленное множество других." - invalidCommandTextWiktionary2 = "Scribe использует данные Wiktionary для предоставления переводов для своей команды «Перевести». Наши данные получены из множества языковых пар, созданных сообществом Wiktionary!" - invalidCommandTextWiktionary3 = "Вы можете создать учетную запись на wiktionary.org, чтобы присоединиться к сообществу, поддерживающему Scribe и многие другие проекты. Помогите нам донести бесплатную информацию до всего мира!" - - baseAutosuggestions = ["я", "а", "в"] - numericAutosuggestions = ["в", "и", "я"] - - translateKeyLbl = "Перевести" - translatePlaceholder = "Введите слово" - translatePrompt = commandPromptSpacing + "ru -› \(getControllerLanguageAbbr()): " - translatePromptAndCursor = translatePrompt + commandCursor - translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder - translatePromptAndColorPlaceholder = NSMutableAttributedString(string: translatePromptAndPlaceholder) - translatePromptAndColorPlaceholder.setColorForText(textForAttribute: translatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - conjugateKeyLbl = "Спрягать" - conjugatePlaceholder = "Введите глагол" - conjugatePrompt = commandPromptSpacing + "Спрягать: " - conjugatePromptAndCursor = conjugatePrompt + commandCursor - conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder - conjugatePromptAndColorPlaceholder = NSMutableAttributedString(string: conjugatePromptAndPlaceholder) - conjugatePromptAndColorPlaceholder.setColorForText(textForAttribute: conjugatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - pluralKeyLbl = "Множ-ое" - pluralPlaceholder = "Введите существительное" - pluralPrompt = commandPromptSpacing + "Множ-ое: " - pluralPromptAndCursor = pluralPrompt + commandCursor - pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder - pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) - pluralPromptAndColorPlaceholder.setColorForText(textForAttribute: pluralPlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - alreadyPluralMsg = "Уже во множ-ом" + getRUKeys() + + currencySymbol = "₽" + currencySymbolAlternates = roubleAlternateKeys + spaceBar = "Пробел" + language = "Pусский" + + invalidCommandMsgWikidata = "Нет в Викиданных" + invalidCommandTextWikidata1 = + "Wikidata — это совместно редактируемый граф знаний, поддерживаемый Фондом Викимедиа. Он служит источником открытых данных для таких проектов, как Википедия и бесчисленное множество других." + invalidCommandTextWikidata2 = + "Scribe использует языковые данные Wikidata для многих своих основных функций. Мы получаем информацию о родах существительных, спряжениях глаголов и многом другом!" + invalidCommandTextWikidata3 = + "Вы можете создать учетную запись на wikidata.org, чтобы присоединиться к сообществу, поддерживающему Scribe и многие другие проекты. Помогите нам предоставлять бесплатную информацию миру!" + + invalidCommandMsgWiktionary = "Нет в Викисловаре" + invalidCommandTextWiktionary1 = + "Wiktionary — это совместно редактируемый словарь, поддерживаемый Фондом Викимедиа. Он служит источником бесплатных лингвистических данных для таких проектов, как Википедия и бесчисленное множество других." + invalidCommandTextWiktionary2 = + "Scribe использует данные Wiktionary для предоставления переводов для своей команды «Перевести». Наши данные получены из множества языковых пар, созданных сообществом Wiktionary!" + invalidCommandTextWiktionary3 = + "Вы можете создать учетную запись на wiktionary.org, чтобы присоединиться к сообществу, поддерживающему Scribe и многие другие проекты. Помогите нам донести бесплатную информацию до всего мира!" + + baseAutosuggestions = ["я", "а", "в"] + numericAutosuggestions = ["в", "и", "я"] + + translateKeyLbl = "Перевести" + translatePlaceholder = "Введите слово" + translatePrompt = commandPromptSpacing + "ru -› \(getControllerLanguageAbbr()): " + translatePromptAndCursor = translatePrompt + commandCursor + translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder + translatePromptAndColorPlaceholder = NSMutableAttributedString( + string: translatePromptAndPlaceholder + ) + translatePromptAndColorPlaceholder.setColorForText( + textForAttribute: translatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + conjugateKeyLbl = "Спрягать" + conjugatePlaceholder = "Введите глагол" + conjugatePrompt = commandPromptSpacing + "Спрягать: " + conjugatePromptAndCursor = conjugatePrompt + commandCursor + conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder + conjugatePromptAndColorPlaceholder = NSMutableAttributedString( + string: conjugatePromptAndPlaceholder + ) + conjugatePromptAndColorPlaceholder.setColorForText( + textForAttribute: conjugatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + pluralKeyLbl = "Множ-ое" + pluralPlaceholder = "Введите существительное" + pluralPrompt = commandPromptSpacing + "Множ-ое: " + pluralPromptAndCursor = pluralPrompt + commandCursor + pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder + pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) + pluralPromptAndColorPlaceholder.setColorForText( + textForAttribute: pluralPlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + alreadyPluralMsg = "Уже во множ-ом" } diff --git a/Keyboards/LanguageKeyboards/Russian/RUKeyboardViewController.swift b/Keyboards/LanguageKeyboards/Russian/RUKeyboardViewController.swift index 1f073ad2..c9fa5c20 100644 --- a/Keyboards/LanguageKeyboards/Russian/RUKeyboardViewController.swift +++ b/Keyboards/LanguageKeyboards/Russian/RUKeyboardViewController.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Class for the Russian Scribe keyboard. */ diff --git a/Keyboards/LanguageKeyboards/Spanish/ESCommandVariables.swift b/Keyboards/LanguageKeyboards/Spanish/ESCommandVariables.swift index d7044e2e..8201dfa4 100644 --- a/Keyboards/LanguageKeyboards/Spanish/ESCommandVariables.swift +++ b/Keyboards/LanguageKeyboards/Spanish/ESCommandVariables.swift @@ -2,17 +2,17 @@ /// Returns the reflexive pronoun for a given pronoun. func getESReflexivePronoun(pronoun: String) -> String { - if pronoun == "yo" { - return "me" - } else if pronoun == "tú" { - return "te" - } else if ["él", "ella", "usted", "ellos", "ellas", "ustedes"].contains(pronoun) { - return "se" - } else if ["nosotros", "nosotras"].contains(pronoun) { - return "nos" - } else if ["vosotros", "vosotras"].contains(pronoun) { - return "os" - } else { - return "" - } + if pronoun == "yo" { + return "me" + } else if pronoun == "tú" { + return "te" + } else if ["él", "ella", "usted", "ellos", "ellas", "ustedes"].contains(pronoun) { + return "se" + } else if ["nosotros", "nosotras"].contains(pronoun) { + return "nos" + } else if ["vosotros", "vosotras"].contains(pronoun) { + return "os" + } else { + return "" + } } diff --git a/Keyboards/LanguageKeyboards/Spanish/ESInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Spanish/ESInterfaceVariables.swift index 34e1e04b..200d9b7b 100644 --- a/Keyboards/LanguageKeyboards/Spanish/ESInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Spanish/ESInterfaceVariables.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Constants and functions to load the Spanish Scribe keyboard. */ @@ -9,304 +9,348 @@ import UIKit // MARK: Constants public enum ESKeyboardConstants { - static let defaultCurrencyKey = "$" - static let currencyKeys = ["$", "€", "£", "¥"] - - // Alternate key vars. - static let keysWithAlternates = ["a", "e", "i", "o", "u", "c", "d", "n", "s"] - static let keysWithAlternatesLeft = ["a", "e", "c", "d", "s"] - static let keysWithAlternatesRight = ["i", "o", "u", "n"] - - static let aAlternateKeys = ["á", "à", "ä", "â", "ã", "å", "ą", "æ", "ā", "ᵃ"] - static let eAlternateKeys = ["é", "è", "ë", "ê", "ę", "ė", "ē"] - static let iAlternateKeys = ["ī", "į", "î", "ì", "ï", "í"] - static let oAlternateKeys = ["ᵒ", "ō", "œ", "ø", "õ", "ô", "ö", "ó", "ò"] - static let uAlternateKeys = ["ū", "û", "ù", "ü", "ú"] - static let cAlternateKeys = ["ç", "ć", "č"] - static let dAlternateKeys = ["đ"] - static let nAlternateKeys = ["ń"] - static let nAlternateKeysDisableAccents = ["ń", "ñ"] - static let sAlternateKeys = ["š"] + static let defaultCurrencyKey = "$" + static let currencyKeys = ["$", "€", "£", "¥"] + + // Alternate key vars. + static let keysWithAlternates = ["a", "e", "i", "o", "u", "c", "d", "n", "s"] + static let keysWithAlternatesLeft = ["a", "e", "c", "d", "s"] + static let keysWithAlternatesRight = ["i", "o", "u", "n"] + + static let aAlternateKeys = ["á", "à", "ä", "â", "ã", "å", "ą", "æ", "ā", "ᵃ"] + static let eAlternateKeys = ["é", "è", "ë", "ê", "ę", "ė", "ē"] + static let iAlternateKeys = ["ī", "į", "î", "ì", "ï", "í"] + static let oAlternateKeys = ["ᵒ", "ō", "œ", "ø", "õ", "ô", "ö", "ó", "ò"] + static let uAlternateKeys = ["ū", "û", "ù", "ü", "ú"] + static let cAlternateKeys = ["ç", "ć", "č"] + static let dAlternateKeys = ["đ"] + static let nAlternateKeys = ["ń"] + static let nAlternateKeysDisableAccents = ["ń", "ñ"] + static let sAlternateKeys = ["š"] } struct ESKeyboardProvider: KeyboardProviderProtocol, KeyboardProviderDisableAccentsProtocol { - // MARK: iPhone Layouts - - static func genPhoneLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ñ"]) - .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "delete"]) - .addRow(["123", "selectKeyboard", "space", "return"]) - .build() - } - - static func genPhoneDisableAccentsLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l"]) - .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "delete"]) - .addRow(["123", "selectKeyboard", "space", "return"]) - .build() - } - - static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) - .addRow(["-", "/", ":", ";", "(", ")", "$", "&", "@", "\""]) - .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - .replaceKey(row: 1, column: 6, to: currencyKey) - .build() - } - - static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) - .addRow(["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "·"]) - .addRow(["123", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 6, to: currencyKeys[0]) - .replaceKey(row: 1, column: 7, to: currencyKeys[1]) - .replaceKey(row: 1, column: 8, to: currencyKeys[2]) - .build() + // MARK: iPhone Layouts + + static func genPhoneLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ñ"]) + .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "delete"]) + .addRow(["123", "selectKeyboard", "space", "return"]) + .build() } - } - - // MARK: iPad Layouts - - static func genPadLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) - .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "delete"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ñ", "return"]) - .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadDisableAccentsLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) - .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "delete"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "return"]) - .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) - .addRow(["@", "#", "$", "&", "*", "(", ")", "'", "\"", "+", "return"]) - .addRow(["#+=", "%", "_", "-", "=", "/", ";", ":", ",", ".", "#+="]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .replaceKey(row: 1, column: 2, to: currencyKey) - .build() - } - - static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) - .addRow(["€", "£", "¥", "^", "[", "]", "{", "}", "ᵒ", "ᵃ", "return"]) - .addRow(["123", "§", "|", "~", "¶", "\\", "<", ">", "¡", "¿", "123"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 0, to: currencyKeys[0]) - .replaceKey(row: 1, column: 1, to: currencyKeys[1]) - .replaceKey(row: 1, column: 2, to: currencyKeys[2]) - .build() + + static func genPhoneDisableAccentsLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l"]) + .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", "delete"]) + .addRow(["123", "selectKeyboard", "space", "return"]) + .build() } - } - - // MARK: Expanded iPad Layouts - - static func genPadExpandedLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["|", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "?", "¿", "delete"]) - .addRow([SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "´", "+", "*"]) - .addRow([SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "ñ", "{", "}", "return"]) - .addRow(["shift", "'", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadExpandedDisableAccentsLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["|", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "?", "¿", "delete"]) - .addRow([SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "´", "+", "*"]) - .addRow([SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "~", "{", "}", "return"]) - .addRow(["shift", "'", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() -} -static func genPadExpandedSymbolKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) - .addRow([SpecialKeys.indent, "(", ")", "{", "}", "#", "%", "^", "*", "+", "=", "\\", "|", "§"]) - .addRow([SpecialKeys.capsLock, "—", "/", ":", ";", "&", "@", "$", "£", "¥", "~", "[", "]", "return"]) - .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "_", "€", ",", ".", "-", "shift"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .build() -} + static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) + .addRow(["-", "/", ":", ";", "(", ")", "$", "&", "@", "\""]) + .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + .replaceKey(row: 1, column: 6, to: currencyKey) + .build() + } + + static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) + .addRow(["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "·"]) + .addRow(["123", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 6, to: currencyKeys[0]) + .replaceKey(row: 1, column: 7, to: currencyKeys[1]) + .replaceKey(row: 1, column: 8, to: currencyKeys[2]) + .build() + } + } + + // MARK: iPad Layouts + + static func genPadLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) + .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "delete"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ñ", "return"]) + .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadDisableAccentsLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) + .addRow(["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "delete"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "return"]) + .addRow(["shift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) + .addRow(["@", "#", "$", "&", "*", "(", ")", "'", "\"", "+", "return"]) + .addRow(["#+=", "%", "_", "-", "=", "/", ";", ":", ",", ".", "#+="]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .replaceKey(row: 1, column: 2, to: currencyKey) + .build() + } + + static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "delete"]) + .addRow(["€", "£", "¥", "^", "[", "]", "{", "}", "ᵒ", "ᵃ", "return"]) + .addRow(["123", "§", "|", "~", "¶", "\\", "<", ">", "¡", "¿", "123"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 0, to: currencyKeys[0]) + .replaceKey(row: 1, column: 1, to: currencyKeys[1]) + .replaceKey(row: 1, column: 2, to: currencyKeys[2]) + .build() + } + } + + // MARK: Expanded iPad Layouts + + static func genPadExpandedLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["|", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "?", "¿", "delete"]) + .addRow([ + SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "´", "+", "*" + ]) + .addRow([ + SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "ñ", "{", "}", + "return" + ]) + .addRow(["shift", "'", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadExpandedDisableAccentsLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["|", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "?", "¿", "delete"]) + .addRow([ + SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "´", "+", "*" + ]) + .addRow([ + SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "~", "{", "}", + "return" + ]) + .addRow(["shift", "'", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadExpandedSymbolKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) + .addRow([ + SpecialKeys.indent, "(", ")", "{", "}", "#", "%", "^", "*", "+", "=", "\\", "|", + "§" + ]) + .addRow([ + SpecialKeys.capsLock, "—", "/", ":", ";", "&", "@", "$", "£", "¥", "~", "[", "]", + "return" + ]) + .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "_", "€", ",", ".", "-", "shift"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .build() + } } // MARK: Get Keys func getESKeys() { - guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") else { - fatalError("Unable to access shared user defaults") - } - - var currencyKey = ESKeyboardConstants.defaultCurrencyKey - var currencyKeys = ESKeyboardConstants.currencyKeys - let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" - if let currencyValue = userDefaults.string(forKey: dictionaryKey) { - currencyKey = currencyValue - } else { - userDefaults.setValue(currencyKey, forKey: dictionaryKey) - } - if let index = currencyKeys.firstIndex(of: currencyKey) { - currencyKeys.remove(at: index) - } - - if DeviceType.isPhone { - if userDefaults.bool(forKey: "esAccentCharacters") { - letterKeys = ESKeyboardProvider.genPhoneDisableAccentsLetterKeys() - } else { - letterKeys = ESKeyboardProvider.genPhoneLetterKeys() + guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + else { + fatalError("Unable to access shared user defaults") } - numberKeys = ESKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) - symbolKeys = ESKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) - leftKeyChars = ["q", "a", "1", "-", "[", "_"] - if userDefaults.bool(forKey: "esAccentCharacters") { - rightKeyChars = ["p", "l", "0", "\"", "=", "·"] + var currencyKey = ESKeyboardConstants.defaultCurrencyKey + var currencyKeys = ESKeyboardConstants.currencyKeys + let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" + if let currencyValue = userDefaults.string(forKey: dictionaryKey) { + currencyKey = currencyValue } else { - rightKeyChars = ["p", "ñ", "0", "\"", "=", "·"] + userDefaults.setValue(currencyKey, forKey: dictionaryKey) } - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } else { - // Use the expanded keys layout if the iPad is wide enough and has no home button. - if usingExpandedKeyboard { - if userDefaults.bool(forKey: "esAccentCharacters") { - letterKeys = ESKeyboardProvider.genPadExpandedDisableAccentsLetterKeys() - } else { - letterKeys = ESKeyboardProvider.genPadExpandedLetterKeys() - } - symbolKeys = ESKeyboardProvider.genPadExpandedSymbolKeys() - - leftKeyChars = ["|", "`"] - rightKeyChars = ["*", "§"] - allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) + if let index = currencyKeys.firstIndex(of: currencyKey) { + currencyKeys.remove(at: index) + } + + if DeviceType.isPhone { + if userDefaults.bool(forKey: "esAccentCharacters") { + letterKeys = ESKeyboardProvider.genPhoneDisableAccentsLetterKeys() + } else { + letterKeys = ESKeyboardProvider.genPhoneLetterKeys() + } + numberKeys = ESKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) + symbolKeys = ESKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + + leftKeyChars = ["q", "a", "1", "-", "[", "_"] + if userDefaults.bool(forKey: "esAccentCharacters") { + rightKeyChars = ["p", "l", "0", "\"", "=", "·"] + } else { + rightKeyChars = ["p", "ñ", "0", "\"", "=", "·"] + } + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } } else { - if userDefaults.bool(forKey: "esAccentCharacters") { - letterKeys = ESKeyboardProvider.genPadDisableAccentsLetterKeys() - } else { - letterKeys = ESKeyboardProvider.genPadLetterKeys() - } - numberKeys = ESKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) - symbolKeys = ESKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) - - letterKeys.removeFirst(1) - - leftKeyChars = ["q", "a", "1", "@", "€"] - rightKeyChars = [] - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + // Use the expanded keys layout if the iPad is wide enough and has no home button. + if usingExpandedKeyboard { + if userDefaults.bool(forKey: "esAccentCharacters") { + letterKeys = ESKeyboardProvider.genPadExpandedDisableAccentsLetterKeys() + } else { + letterKeys = ESKeyboardProvider.genPadExpandedLetterKeys() + } + symbolKeys = ESKeyboardProvider.genPadExpandedSymbolKeys() + + leftKeyChars = ["|", "`"] + rightKeyChars = ["*", "§"] + allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) + } else { + if userDefaults.bool(forKey: "esAccentCharacters") { + letterKeys = ESKeyboardProvider.genPadDisableAccentsLetterKeys() + } else { + letterKeys = ESKeyboardProvider.genPadLetterKeys() + } + numberKeys = ESKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) + symbolKeys = ESKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) + + letterKeys.removeFirst(1) + + leftKeyChars = ["q", "a", "1", "@", "€"] + rightKeyChars = [] + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + } + + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } } - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } - - keysWithAlternates = ESKeyboardConstants.keysWithAlternates - keysWithAlternatesLeft = ESKeyboardConstants.keysWithAlternatesLeft - keysWithAlternatesRight = ESKeyboardConstants.keysWithAlternatesRight - aAlternateKeys = ESKeyboardConstants.aAlternateKeys - eAlternateKeys = ESKeyboardConstants.eAlternateKeys - iAlternateKeys = ESKeyboardConstants.iAlternateKeys - oAlternateKeys = ESKeyboardConstants.oAlternateKeys - uAlternateKeys = ESKeyboardConstants.uAlternateKeys - sAlternateKeys = ESKeyboardConstants.sAlternateKeys - dAlternateKeys = ESKeyboardConstants.dAlternateKeys - cAlternateKeys = ESKeyboardConstants.cAlternateKeys - - if userDefaults.bool(forKey: "esAccentCharacters") { - nAlternateKeys = ESKeyboardConstants.nAlternateKeysDisableAccents - } else { - nAlternateKeys = ESKeyboardConstants.nAlternateKeys - } + keysWithAlternates = ESKeyboardConstants.keysWithAlternates + keysWithAlternatesLeft = ESKeyboardConstants.keysWithAlternatesLeft + keysWithAlternatesRight = ESKeyboardConstants.keysWithAlternatesRight + aAlternateKeys = ESKeyboardConstants.aAlternateKeys + eAlternateKeys = ESKeyboardConstants.eAlternateKeys + iAlternateKeys = ESKeyboardConstants.iAlternateKeys + oAlternateKeys = ESKeyboardConstants.oAlternateKeys + uAlternateKeys = ESKeyboardConstants.uAlternateKeys + sAlternateKeys = ESKeyboardConstants.sAlternateKeys + dAlternateKeys = ESKeyboardConstants.dAlternateKeys + cAlternateKeys = ESKeyboardConstants.cAlternateKeys + + if userDefaults.bool(forKey: "esAccentCharacters") { + nAlternateKeys = ESKeyboardConstants.nAlternateKeysDisableAccents + } else { + nAlternateKeys = ESKeyboardConstants.nAlternateKeys + } } // MARK: Provide Layout func setESKeyboardLayout() { - getESKeys() - - currencySymbol = "$" - currencySymbolAlternates = dollarAlternateKeys - spaceBar = "espacio" - language = "Español" - - invalidCommandMsgWikidata = "No en Wikidata" - invalidCommandTextWikidata1 = "Wikidata es un gráfico de conocimiento editado de forma colaborativa y mantenido por la Fundación Wikimedia. Sirve como fuente de datos abiertos para proyectos como Wikipedia y muchos otros." - invalidCommandTextWikidata2 = "Scribe utiliza los datos lingüísticos de Wikidata para muchas de sus funciones principales. ¡Obtenemos información como géneros de sustantivos, conjugaciones de verbos y mucho más!" - invalidCommandTextWikidata3 = "Puedes crear una cuenta en wikidata.org para unirte a la comunidad que apoya a Scribe y a muchos otros proyectos. ¡Ayúdanos a llevar información gratuita al mundo!" - - invalidCommandMsgWiktionary = "No en Wiktionary" - invalidCommandTextWiktionary1 = "Wikcionario es un diccionario editado de forma colaborativa y mantenido por la Fundación Wikimedia. Sirve como fuente de datos lingüísticos gratuitos para proyectos como Wikipedia y muchos otros." - invalidCommandTextWiktionary2 = "Scribe utiliza los datos de Wikcionario para proporcionar traducciones para su comando Traducir. ¡Nuestros datos provienen de los numerosos pares de idiomas que la comunidad de Wikcionario ha creado!" - invalidCommandTextWiktionary3 = "Puedes crear una cuenta en wiktionary.org para unirte a la comunidad que apoya a Scribe y a muchos otros proyectos. ¡Ayúdanos a llevar información gratuita al mundo!" - - baseAutosuggestions = ["el", "la", "no"] - numericAutosuggestions = ["que", "de", "en"] - verbsAfterPronounsArray = ["ser", "REFLEXIVE_PRONOUN", "no"] - pronounAutosuggestionTenses = [ - "yo": "presFPS", - "tú": "presSPS", - "él": "presTPS", - "ella": "presTPS", - "nosotros": "presFPP", - "nosotras": "presFPP", - "vosotros": "presSPP", - "vosotras": "presSPP", - "ellos": "presTPP", - "ellas": "presTPP", - "ustedes": "presTPP" - ] - - translateKeyLbl = "Traducir" - translatePlaceholder = "Ingrese una palabra" - translatePrompt = commandPromptSpacing + "es -› \(getControllerLanguageAbbr()): " - translatePromptAndCursor = translatePrompt + commandCursor - translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder - translatePromptAndColorPlaceholder = NSMutableAttributedString(string: translatePromptAndPlaceholder) - translatePromptAndColorPlaceholder.setColorForText(textForAttribute: translatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - conjugateKeyLbl = "Conjugar" - conjugatePlaceholder = "Ingrese un verbo" - conjugatePrompt = commandPromptSpacing + "Conjugar: " - conjugatePromptAndCursor = conjugatePrompt + commandCursor - conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder - conjugatePromptAndColorPlaceholder = NSMutableAttributedString(string: conjugatePromptAndPlaceholder) - conjugatePromptAndColorPlaceholder.setColorForText(textForAttribute: conjugatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - pluralKeyLbl = "Plural" - pluralPlaceholder = "Ingrese un sustantivo" - pluralPrompt = commandPromptSpacing + "Plural: " - pluralPromptAndCursor = pluralPrompt + commandCursor - pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder - pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) - pluralPromptAndColorPlaceholder.setColorForText(textForAttribute: pluralPlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - alreadyPluralMsg = "Ya en plural" + getESKeys() + + currencySymbol = "$" + currencySymbolAlternates = dollarAlternateKeys + spaceBar = "espacio" + language = "Español" + + invalidCommandMsgWikidata = "No en Wikidata" + invalidCommandTextWikidata1 = + "Wikidata es un gráfico de conocimiento editado de forma colaborativa y mantenido por la Fundación Wikimedia. Sirve como fuente de datos abiertos para proyectos como Wikipedia y muchos otros." + invalidCommandTextWikidata2 = + "Scribe utiliza los datos lingüísticos de Wikidata para muchas de sus funciones principales. ¡Obtenemos información como géneros de sustantivos, conjugaciones de verbos y mucho más!" + invalidCommandTextWikidata3 = + "Puedes crear una cuenta en wikidata.org para unirte a la comunidad que apoya a Scribe y a muchos otros proyectos. ¡Ayúdanos a llevar información gratuita al mundo!" + + invalidCommandMsgWiktionary = "No en Wiktionary" + invalidCommandTextWiktionary1 = + "Wikcionario es un diccionario editado de forma colaborativa y mantenido por la Fundación Wikimedia. Sirve como fuente de datos lingüísticos gratuitos para proyectos como Wikipedia y muchos otros." + invalidCommandTextWiktionary2 = + "Scribe utiliza los datos de Wikcionario para proporcionar traducciones para su comando Traducir. ¡Nuestros datos provienen de los numerosos pares de idiomas que la comunidad de Wikcionario ha creado!" + invalidCommandTextWiktionary3 = + "Puedes crear una cuenta en wiktionary.org para unirte a la comunidad que apoya a Scribe y a muchos otros proyectos. ¡Ayúdanos a llevar información gratuita al mundo!" + + baseAutosuggestions = ["el", "la", "no"] + numericAutosuggestions = ["que", "de", "en"] + verbsAfterPronounsArray = ["ser", "REFLEXIVE_PRONOUN", "no"] + pronounAutosuggestionTenses = [ + "yo": "presFPS", + "tú": "presSPS", + "él": "presTPS", + "ella": "presTPS", + "nosotros": "presFPP", + "nosotras": "presFPP", + "vosotros": "presSPP", + "vosotras": "presSPP", + "ellos": "presTPP", + "ellas": "presTPP", + "ustedes": "presTPP" + ] + + translateKeyLbl = "Traducir" + translatePlaceholder = "Ingrese una palabra" + translatePrompt = commandPromptSpacing + "es -› \(getControllerLanguageAbbr()): " + translatePromptAndCursor = translatePrompt + commandCursor + translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder + translatePromptAndColorPlaceholder = NSMutableAttributedString( + string: translatePromptAndPlaceholder + ) + translatePromptAndColorPlaceholder.setColorForText( + textForAttribute: translatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + conjugateKeyLbl = "Conjugar" + conjugatePlaceholder = "Ingrese un verbo" + conjugatePrompt = commandPromptSpacing + "Conjugar: " + conjugatePromptAndCursor = conjugatePrompt + commandCursor + conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder + conjugatePromptAndColorPlaceholder = NSMutableAttributedString( + string: conjugatePromptAndPlaceholder + ) + conjugatePromptAndColorPlaceholder.setColorForText( + textForAttribute: conjugatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + pluralKeyLbl = "Plural" + pluralPlaceholder = "Ingrese un sustantivo" + pluralPrompt = commandPromptSpacing + "Plural: " + pluralPromptAndCursor = pluralPrompt + commandCursor + pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder + pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) + pluralPromptAndColorPlaceholder.setColorForText( + textForAttribute: pluralPlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + alreadyPluralMsg = "Ya en plural" } diff --git a/Keyboards/LanguageKeyboards/Spanish/ESKeyboardViewController.swift b/Keyboards/LanguageKeyboards/Spanish/ESKeyboardViewController.swift index 470946ca..5f0d5a71 100644 --- a/Keyboards/LanguageKeyboards/Spanish/ESKeyboardViewController.swift +++ b/Keyboards/LanguageKeyboards/Spanish/ESKeyboardViewController.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Class for the Spanish Scribe keyboard. */ diff --git a/Keyboards/LanguageKeyboards/Swedish/SVInterfaceVariables.swift b/Keyboards/LanguageKeyboards/Swedish/SVInterfaceVariables.swift index 5548e77d..3943138d 100644 --- a/Keyboards/LanguageKeyboards/Swedish/SVInterfaceVariables.swift +++ b/Keyboards/LanguageKeyboards/Swedish/SVInterfaceVariables.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Constants and functions to load the Swedish Scribe keyboard. */ @@ -9,161 +9,178 @@ import UIKit // MARK: Constants public enum SVKeyboardConstants { - static let defaultCurrencyKey = "kr" - static let currencyKeys = ["kr", "€", "$", "£"] - - // Alternate key vars. - static let keysWithAlternates = ["a", "e", "i", "o", "u", "ä", "ö", "c", "n", "s"] - static let keysWithAlernatesDisableAccents = ["a", "e", "i", "o", "u", "c", "n", "s"] - static let keysWithAlternatesLeft = ["a", "e", "c", "s"] - static let keysWithAlternatesRight = ["i", "o", "u", "ä", "ö", "n"] - static let keysWithAlternatesRightDisableAccents = ["i", "o", "u", "n"] - - static let aAlternateKeys = ["á", "à", "â", "ã", "ā"] - static let aAlternateKeysDisableAccents = ["á", "à", "â", "ã", "ā", "å"] - static let eAlternateKeys = ["é", "ë", "è", "ê", "ẽ", "ē", "ę"] - static let iAlternateKeys = ["ī", "î", "í", "ï", "ì", "ĩ"] - static let oAlternateKeys = ["ō", "õ", "ô", "ò", "ó", "œ"] - static let oAlternateKeysDisableAccents = ["ō", "õ", "ô", "ò", "ó", "œ", "ö", "ø"] - static let uAlternateKeys = ["û", "ú", "ü", "ù", "ũ", "ū"] - static let äAlternateKeys = ["æ"] - static let öAlternateKeys = ["ø"] - static let cAlternateKeys = ["ç"] - static let nAlternateKeys = ["ñ"] - static let sAlternateKeys = ["ß", "ś", "š"] + static let defaultCurrencyKey = "kr" + static let currencyKeys = ["kr", "€", "$", "£"] + + // Alternate key vars. + static let keysWithAlternates = ["a", "e", "i", "o", "u", "ä", "ö", "c", "n", "s"] + static let keysWithAlernatesDisableAccents = ["a", "e", "i", "o", "u", "c", "n", "s"] + static let keysWithAlternatesLeft = ["a", "e", "c", "s"] + static let keysWithAlternatesRight = ["i", "o", "u", "ä", "ö", "n"] + static let keysWithAlternatesRightDisableAccents = ["i", "o", "u", "n"] + + static let aAlternateKeys = ["á", "à", "â", "ã", "ā"] + static let aAlternateKeysDisableAccents = ["á", "à", "â", "ã", "ā", "å"] + static let eAlternateKeys = ["é", "ë", "è", "ê", "ẽ", "ē", "ę"] + static let iAlternateKeys = ["ī", "î", "í", "ï", "ì", "ĩ"] + static let oAlternateKeys = ["ō", "õ", "ô", "ò", "ó", "œ"] + static let oAlternateKeysDisableAccents = ["ō", "õ", "ô", "ò", "ó", "œ", "ö", "ø"] + static let uAlternateKeys = ["û", "ú", "ü", "ù", "ũ", "ū"] + static let äAlternateKeys = ["æ"] + static let öAlternateKeys = ["ø"] + static let cAlternateKeys = ["ç"] + static let nAlternateKeys = ["ñ"] + static let sAlternateKeys = ["ß", "ś", "š"] } struct SVKeyboardProvider: KeyboardProviderProtocol, KeyboardProviderDisableAccentsProtocol { - // MARK: iPhone Layouts - - static func genPhoneLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "å"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä"]) - .addRow(["shift", "y", "x", "c", "v", "b", "n", "m", "delete"]) - .addRow(["123", "selectKeyboard", "space", "return"]) - .build() - } - - static func genPhoneDisableAccentsLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["q", "w", "e", "r", "t", "z", "u", "i", "o", "p"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l"]) - .addRow(["shift", "y", "x", "c", "v", "b", "n", "m", "delete"]) - .addRow(["123", "selectKeyboard", "space", "return"]) - .build() - } - - static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) - .addRow(["-", "/", ":", ";", "(", ")", "kr", "&", "@", "\""]) - .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - .replaceKey(row: 1, column: 6, to: currencyKey) - .build() - } - - static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) - .addRow(["_", "\\", "|", "~", "<", ">", "€", "$", "£", "·"]) - .addRow(["123", ".", ",", "?", "!", "'", "delete"]) - .addRow(["ABC", "selectKeyboard", "space", "return"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 6, to: currencyKeys[0]) - .replaceKey(row: 1, column: 7, to: currencyKeys[1]) - .replaceKey(row: 1, column: 8, to: currencyKeys[2]) - .build() + // MARK: iPhone Layouts + + static func genPhoneLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "å"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä"]) + .addRow(["shift", "y", "x", "c", "v", "b", "n", "m", "delete"]) + .addRow(["123", "selectKeyboard", "space", "return"]) + .build() } - } - - // MARK: iPad Layouts - - static func genPadLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) - .addRow(["q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "å", "delete"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä", "return"]) - .addRow(["shift", "y", "x", "c", "v", "b", "n", "m", ",", ".", "?", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadDisableAccentsLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) - .addRow(["q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "delete"]) - .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "return"]) - .addRow(["shift", "y", "x", "c", "v", "b", "n", "m", ",", ".", "?", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadNumberKeys(currencyKey: String) -> [[String]] { - return KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "`", "delete"]) - .addRow(["@", "#", "kr", "&", "*", "(", ")", "'", "\"", "+", "·", "return"]) - .addRow(["#+=", "%", "≈", "±", "=", "/", ";", ":", ",", ".", "-", "#+="]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .replaceKey(row: 1, column: 2, to: currencyKey) - .build() - } - - static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { - let keyboardBuilder = KeyboardBuilder() - .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "*", "delete"]) - .addRow(["€", "$", "£", "^", "[", "]", "{", "}", "―", "ᵒ", "...", "return"]) - .addRow(["123", "§", "|", "~", "≠", "\\", "<", ">", "!", "?", "_", "123"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - - if currencyKeys.count < 3 { - return keyboardBuilder.build() - } else { - return keyboardBuilder - .replaceKey(row: 1, column: 0, to: currencyKeys[0]) - .replaceKey(row: 1, column: 1, to: currencyKeys[1]) - .replaceKey(row: 1, column: 2, to: currencyKeys[2]) - .build() + + static func genPhoneDisableAccentsLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["q", "w", "e", "r", "t", "z", "u", "i", "o", "p"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l"]) + .addRow(["shift", "y", "x", "c", "v", "b", "n", "m", "delete"]) + .addRow(["123", "selectKeyboard", "space", "return"]) + .build() + } + + static func genPhoneNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]) + .addRow(["-", "/", ":", ";", "(", ")", "kr", "&", "@", "\""]) + .addRow(["#+=", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + .replaceKey(row: 1, column: 6, to: currencyKey) + .build() + } + + static func genPhoneSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["[", "]", "{", "}", "#", "%", "^", "*", "+", "="]) + .addRow(["_", "\\", "|", "~", "<", ">", "€", "$", "£", "·"]) + .addRow(["123", ".", ",", "?", "!", "'", "delete"]) + .addRow(["ABC", "selectKeyboard", "space", "return"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 6, to: currencyKeys[0]) + .replaceKey(row: 1, column: 7, to: currencyKeys[1]) + .replaceKey(row: 1, column: 8, to: currencyKeys[2]) + .build() + } + } + + // MARK: iPad Layouts + + static func genPadLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) + .addRow(["q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "å", "delete"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä", "return"]) + .addRow(["shift", "y", "x", "c", "v", "b", "n", "m", ",", ".", "?", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadDisableAccentsLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "+"]) + .addRow(["q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "delete"]) + .addRow(["a", "s", "d", "f", "g", "h", "j", "k", "l", "return"]) + .addRow(["shift", "y", "x", "c", "v", "b", "n", "m", ",", ".", "?", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadNumberKeys(currencyKey: String) -> [[String]] { + return KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "`", "delete"]) + .addRow(["@", "#", "kr", "&", "*", "(", ")", "'", "\"", "+", "·", "return"]) + .addRow(["#+=", "%", "≈", "±", "=", "/", ";", ":", ",", ".", "-", "#+="]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .replaceKey(row: 1, column: 2, to: currencyKey) + .build() + } + + static func genPadSymbolKeys(currencyKeys: [String]) -> [[String]] { + let keyboardBuilder = KeyboardBuilder() + .addRow(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "*", "delete"]) + .addRow(["€", "$", "£", "^", "[", "]", "{", "}", "―", "ᵒ", "...", "return"]) + .addRow(["123", "§", "|", "~", "≠", "\\", "<", ">", "!", "?", "_", "123"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + + if currencyKeys.count < 3 { + return keyboardBuilder.build() + } else { + return + keyboardBuilder + .replaceKey(row: 1, column: 0, to: currencyKeys[0]) + .replaceKey(row: 1, column: 1, to: currencyKeys[1]) + .replaceKey(row: 1, column: 2, to: currencyKeys[2]) + .build() + } + } + + // MARK: Expanded iPad Layouts + + static func genPadExpandedLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["§", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "+", "´", "delete"]) + .addRow([ + SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "å", "^", "*" + ]) + .addRow([ + SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä", "'", + "return" + ]) + .addRow(["shift", "'", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() + } + + static func genPadExpandedDisableAccentsLetterKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["§", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "+", "´", "delete"]) + .addRow([ + SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "\"", "^", + "*" + ]) + .addRow([ + SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "(", ")", "'", + "return" + ]) + .addRow(["shift", "'", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "shift"]) + .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) + .build() } - } - - // MARK: Expanded iPad Layouts - - static func genPadExpandedLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["§", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "+", "´", "delete"]) - .addRow([SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "å", "^", "*"]) - .addRow([SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä", "'", "return"]) - .addRow(["shift", "'", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadExpandedDisableAccentsLetterKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["§", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "+", "´", "delete"]) - .addRow([SpecialKeys.indent, "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "\"", "^", "*"]) - .addRow([SpecialKeys.capsLock, "a", "s", "d", "f", "g", "h", "j", "k", "l", "(", ")", "'", "return"]) - .addRow(["shift", "'", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "shift"]) - .addRow(["selectKeyboard", ".?123", "space", ".?123", "hideKeyboard"]) - .build() - } - - static func genPadExpandedSymbolKeys() -> [[String]] { - return KeyboardBuilder() - .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) - .addRow([SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "°", "|", "§"]) - .addRow([SpecialKeys.capsLock, "—", "/", ":", ";", "(", ")", "&", "@", "$", "£", "¥", "~", "return"]) - .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "_", "€", ",", ".", "-", "shift"]) - .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) - .build() - } + static func genPadExpandedSymbolKeys() -> [[String]] { + return KeyboardBuilder() + .addRow(["`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "<", ">", "delete"]) + .addRow([ + SpecialKeys.indent, "[", "]", "{", "}", "#", "%", "^", "*", "+", "=", "°", "|", "§" + ]) + .addRow([ + SpecialKeys.capsLock, "—", "/", ":", ";", "(", ")", "&", "@", "$", "£", "¥", "~", + "return" + ]) + .addRow(["shift", "…", "?", "!", "≠", "'", "\"", "_", "€", ",", ".", "-", "shift"]) + .addRow(["selectKeyboard", "ABC", "space", "ABC", "hideKeyboard"]) + .build() + } } // MARK: Generate and set keyboard @@ -171,139 +188,165 @@ struct SVKeyboardProvider: KeyboardProviderProtocol, KeyboardProviderDisableAcce // MARK: Get Keys func getSVKeys() { - guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") else { - fatalError("Unable to access shared user defaults") - } - - var currencyKey = SVKeyboardConstants.defaultCurrencyKey - var currencyKeys = SVKeyboardConstants.currencyKeys - let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" - if let currencyValue = userDefaults.string(forKey: dictionaryKey) { - currencyKey = currencyValue - } else { - userDefaults.setValue(currencyKey, forKey: dictionaryKey) - } - if let index = currencyKeys.firstIndex(of: currencyKey) { - currencyKeys.remove(at: index) - } - - if DeviceType.isPhone { - if userDefaults.bool(forKey: "svAccentCharacters") { - letterKeys = SVKeyboardProvider.genPhoneDisableAccentsLetterKeys() - } else { - letterKeys = SVKeyboardProvider.genPhoneLetterKeys() + guard let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + else { + fatalError("Unable to access shared user defaults") } - numberKeys = SVKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) - symbolKeys = SVKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) - leftKeyChars = ["q", "a", "1", "-", "[", "_"] - if userDefaults.bool(forKey: "svAccentCharacters") { - rightKeyChars = ["p", "l", "0", "\"", "=", "·"] + var currencyKey = SVKeyboardConstants.defaultCurrencyKey + var currencyKeys = SVKeyboardConstants.currencyKeys + let dictionaryKey = controllerLanguage + "defaultCurrencySymbol" + if let currencyValue = userDefaults.string(forKey: dictionaryKey) { + currencyKey = currencyValue } else { - rightKeyChars = ["å", "ä", "0", "\"", "=", "·"] + userDefaults.setValue(currencyKey, forKey: dictionaryKey) + } + if let index = currencyKeys.firstIndex(of: currencyKey) { + currencyKeys.remove(at: index) } - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } else { - // Use the expanded keys layout if the iPad is wide enough and has no home button. - if usingExpandedKeyboard { - if userDefaults.bool(forKey: "svAccentCharacters") { - letterKeys = SVKeyboardProvider.genPadExpandedDisableAccentsLetterKeys() - } else { - letterKeys = SVKeyboardProvider.genPadExpandedLetterKeys() - } - symbolKeys = SVKeyboardProvider.genPadExpandedSymbolKeys() - - leftKeyChars = ["§", "`"] - rightKeyChars = ["§", "*"] - allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) + + if DeviceType.isPhone { + if userDefaults.bool(forKey: "svAccentCharacters") { + letterKeys = SVKeyboardProvider.genPhoneDisableAccentsLetterKeys() + } else { + letterKeys = SVKeyboardProvider.genPhoneLetterKeys() + } + numberKeys = SVKeyboardProvider.genPhoneNumberKeys(currencyKey: currencyKey) + symbolKeys = SVKeyboardProvider.genPhoneSymbolKeys(currencyKeys: currencyKeys) + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + + leftKeyChars = ["q", "a", "1", "-", "[", "_"] + if userDefaults.bool(forKey: "svAccentCharacters") { + rightKeyChars = ["p", "l", "0", "\"", "=", "·"] + } else { + rightKeyChars = ["å", "ä", "0", "\"", "=", "·"] + } + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } } else { - if userDefaults.bool(forKey: "svAccentCharacters") { - letterKeys = SVKeyboardProvider.genPadDisableAccentsLetterKeys() - } else { - letterKeys = SVKeyboardProvider.genPadLetterKeys() - } - numberKeys = SVKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) - symbolKeys = SVKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) - - letterKeys.removeFirst(1) - - leftKeyChars = ["q", "a", "1", "@", "€"] - rightKeyChars = [] - allKeys = Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + // Use the expanded keys layout if the iPad is wide enough and has no home button. + if usingExpandedKeyboard { + if userDefaults.bool(forKey: "svAccentCharacters") { + letterKeys = SVKeyboardProvider.genPadExpandedDisableAccentsLetterKeys() + } else { + letterKeys = SVKeyboardProvider.genPadExpandedLetterKeys() + } + symbolKeys = SVKeyboardProvider.genPadExpandedSymbolKeys() + + leftKeyChars = ["§", "`"] + rightKeyChars = ["§", "*"] + allKeys = Array(letterKeys.joined()) + Array(symbolKeys.joined()) + } else { + if userDefaults.bool(forKey: "svAccentCharacters") { + letterKeys = SVKeyboardProvider.genPadDisableAccentsLetterKeys() + } else { + letterKeys = SVKeyboardProvider.genPadLetterKeys() + } + numberKeys = SVKeyboardProvider.genPadNumberKeys(currencyKey: currencyKey) + symbolKeys = SVKeyboardProvider.genPadSymbolKeys(currencyKeys: currencyKeys) + + letterKeys.removeFirst(1) + + leftKeyChars = ["q", "a", "1", "@", "€"] + rightKeyChars = [] + allKeys = + Array(letterKeys.joined()) + Array(numberKeys.joined()) + Array(symbolKeys.joined()) + } + + centralKeyChars = allKeys.filter { + !leftKeyChars.contains($0) && !rightKeyChars.contains($0) + } } - centralKeyChars = allKeys.filter { !leftKeyChars.contains($0) && !rightKeyChars.contains($0) } - } - - keysWithAlternatesLeft = SVKeyboardConstants.keysWithAlternatesLeft - eAlternateKeys = SVKeyboardConstants.eAlternateKeys - iAlternateKeys = SVKeyboardConstants.iAlternateKeys - uAlternateKeys = SVKeyboardConstants.uAlternateKeys - sAlternateKeys = SVKeyboardConstants.sAlternateKeys - cAlternateKeys = SVKeyboardConstants.cAlternateKeys - nAlternateKeys = SVKeyboardConstants.nAlternateKeys - - if userDefaults.bool(forKey: "svAccentCharacters") { - keysWithAlternates = SVKeyboardConstants.keysWithAlernatesDisableAccents - keysWithAlternatesRight = SVKeyboardConstants.keysWithAlternatesRightDisableAccents - aAlternateKeys = SVKeyboardConstants.aAlternateKeysDisableAccents - oAlternateKeys = SVKeyboardConstants.oAlternateKeysDisableAccents - } else { - keysWithAlternates = SVKeyboardConstants.keysWithAlternates - keysWithAlternatesRight = SVKeyboardConstants.keysWithAlternatesRight - aAlternateKeys = SVKeyboardConstants.aAlternateKeys - oAlternateKeys = SVKeyboardConstants.oAlternateKeys - äAlternateKeys = SVKeyboardConstants.äAlternateKeys - öAlternateKeys = SVKeyboardConstants.öAlternateKeys - } + keysWithAlternatesLeft = SVKeyboardConstants.keysWithAlternatesLeft + eAlternateKeys = SVKeyboardConstants.eAlternateKeys + iAlternateKeys = SVKeyboardConstants.iAlternateKeys + uAlternateKeys = SVKeyboardConstants.uAlternateKeys + sAlternateKeys = SVKeyboardConstants.sAlternateKeys + cAlternateKeys = SVKeyboardConstants.cAlternateKeys + nAlternateKeys = SVKeyboardConstants.nAlternateKeys + + if userDefaults.bool(forKey: "svAccentCharacters") { + keysWithAlternates = SVKeyboardConstants.keysWithAlernatesDisableAccents + keysWithAlternatesRight = SVKeyboardConstants.keysWithAlternatesRightDisableAccents + aAlternateKeys = SVKeyboardConstants.aAlternateKeysDisableAccents + oAlternateKeys = SVKeyboardConstants.oAlternateKeysDisableAccents + } else { + keysWithAlternates = SVKeyboardConstants.keysWithAlternates + keysWithAlternatesRight = SVKeyboardConstants.keysWithAlternatesRight + aAlternateKeys = SVKeyboardConstants.aAlternateKeys + oAlternateKeys = SVKeyboardConstants.oAlternateKeys + äAlternateKeys = SVKeyboardConstants.äAlternateKeys + öAlternateKeys = SVKeyboardConstants.öAlternateKeys + } } // MARK: Provide Layout func setSVKeyboardLayout() { - getSVKeys() - - currencySymbol = "kr" - currencySymbolAlternates = kronaAlternateKeys - spaceBar = "mellanslag" - language = "Svenska" - - invalidCommandMsgWikidata = "Inte i Wikidata" - invalidCommandTextWikidata1 = "Wikidata är en gemensamt redigerad kunskapsgraf som underhålls av Wikimedia Foundation. Det fungerar som en källa till öppen data för projekt som Wikipedia och flera andra." - invalidCommandTextWikidata2 = "Scribe använder Wikidatas språkdata för många av sina kärnfunktioner. Vi får information som substantiv, genus, verbböjningar och mycket mer!" - invalidCommandTextWikidata3 = "Du kan skapa ett konto på wikidata.org för att gå med i communityn som stöder Scribe och så många andra projekt. Hjälp oss att ge gratis information till världen!" - - invalidCommandMsgWiktionary = "Inte i Wiktionary" - invalidCommandTextWiktionary1 = "Wiktionary är en gemensamt redigerad ordbok som underhålls av Wikimedia Foundation. Den fungerar som en källa till fri språkdata för projekt som Wikipedia och otaliga andra." - invalidCommandTextWiktionary2 = "Scribe använder Wiktionarys data för att tillhandahålla översättningar för sitt översättningskommando. Våra data härleds från de många språkpar som Wiktionarys gemenskap har skapat!" - invalidCommandTextWiktionary3 = "Du kan skapa ett konto på wiktionary.org för att gå med i gemenskapen som stöder Scribe och många andra projekt. Hjälp oss att ge världen fri information!" - - baseAutosuggestions = ["jag", "det", "men"] - numericAutosuggestions = ["jag", "det", "och"] - - translateKeyLbl = "Översätt" - translatePlaceholder = "Ange ett ord" - translatePrompt = commandPromptSpacing + "sv -› \(getControllerLanguageAbbr()): " - translatePromptAndCursor = translatePrompt + commandCursor - translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder - translatePromptAndColorPlaceholder = NSMutableAttributedString(string: translatePromptAndPlaceholder) - translatePromptAndColorPlaceholder.setColorForText(textForAttribute: translatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - conjugateKeyLbl = "Konjugera" - conjugatePlaceholder = "Ange ett verb" - conjugatePrompt = commandPromptSpacing + "Konjugera: " - conjugatePromptAndCursor = conjugatePrompt + commandCursor - conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder - conjugatePromptAndColorPlaceholder = NSMutableAttributedString(string: conjugatePromptAndPlaceholder) - conjugatePromptAndColorPlaceholder.setColorForText(textForAttribute: conjugatePlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - - pluralKeyLbl = "Plural" - pluralPlaceholder = "Ange ett substantiv" - pluralPrompt = commandPromptSpacing + "Plural: " - pluralPromptAndCursor = pluralPrompt + commandCursor - pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder - pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) - pluralPromptAndColorPlaceholder.setColorForText(textForAttribute: pluralPlaceholder, withColor: UIColor(cgColor: commandBarPlaceholderColorCG)) - alreadyPluralMsg = "Redan plural" + getSVKeys() + + currencySymbol = "kr" + currencySymbolAlternates = kronaAlternateKeys + spaceBar = "mellanslag" + language = "Svenska" + + invalidCommandMsgWikidata = "Inte i Wikidata" + invalidCommandTextWikidata1 = + "Wikidata är en gemensamt redigerad kunskapsgraf som underhålls av Wikimedia Foundation. Det fungerar som en källa till öppen data för projekt som Wikipedia och flera andra." + invalidCommandTextWikidata2 = + "Scribe använder Wikidatas språkdata för många av sina kärnfunktioner. Vi får information som substantiv, genus, verbböjningar och mycket mer!" + invalidCommandTextWikidata3 = + "Du kan skapa ett konto på wikidata.org för att gå med i communityn som stöder Scribe och så många andra projekt. Hjälp oss att ge gratis information till världen!" + + invalidCommandMsgWiktionary = "Inte i Wiktionary" + invalidCommandTextWiktionary1 = + "Wiktionary är en gemensamt redigerad ordbok som underhålls av Wikimedia Foundation. Den fungerar som en källa till fri språkdata för projekt som Wikipedia och otaliga andra." + invalidCommandTextWiktionary2 = + "Scribe använder Wiktionarys data för att tillhandahålla översättningar för sitt översättningskommando. Våra data härleds från de många språkpar som Wiktionarys gemenskap har skapat!" + invalidCommandTextWiktionary3 = + "Du kan skapa ett konto på wiktionary.org för att gå med i gemenskapen som stöder Scribe och många andra projekt. Hjälp oss att ge världen fri information!" + + baseAutosuggestions = ["jag", "det", "men"] + numericAutosuggestions = ["jag", "det", "och"] + + translateKeyLbl = "Översätt" + translatePlaceholder = "Ange ett ord" + translatePrompt = commandPromptSpacing + "sv -› \(getControllerLanguageAbbr()): " + translatePromptAndCursor = translatePrompt + commandCursor + translatePromptAndPlaceholder = translatePromptAndCursor + " " + translatePlaceholder + translatePromptAndColorPlaceholder = NSMutableAttributedString( + string: translatePromptAndPlaceholder + ) + translatePromptAndColorPlaceholder.setColorForText( + textForAttribute: translatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + conjugateKeyLbl = "Konjugera" + conjugatePlaceholder = "Ange ett verb" + conjugatePrompt = commandPromptSpacing + "Konjugera: " + conjugatePromptAndCursor = conjugatePrompt + commandCursor + conjugatePromptAndPlaceholder = conjugatePromptAndCursor + " " + conjugatePlaceholder + conjugatePromptAndColorPlaceholder = NSMutableAttributedString( + string: conjugatePromptAndPlaceholder + ) + conjugatePromptAndColorPlaceholder.setColorForText( + textForAttribute: conjugatePlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + + pluralKeyLbl = "Plural" + pluralPlaceholder = "Ange ett substantiv" + pluralPrompt = commandPromptSpacing + "Plural: " + pluralPromptAndCursor = pluralPrompt + commandCursor + pluralPromptAndPlaceholder = pluralPromptAndCursor + " " + pluralPlaceholder + pluralPromptAndColorPlaceholder = NSMutableAttributedString(string: pluralPromptAndPlaceholder) + pluralPromptAndColorPlaceholder.setColorForText( + textForAttribute: pluralPlaceholder, + withColor: UIColor(cgColor: commandBarPlaceholderColorCG) + ) + alreadyPluralMsg = "Redan plural" } diff --git a/Keyboards/LanguageKeyboards/Swedish/SVKeyboardViewController.swift b/Keyboards/LanguageKeyboards/Swedish/SVKeyboardViewController.swift index 9d2cad63..22ffed54 100644 --- a/Keyboards/LanguageKeyboards/Swedish/SVKeyboardViewController.swift +++ b/Keyboards/LanguageKeyboards/Swedish/SVKeyboardViewController.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Class for the Swedish Scribe keyboard. */ diff --git a/Network/APIClient.swift b/Network/APIClient.swift index bbfe7075..6128256c 100644 --- a/Network/APIClient.swift +++ b/Network/APIClient.swift @@ -3,6 +3,7 @@ import Foundation // MARK: Network Error + enum NetworkError: Error, LocalizedError { case invalidURL case invalidResponse @@ -15,18 +16,19 @@ enum NetworkError: Error, LocalizedError { return "Invalid URL" case .invalidResponse: return "Invalid server response" - case .httpError(let code, let message): + case let .httpError(code, message): if let message = message { return "HTTP error \(code): \(message)" } return "HTTP error: \(code)" - case .decodingError(let error): + case let .decodingError(error): return "Decoding failed: \(error.localizedDescription)" } } } // MARK: API Client + /// APIClient handles all network interactions with the Scribe server, including fetching language data and version information. final class LanguageDataAPIClient { static let shared = LanguageDataAPIClient() @@ -49,11 +51,12 @@ final class LanguageDataAPIClient { self.session = session ?? URLSession(configuration: config) // Configure decoder - self.decoder = JSONDecoder() - self.decoder.dateDecodingStrategy = .iso8601 + decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 } // MARK: Fetch Language Data + /// Fetches the complete language data including contract and data tables /// - Parameter language: The language code (e.g., "en", "es", "fr") /// - Returns: DataResponse containing language data @@ -63,6 +66,7 @@ final class LanguageDataAPIClient { } // MARK: Fetch Data Version + /// Fetches the version information for a specific language /// - Parameter language: The language code (e.g., "en", "es", "fr") /// - Returns: DataVersionResponse containing version information @@ -72,11 +76,13 @@ final class LanguageDataAPIClient { } // MARK: Generic Fetch + private func fetch( - _ type: T.Type, + _: T.Type, from endpoint: String ) async throws -> T { - guard let url = URL(string: endpoint, relativeTo: baseURL) else { + guard let url = URL(string: endpoint, relativeTo: baseURL) + else { throw NetworkError.invalidURL } @@ -84,12 +90,14 @@ final class LanguageDataAPIClient { let (data, response) = try await session.data(for: request) // Validate response. - guard let httpResponse = response as? HTTPURLResponse else { + guard let httpResponse = response as? HTTPURLResponse + else { throw NetworkError.invalidResponse } // Handle non-success status codes. - guard (200...299).contains(httpResponse.statusCode) else { + guard (200 ... 299).contains(httpResponse.statusCode) + else { let errorMessage = try? JSONDecoder().decode( ErrorResponse.self, from: data @@ -110,6 +118,7 @@ final class LanguageDataAPIClient { } // MARK: Error Response + private struct ErrorResponse: Decodable { let message: String } diff --git a/Scribe/AboutTab/AboutTableData.swift b/Scribe/AboutTab/AboutTableData.swift index a5333894..b99d16cd 100644 --- a/Scribe/AboutTab/AboutTableData.swift +++ b/Scribe/AboutTab/AboutTableData.swift @@ -1,122 +1,157 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Controls data displayed in the About tab. */ import Foundation -struct AboutTableData { - static var aboutTableData = [ - ParentTableCellModel( - headingTitle: NSLocalizedString("i18n.app.about.community.title", value: "Community", comment: ""), - section: [ - Section( - sectionTitle: NSLocalizedString("i18n.app.about.community.github", value: "See the code on GitHub", comment: ""), - imageString: "github", - sectionState: .github, - externalLink: true +enum AboutTableData { + static var aboutTableData = [ + ParentTableCellModel( + headingTitle: NSLocalizedString( + "i18n.app.about.community.title", value: "Community", comment: "" + ), + section: [ + Section( + sectionTitle: NSLocalizedString( + "i18n.app.about.community.github", value: "See the code on GitHub", + comment: "" + ), + imageString: "github", + sectionState: .github, + externalLink: true + ), + Section( + sectionTitle: NSLocalizedString( + "i18n.app.about.community.matrix", value: "Chat with the team on Matrix", + comment: "" + ), + imageString: "matrix", + sectionState: .matrix, + externalLink: true + ), + Section( + sectionTitle: NSLocalizedString( + "i18n.app.about.community.mastodon", value: "Follow us on Mastodon", + comment: "" + ), + imageString: "mastodon", + sectionState: .mastodon, + externalLink: true + ), + Section( + sectionTitle: NSLocalizedString( + "i18n.app.about.community.visit_website", value: "Visit the Scribe website", + comment: "" + ), + imageString: "globe", + sectionState: .scribeApps, + externalLink: true + ), + Section( + sectionTitle: NSLocalizedString( + "i18n.app.about.community.share_scribe", value: "Share Scribe", comment: "" + ), + imageString: "square.and.arrow.up", + sectionState: .shareScribe, + externalLink: true + ), + Section( + sectionTitle: NSLocalizedString( + "i18n.app.about.community.wikimedia", + value: "Wikimedia and Scribe", + comment: "" + ), + imageString: "wikimedia", + hasNestedNavigation: true, + sectionState: .wikimedia + ) + ], + hasDynamicData: nil ), - Section( - sectionTitle: NSLocalizedString("i18n.app.about.community.matrix", value: "Chat with the team on Matrix", comment: ""), - imageString: "matrix", - sectionState: .matrix, - externalLink: true + ParentTableCellModel( + headingTitle: NSLocalizedString( + "i18n.app.about.feedback.title", + value: "Feedback and support", + comment: "" + ), + section: [ + Section( + sectionTitle: NSLocalizedString( + "i18n.app.about.feedback.rate_scribe", + value: "Rate Scribe", + comment: "" + ), + imageString: "star.fill", + sectionState: .rateScribe, + externalLink: true + ), + Section( + sectionTitle: NSLocalizedString( + "i18n.app.about.feedback.bug_report", + value: "Report a bug", + comment: "" + ), + imageString: "ladybug", + sectionState: .bugReport, + externalLink: true + ), + Section( + sectionTitle: NSLocalizedString( + "i18n.app.about.feedback.send_email", + value: "Send us an email", + comment: "" + ), + imageString: "envelope", + sectionState: .email, + externalLink: true + ), + Section( + sectionTitle: NSLocalizedString( + "i18n.app.about.feedback.version", + value: "Check version", + comment: "" + ), + imageString: "bookmark", + sectionState: .version, + externalLink: true + ), + Section( + sectionTitle: NSLocalizedString( + "i18n.app.about.feedback.reset_app_hints", + value: "Reset app hints", + comment: "" + ), + imageString: "lightbulb.max", + hasNestedNavigation: true, + sectionState: .appHints + ) + ], + hasDynamicData: nil ), - Section( - sectionTitle: NSLocalizedString("i18n.app.about.community.mastodon", value: "Follow us on Mastodon", comment: ""), - imageString: "mastodon", - sectionState: .mastodon, - externalLink: true - ), - Section( - sectionTitle: NSLocalizedString("i18n.app.about.community.visit_website", value: "Visit the Scribe website", comment: ""), - imageString: "globe", - sectionState: .scribeApps, - externalLink: true - ), - Section( - sectionTitle: NSLocalizedString("i18n.app.about.community.share_scribe", value: "Share Scribe", comment: ""), - imageString: "square.and.arrow.up", - sectionState: .shareScribe, - externalLink: true - ), - Section( - sectionTitle: NSLocalizedString("i18n.app.about.community.wikimedia", - value: "Wikimedia and Scribe", - comment: ""), - imageString: "wikimedia", - hasNestedNavigation: true, - sectionState: .wikimedia - ) - ], - hasDynamicData: nil - ), - ParentTableCellModel( - headingTitle: NSLocalizedString("i18n.app.about.feedback.title", - value: "Feedback and support", - comment: ""), - section: [ - Section( - sectionTitle: NSLocalizedString("i18n.app.about.feedback.rate_scribe", - value: "Rate Scribe", - comment: ""), - imageString: "star.fill", - sectionState: .rateScribe, - externalLink: true - ), - Section( - sectionTitle: NSLocalizedString("i18n.app.about.feedback.bug_report", - value: "Report a bug", - comment: ""), - imageString: "ladybug", - sectionState: .bugReport, - externalLink: true - ), - Section( - sectionTitle: NSLocalizedString("i18n.app.about.feedback.send_email", - value: "Send us an email", - comment: ""), - imageString: "envelope", - sectionState: .email, - externalLink: true - ), - Section( - sectionTitle: NSLocalizedString("i18n.app.about.feedback.version", - value: "Check version", - comment: ""), - imageString: "bookmark", - sectionState: .version, - externalLink: true - ), - Section( - sectionTitle: NSLocalizedString("i18n.app.about.feedback.reset_app_hints", - value: "Reset app hints", - comment: ""), - imageString: "lightbulb.max", - hasNestedNavigation: true, - sectionState: .appHints - ) - ], - hasDynamicData: nil - ), - ParentTableCellModel( - headingTitle: NSLocalizedString("i18n._global.legal", value: "Legal", comment: ""), - section: [ - Section( - sectionTitle: NSLocalizedString("i18n._global.privacy_policy", value: "Privacy policy", comment: ""), - imageString: "lock.shield", - hasNestedNavigation: true, - sectionState: .privacyPolicy - ), - Section( - sectionTitle: NSLocalizedString("i18n.app.about.legal.third_party", value: "Third-party licenses", comment: ""), - imageString: "doc.text", - hasNestedNavigation: true, - sectionState: .licenses + ParentTableCellModel( + headingTitle: NSLocalizedString("i18n._global.legal", value: "Legal", comment: ""), + section: [ + Section( + sectionTitle: NSLocalizedString( + "i18n._global.privacy_policy", value: "Privacy policy", comment: "" + ), + imageString: "lock.shield", + hasNestedNavigation: true, + sectionState: .privacyPolicy + ), + Section( + sectionTitle: NSLocalizedString( + "i18n.app.about.legal.third_party", value: "Third-party licenses", + comment: "" + ), + imageString: "doc.text", + hasNestedNavigation: true, + sectionState: .licenses + ) + ], + hasDynamicData: nil ) - ], - hasDynamicData: nil - ) - ] + ] } diff --git a/Scribe/AboutTab/AboutViewController.swift b/Scribe/AboutTab/AboutViewController.swift index 0cbe32de..f9067a85 100644 --- a/Scribe/AboutTab/AboutViewController.swift +++ b/Scribe/AboutTab/AboutViewController.swift @@ -1,284 +1,321 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Functions for the About tab. */ import MessageUI import StoreKit -import UIKit import SwiftUI +import UIKit final class AboutViewController: BaseTableViewController { - override var dataSet: [ParentTableCellModel] { - AboutTableData.aboutTableData - } - - private let aboutTipCardState: Bool = { - let userDefault = UserDefaults.standard - let state = userDefault.object(forKey: "aboutTipCardState") as? Bool ?? true - return state - }() - - private var tipHostingController: UIHostingController! - private var tableViewOffset: CGFloat? - private let cornerRadius: CGFloat = 12 - - override func viewDidLoad() { - super.viewDidLoad() - showTipCardView() - title = NSLocalizedString("i18n.app.about.title", - value: "About", - comment: "") - - tableView.register( - WrapperCell.self, - forCellReuseIdentifier: WrapperCell.reuseIdentifier - ) - - tableView.register( - UINib(nibName: "AboutTableViewCell", bundle: nil), - forCellReuseIdentifier: AboutTableViewCell.reuseIdentifier - ) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - if tableViewOffset == nil && UIDevice.current.userInterfaceIdiom != .pad { - tableViewOffset = tableView.contentOffset.y + override var dataSet: [ParentTableCellModel] { + AboutTableData.aboutTableData } - } -} -// MARK: UITableViewDataSource - -extension AboutViewController { - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell( - withIdentifier: WrapperCell.reuseIdentifier, - for: indexPath - ) as? WrapperCell else { - fatalError("Failed to dequeue WrapperCell") + private let aboutTipCardState: Bool = { + let userDefault = UserDefaults.standard + return userDefault.object(forKey: "aboutTipCardState") as? Bool ?? true + }() + + private var tipHostingController: UIHostingController! + private var tableViewOffset: CGFloat? + private let cornerRadius: CGFloat = 12 + + override func viewDidLoad() { + super.viewDidLoad() + showTipCardView() + title = NSLocalizedString( + "i18n.app.about.title", + value: "About", + comment: "" + ) + + tableView.register( + WrapperCell.self, + forCellReuseIdentifier: WrapperCell.reuseIdentifier + ) + + tableView.register( + UINib(nibName: "AboutTableViewCell", bundle: nil), + forCellReuseIdentifier: AboutTableViewCell.reuseIdentifier + ) } - let section = dataSet[indexPath.section] - let setting = section.section[indexPath.row] + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() - cell.configure(withCellNamed: "AboutTableViewCell", section: setting) + if tableViewOffset == nil, UIDevice.current.userInterfaceIdiom != .pad { + tableViewOffset = tableView.contentOffset.y + } + } +} - let isFirstRow = indexPath.row == 0 - let isLastRow = indexPath.row == section.section.count - 1 - WrapperCell.applyCornerRadius(to: cell, isFirst: isFirstRow, isLast: isLastRow) +// MARK: UITableViewDataSource - return cell - } +extension AboutViewController { + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) + -> UITableViewCell { + guard + let cell = tableView.dequeueReusableCell( + withIdentifier: WrapperCell.reuseIdentifier, + for: indexPath + ) as? WrapperCell + else { + fatalError("Failed to dequeue WrapperCell") + } + + let section = dataSet[indexPath.section] + let setting = section.section[indexPath.row] + + cell.configure(withCellNamed: "AboutTableViewCell", section: setting) + + let isFirstRow = indexPath.row == 0 + let isLastRow = indexPath.row == section.section.count - 1 + WrapperCell.applyCornerRadius(to: cell, isFirst: isFirstRow, isLast: isLastRow) + + return cell + } } // MARK: UITableViewDelegate extension AboutViewController { - override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let section = dataSet[indexPath.section] - let setting = section.section[indexPath.row] - - let hasDescription = setting.shortDescription != nil - return hasDescription ? 80.0 : 48.0 - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let tableSection = dataSet[indexPath.section] - let section = tableSection.section[indexPath.row] - - switch section.sectionState { - case .github: - openURLString(urlString: "https://github.com/scribe-org/Scribe-iOS", withEncoding: false) - - case .matrix: - openURLString(urlString: "https://matrix.to/#/#scribe_community:matrix.org", withEncoding: true) - - case .mastodon: - openURLString(urlString: "https://wikis.world/@scribe", withEncoding: false) - - case .scribeApps: - openURLString(urlString: "https://scri.be/", withEncoding: false) - - case .wikimedia: - if let viewController = storyboard?.instantiateViewController( - identifier: "InformationScreenVC" - ) as? InformationScreenVC { - navigationController?.pushViewController(viewController, animated: true) - viewController.section = .wikimedia - } - - case .shareScribe: - showShareSheet() - - case .rateScribe: - showRateScribeUI() - - case .bugReport: - openURLString( - urlString: "https://github.com/scribe-org/Scribe-iOS/issues", withEncoding: false - ) - case .version: - openURLString( - urlString: "https://github.com/scribe-org/Scribe-iOS/releases", withEncoding: false - ) - - case .email: - showEmailUI() - - case .appHints: - let userDefaults = UserDefaults.standard - userDefaults.set(true, forKey: "installationTipCardState") - userDefaults.set(true, forKey: "settingsTipCardState") - userDefaults.set(true, forKey: "aboutTipCardState") - - startGlowingEffect(on: tipHostingController.view) - - case .privacyPolicy: - if let viewController = storyboard?.instantiateViewController( - identifier: "InformationScreenVC" - ) as? InformationScreenVC { - navigationController?.pushViewController(viewController, animated: true) - viewController.section = .privacyPolicy - } - - case .licenses: - if let viewController = storyboard?.instantiateViewController( - identifier: "InformationScreenVC" - ) as? InformationScreenVC { - navigationController?.pushViewController(viewController, animated: true) - viewController.section = .licenses - } - - default: break + override func tableView(_: UITableView, heightForRowAt indexPath: IndexPath) + -> CGFloat { + let section = dataSet[indexPath.section] + let setting = section.section[indexPath.row] + + let hasDescription = setting.shortDescription != nil + return hasDescription ? 80.0 : 48.0 } - if let selectedIndexPath = tableView.indexPathForSelectedRow { - tableView.deselectRow(at: selectedIndexPath, animated: false) + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let tableSection = dataSet[indexPath.section] + let section = tableSection.section[indexPath.row] + + switch section.sectionState { + case .github: + openURLString( + urlString: "https://github.com/scribe-org/Scribe-iOS", withEncoding: false + ) + + case .matrix: + openURLString( + urlString: "https://matrix.to/#/#scribe_community:matrix.org", withEncoding: true + ) + + case .mastodon: + openURLString(urlString: "https://wikis.world/@scribe", withEncoding: false) + + case .scribeApps: + openURLString(urlString: "https://scri.be/", withEncoding: false) + + case .wikimedia: + if let viewController = storyboard?.instantiateViewController( + identifier: "InformationScreenVC" + ) as? InformationScreenVC { + navigationController?.pushViewController(viewController, animated: true) + viewController.section = .wikimedia + } + + case .shareScribe: + showShareSheet() + + case .rateScribe: + showRateScribeUI() + + case .bugReport: + openURLString( + urlString: "https://github.com/scribe-org/Scribe-iOS/issues", withEncoding: false + ) + + case .version: + openURLString( + urlString: "https://github.com/scribe-org/Scribe-iOS/releases", withEncoding: false + ) + + case .email: + showEmailUI() + + case .appHints: + let userDefaults = UserDefaults.standard + userDefaults.set(true, forKey: "installationTipCardState") + userDefaults.set(true, forKey: "settingsTipCardState") + userDefaults.set(true, forKey: "aboutTipCardState") + + startGlowingEffect(on: tipHostingController.view) + + case .privacyPolicy: + if let viewController = storyboard?.instantiateViewController( + identifier: "InformationScreenVC" + ) as? InformationScreenVC { + navigationController?.pushViewController(viewController, animated: true) + viewController.section = .privacyPolicy + } + + case .licenses: + if let viewController = storyboard?.instantiateViewController( + identifier: "InformationScreenVC" + ) as? InformationScreenVC { + navigationController?.pushViewController(viewController, animated: true) + viewController.section = .licenses + } + + default: break + } + + if let selectedIndexPath = tableView.indexPathForSelectedRow { + tableView.deselectRow(at: selectedIndexPath, animated: false) + } } - } - - private func openURLString(urlString: String, withEncoding: Bool) { - if withEncoding { - let encodedString = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) - guard let encodedURLString = encodedString, let url = URL(string: encodedURLString) else { return } - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } else { - guard let url = URL(string: urlString) else { return } - UIApplication.shared.open(url, options: [:], completionHandler: nil) + + private func openURLString(urlString: String, withEncoding: Bool) { + if withEncoding { + let encodedString = urlString.addingPercentEncoding( + withAllowedCharacters: .urlQueryAllowed + ) + guard let encodedURLString = encodedString, let url = URL(string: encodedURLString) + else { + return + } + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } else { + guard let url = URL(string: urlString) else { return } + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } } - } - - private func showRateScribeUI() { - guard let scene = UIApplication.shared.foregroundActiveScene else { return } - SKStoreReviewController.requestReview(in: scene) - } - - private func openScribeAppStore(alert _: UIAlertAction) { - openURLString(urlString: "itms-apps: //itunes.apple.com/app/id1596613886", withEncoding: true) - } - - private func showEmailUI() { - if MFMailComposeViewController.canSendMail() { - let mailComposeViewController = MFMailComposeViewController() - mailComposeViewController.mailComposeDelegate = self - mailComposeViewController.setToRecipients(["team@scri.be"]) - mailComposeViewController.setSubject("Hey Scribe!") - - present(mailComposeViewController, animated: true, completion: nil) - } else { - // Show alert mentioning the email address. - let alert = UIAlertController( - title: NSLocalizedString("i18n.app.about.feedback.send_email", - value: "Send us an email", - comment: ""), - message: "Reach out to us at team@scri.be", preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - present(alert, animated: true) + + private func showRateScribeUI() { + guard let scene = UIApplication.shared.foregroundActiveScene else { return } + SKStoreReviewController.requestReview(in: scene) + } + + private func openScribeAppStore(alert _: UIAlertAction) { + openURLString( + urlString: "itms-apps: //itunes.apple.com/app/id1596613886", withEncoding: true + ) } - } - private func showShareSheet() { - let urlString = "itms-apps: //itunes.apple.com/app/id1596613886" - let encodedString = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) - guard let encodedURLString = encodedString, let url = URL(string: encodedURLString) else { return } + private func showEmailUI() { + if MFMailComposeViewController.canSendMail() { + let mailComposeViewController = MFMailComposeViewController() + mailComposeViewController.mailComposeDelegate = self + mailComposeViewController.setToRecipients(["team@scri.be"]) + mailComposeViewController.setSubject("Hey Scribe!") + + present(mailComposeViewController, animated: true, completion: nil) + } else { + // Show alert mentioning the email address. + let alert = UIAlertController( + title: NSLocalizedString( + "i18n.app.about.feedback.send_email", + value: "Send us an email", + comment: "" + ), + message: "Reach out to us at team@scri.be", preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + present(alert, animated: true) + } + } + + private func showShareSheet() { + let urlString = "itms-apps: //itunes.apple.com/app/id1596613886" + let encodedString = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) + guard let encodedURLString = encodedString, let url = URL(string: encodedURLString) + else { + return + } - let shareSheetVC = UIActivityViewController(activityItems: [url], applicationActivities: nil) + let shareSheetVC = UIActivityViewController( + activityItems: [url], applicationActivities: nil + ) - present(shareSheetVC, animated: true, completion: nil) - } + present(shareSheetVC, animated: true, completion: nil) + } } // MARK: MFMailComposeViewControllerDelegate extension AboutViewController: MFMailComposeViewControllerDelegate { - func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith _: MFMailComposeResult, error _: Error?) { - controller.dismiss(animated: true, completion: nil) - } + func mailComposeController( + _ controller: MFMailComposeViewController, didFinishWith _: MFMailComposeResult, + error _: Error? + ) { + controller.dismiss(animated: true, completion: nil) + } } // MARK: TipCardView + extension AboutViewController { - private func showTipCardView() { - let overlayView = AboutTipCardView( - aboutTipCardState: aboutTipCardState - ) - - let hostingController = UIHostingController(rootView: overlayView) - tipHostingController = hostingController - hostingController.view.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: -40) - hostingController.view.backgroundColor = .clear - hostingController.view.translatesAutoresizingMaskIntoConstraints = false - hostingController.view.isUserInteractionEnabled = true - - let navigationView = navigationController?.navigationBar - guard let navigationView else { return } - navigationView.addSubview(hostingController.view) - navigationView.bringSubviewToFront(hostingController.view) - - NSLayoutConstraint.activate([ - hostingController.view.topAnchor.constraint(equalTo: navigationView.topAnchor, constant: 30), - hostingController.view.leadingAnchor.constraint(equalTo: navigationView.leadingAnchor), - hostingController.view.trailingAnchor.constraint(equalTo: navigationView.trailingAnchor) - ]) - hostingController.didMove(toParent: self) - startGlowingEffect(on: hostingController.view) - } - - override func scrollViewDidScroll(_ scrollView: UIScrollView) { - guard let hostingController = tipHostingController, - let tableViewOffset else { return } - - let currentOffset = scrollView.contentOffset.y - - if currentOffset > tableViewOffset { - // Scrolling up. - UIView.animate(withDuration: 0.2) { - hostingController.view.alpha = 0 - } - } else if currentOffset == tableViewOffset { - // Show the view only when scrolled to the top. - UIView.animate(withDuration: 0.1) { - hostingController.view.alpha = 1 - } + private func showTipCardView() { + let overlayView = AboutTipCardView( + aboutTipCardState: aboutTipCardState + ) + + let hostingController = UIHostingController(rootView: overlayView) + tipHostingController = hostingController + hostingController.view.frame = CGRect( + x: 0, y: 0, width: view.bounds.width, height: -40 + ) + hostingController.view.backgroundColor = .clear + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + hostingController.view.isUserInteractionEnabled = true + + let navigationView = navigationController?.navigationBar + guard let navigationView else { return } + navigationView.addSubview(hostingController.view) + navigationView.bringSubviewToFront(hostingController.view) + + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint( + equalTo: navigationView.topAnchor, constant: 30 + ), + hostingController.view.leadingAnchor.constraint(equalTo: navigationView.leadingAnchor), + hostingController.view.trailingAnchor.constraint( + equalTo: navigationView.trailingAnchor + ) + ]) + hostingController.didMove(toParent: self) + startGlowingEffect(on: hostingController.view) + } + + override func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard let hostingController = tipHostingController, + let tableViewOffset + else { return } + + let currentOffset = scrollView.contentOffset.y + + if currentOffset > tableViewOffset { + // Scrolling up. + UIView.animate(withDuration: 0.2) { + hostingController.view.alpha = 0 + } + } else if currentOffset == tableViewOffset { + // Show the view only when scrolled to the top. + UIView.animate(withDuration: 0.1) { + hostingController.view.alpha = 1 + } + } + } + + func startGlowingEffect(on view: UIView, duration: TimeInterval = 1.0) { + view.layer.shadowColor = UIColor.scribeCTA.cgColor + view.layer.shadowRadius = 8 + view.layer.shadowOpacity = 0.0 + view.layer.shadowOffset = CGSize(width: 0, height: 0) + + UIView.animate( + withDuration: duration, + delay: 0, + options: [.curveEaseOut, .autoreverse], + animations: { + view.layer.shadowOpacity = 0.6 + }, completion: nil + ) } - } - - func startGlowingEffect(on view: UIView, duration: TimeInterval = 1.0) { - view.layer.shadowColor = UIColor.scribeCTA.cgColor - view.layer.shadowRadius = 8 - view.layer.shadowOpacity = 0.0 - view.layer.shadowOffset = CGSize(width: 0, height: 0) - - UIView.animate(withDuration: duration, - delay: 0, - options: [.curveEaseOut, .autoreverse], - animations: { - view.layer.shadowOpacity = 0.6 - }, completion: nil) - } } diff --git a/Scribe/AboutTab/InformationScreenVC.swift b/Scribe/AboutTab/InformationScreenVC.swift index e6715996..27ba5a99 100644 --- a/Scribe/AboutTab/InformationScreenVC.swift +++ b/Scribe/AboutTab/InformationScreenVC.swift @@ -1,201 +1,217 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Sets up information views in the About tab. */ import UIKit class InformationScreenVC: UIViewController { - @IBOutlet var scrollViewPhone: UIScrollView! - @IBOutlet var scrollViewPad: UIScrollView! - - @IBOutlet var scrollContainerViewPhone: UIView! - @IBOutlet var scrollContainerViewPad: UIView! - var scrollContainerView: UIView! - - @IBOutlet var relativeViewPhone: UIView! - @IBOutlet var relativeViewPad: UIView! - var relativeView: UIView! - - @IBOutlet var contentContainerViewPhone: UIView! - @IBOutlet var contentContainerViewPad: UIView! - var contentContainerView: UIView! - - @IBOutlet var headingLabelPhone: UILabel! - @IBOutlet var headingLabelPad: UILabel! - var headingLabel: UILabel! - - @IBOutlet var textViewPhone: UITextView! - @IBOutlet var textViewPad: UITextView! - var textView: UITextView! - - var text: String = "" - var section: SectionState = .privacyPolicy - - func setAppTextView() { - if DeviceType.isPad { - scrollContainerView = scrollContainerViewPad - relativeView = relativeViewPad - contentContainerView = contentContainerViewPad - headingLabel = headingLabelPad - textView = textViewPad - - scrollViewPhone.removeFromSuperview() - scrollContainerViewPhone.removeFromSuperview() - relativeViewPhone.removeFromSuperview() - contentContainerViewPhone.removeFromSuperview() - headingLabelPhone.removeFromSuperview() - textViewPhone.removeFromSuperview() - } else { - scrollContainerView = scrollContainerViewPhone - relativeView = relativeViewPhone - contentContainerView = contentContainerViewPhone - headingLabel = headingLabelPhone - textView = textViewPhone - - scrollViewPad.removeFromSuperview() - scrollContainerViewPad.removeFromSuperview() - relativeViewPad.removeFromSuperview() - contentContainerViewPad.removeFromSuperview() - headingLabelPad.removeFromSuperview() - textViewPad.removeFromSuperview() + @IBOutlet var scrollViewPhone: UIScrollView! + @IBOutlet var scrollViewPad: UIScrollView! + + @IBOutlet var scrollContainerViewPhone: UIView! + @IBOutlet var scrollContainerViewPad: UIView! + var scrollContainerView: UIView! + + @IBOutlet var relativeViewPhone: UIView! + @IBOutlet var relativeViewPad: UIView! + var relativeView: UIView! + + @IBOutlet var contentContainerViewPhone: UIView! + @IBOutlet var contentContainerViewPad: UIView! + var contentContainerView: UIView! + + @IBOutlet var headingLabelPhone: UILabel! + @IBOutlet var headingLabelPad: UILabel! + var headingLabel: UILabel! + + @IBOutlet var textViewPhone: UITextView! + @IBOutlet var textViewPad: UITextView! + var textView: UITextView! + + var text: String = "" + var section: SectionState = .privacyPolicy + + func setAppTextView() { + if DeviceType.isPad { + scrollContainerView = scrollContainerViewPad + relativeView = relativeViewPad + contentContainerView = contentContainerViewPad + headingLabel = headingLabelPad + textView = textViewPad + + scrollViewPhone.removeFromSuperview() + scrollContainerViewPhone.removeFromSuperview() + relativeViewPhone.removeFromSuperview() + contentContainerViewPhone.removeFromSuperview() + headingLabelPhone.removeFromSuperview() + textViewPhone.removeFromSuperview() + } else { + scrollContainerView = scrollContainerViewPhone + relativeView = relativeViewPhone + contentContainerView = contentContainerViewPhone + headingLabel = headingLabelPhone + textView = textViewPhone + + scrollViewPad.removeFromSuperview() + scrollContainerViewPad.removeFromSuperview() + relativeViewPad.removeFromSuperview() + contentContainerViewPad.removeFromSuperview() + headingLabelPad.removeFromSuperview() + textViewPad.removeFromSuperview() + } } - } - override func viewDidLoad() { - super.viewDidLoad() + override func viewDidLoad() { + super.viewDidLoad() - setupInformationPageUI() + setupInformationPageUI() - if section == .privacyPolicy { - setupPrivacyPolicyPage() - } else if section == .licenses { - setupLicensesPage() - } else { - setupWikimediaAndScribePage() + if section == .privacyPolicy { + setupPrivacyPolicyPage() + } else if section == .licenses { + setupLicensesPage() + } else { + setupWikimediaAndScribePage() + } } - } - - /// Needed since Wikimedia and Scribe have an image as an attachment in the text. - /// Thus, it doesn't dynamically switch on theme change like a UIImage would. - /// Therefore, monitor for theme change and manually re-render textView. - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { - if section == .wikimedia { - textView.attributedText = switchAttachmentOnThemeChange(for: textView.attributedText) - } + + /// Needed since Wikimedia and Scribe have an image as an attachment in the text. + /// Thus, it doesn't dynamically switch on theme change like a UIImage would. + /// Therefore, monitor for theme change and manually re-render textView. + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + if section == .wikimedia { + textView.attributedText = switchAttachmentOnThemeChange( + for: textView.attributedText + ) + } + } } - } - func setupInformationPageUI() { - setAppTextView() + func setupInformationPageUI() { + setAppTextView() - textView.backgroundColor = .clear - scrollContainerView.backgroundColor = .clear - relativeView.backgroundColor = .clear + textView.backgroundColor = .clear + scrollContainerView.backgroundColor = .clear + relativeView.backgroundColor = .clear - contentContainerView.backgroundColor = lightWhiteDarkBlackColor - applyCornerRadius(elem: contentContainerView, radius: contentContainerView.frame.width * 0.05) + contentContainerView.backgroundColor = lightWhiteDarkBlackColor + applyCornerRadius( + elem: contentContainerView, radius: contentContainerView.frame.width * 0.05 + ) - contentContainerView.clipsToBounds = true + contentContainerView.clipsToBounds = true - textView.isEditable = false - } + textView.isEditable = false + } - func setupPrivacyPolicyPage() { - navigationItem.title = NSLocalizedString("i18n._global.privacy_policy", value: "Privacy policy", comment: "") - headingLabel.attributedText = NSMutableAttributedString( - string: NSLocalizedString("i18n.app.about.legal.privacy_policy.caption", value: "Keeping you safe", comment: ""), - attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: fontSize * 1.1)] - ) - textView.attributedText = setPrivacyPolicy(fontSize: fontSize, text: NSLocalizedString("i18n.app.about.legal.privacy_policy.text", value: """ -Please note that the English version of this policy takes precedence over all other versions. + func setupPrivacyPolicyPage() { + navigationItem.title = NSLocalizedString( + "i18n._global.privacy_policy", value: "Privacy policy", comment: "" + ) + headingLabel.attributedText = NSMutableAttributedString( + string: NSLocalizedString( + "i18n.app.about.legal.privacy_policy.caption", value: "Keeping you safe", + comment: "" + ), + attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: fontSize * 1.1)] + ) + textView.attributedText = setPrivacyPolicy( + fontSize: fontSize, + text: NSLocalizedString( + "i18n.app.about.legal.privacy_policy.text", + value: """ + Please note that the English version of this policy takes precedence over all other versions. -The Scribe developers (SCRIBE) built the iOS application "Scribe - Language Keyboards" (SERVICE) as an open-source application. This SERVICE is provided by SCRIBE at no cost and is intended for use as is. + The Scribe developers (SCRIBE) built the iOS application "Scribe - Language Keyboards" (SERVICE) as an open-source application. This SERVICE is provided by SCRIBE at no cost and is intended for use as is. -This privacy policy (POLICY) is used to inform the reader of the policies for the access, tracking, collection, retention, use, and disclosure of personal information (USER INFORMATION) and usage data (USER DATA) for all individuals who make use of this SERVICE (USERS). + This privacy policy (POLICY) is used to inform the reader of the policies for the access, tracking, collection, retention, use, and disclosure of personal information (USER INFORMATION) and usage data (USER DATA) for all individuals who make use of this SERVICE (USERS). -USER INFORMATION is specifically defined as any information related to the USERS themselves or the devices they use to access the SERVICE. + USER INFORMATION is specifically defined as any information related to the USERS themselves or the devices they use to access the SERVICE. -USER DATA is specifically defined as any text that is typed or actions that are done by the USERS while using the SERVICE. + USER DATA is specifically defined as any text that is typed or actions that are done by the USERS while using the SERVICE. -1. Policy Statement + 1. Policy Statement -This SERVICE does not access, track, collect, retain, use, or disclose any USER INFORMATION or USER DATA. + This SERVICE does not access, track, collect, retain, use, or disclose any USER INFORMATION or USER DATA. -2. Do Not Track + 2. Do Not Track -USERS contacting SCRIBE to ask that their USER INFORMATION and USER DATA not be tracked will be provided with a copy of this POLICY as well as a link to all source codes as proof that they are not being tracked. + USERS contacting SCRIBE to ask that their USER INFORMATION and USER DATA not be tracked will be provided with a copy of this POLICY as well as a link to all source codes as proof that they are not being tracked. -3. Third-Party Data + 3. Third-Party Data -This SERVICE makes use of third-party data. All data used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. Specifically, the data for this SERVICE comes from Wikidata, Wikipedia and Unicode. Wikidata states that, "All structured data in the main, property and lexeme namespaces is made available under the Creative Commons CC0 License; text in other namespaces is made available under the Creative Commons Attribution-Share Alike License." The policy detailing Wikidata data usage can be found at https://www.wikidata.org/wiki/Wikidata:Licensing. Wikipedia states that text data, the type of data used by the SERVICE, "… can be used under the terms of the Creative Commons Attribution Share-Alike license". The policy detailing Wikipedia data usage can be found at https://en.wikipedia.org/wiki/Wikipedia:Reusing_Wikipedia_content. Unicode provides permission, "… free of charge, to any person obtaining a copy of the Unicode data files and any associated documentation (the "Data Files") or Unicode software and any associated documentation (the "Software") to deal in the Data Files or Software without restriction…" The policy detailing Unicode data usage can be found at https://www.unicode.org/license.txt. + This SERVICE makes use of third-party data. All data used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. Specifically, the data for this SERVICE comes from Wikidata, Wikipedia and Unicode. Wikidata states that, "All structured data in the main, property and lexeme namespaces is made available under the Creative Commons CC0 License; text in other namespaces is made available under the Creative Commons Attribution-Share Alike License." The policy detailing Wikidata data usage can be found at https://www.wikidata.org/wiki/Wikidata:Licensing. Wikipedia states that text data, the type of data used by the SERVICE, "… can be used under the terms of the Creative Commons Attribution Share-Alike license". The policy detailing Wikipedia data usage can be found at https://en.wikipedia.org/wiki/Wikipedia:Reusing_Wikipedia_content. Unicode provides permission, "… free of charge, to any person obtaining a copy of the Unicode data files and any associated documentation (the "Data Files") or Unicode software and any associated documentation (the "Software") to deal in the Data Files or Software without restriction…" The policy detailing Unicode data usage can be found at https://www.unicode.org/license.txt. -4. Third-Party Source Code + 4. Third-Party Source Code -This SERVICE was based on third-party code. All source code used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. Specifically, the basis of this project was the project Custom Keyboard by Ethan Sarif-Kattan. Custom Keyboard was released under an MIT license, with this license being available at https://github.com/EthanSK/CustomKeyboard/blob/master/LICENSE. + This SERVICE was based on third-party code. All source code used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. Specifically, the basis of this project was the project Custom Keyboard by Ethan Sarif-Kattan. Custom Keyboard was released under an MIT license, with this license being available at https://github.com/EthanSK/CustomKeyboard/blob/master/LICENSE. -5. Third-Party Services + 5. Third-Party Services -This SERVICE makes use of third-party services to manipulate some of the third-party data. Specifically, data has been translated using models from Hugging Face transformers. This service is covered by an Apache License 2.0, which states that it is available for commercial use, modification, distribution, patent use, and private use. The license for the aforementioned service can be found at https://github.com/huggingface/transformers/blob/master/LICENSE. + This SERVICE makes use of third-party services to manipulate some of the third-party data. Specifically, data has been translated using models from Hugging Face transformers. This service is covered by an Apache License 2.0, which states that it is available for commercial use, modification, distribution, patent use, and private use. The license for the aforementioned service can be found at https://github.com/huggingface/transformers/blob/master/LICENSE. -6. Third-Party Links + 6. Third-Party Links -This SERVICE contains links to external websites. If USERS click on a third-party link, they will be directed to a website. Note that these external websites are not operated by this SERVICE. Therefore, USERS are strongly advised to review the privacy policy of these websites. This SERVICE has no control over and assumes no responsibility for the content, privacy policies, or practices of any third-party sites or services. + This SERVICE contains links to external websites. If USERS click on a third-party link, they will be directed to a website. Note that these external websites are not operated by this SERVICE. Therefore, USERS are strongly advised to review the privacy policy of these websites. This SERVICE has no control over and assumes no responsibility for the content, privacy policies, or practices of any third-party sites or services. -7. Third-Party Images + 7. Third-Party Images -This SERVICE contains images that are copyrighted by third-parties. Specifically, this app includes a copy of the logos of GitHub, Inc and Wikidata, trademarked by Wikimedia Foundation, Inc. The terms by which the GitHub logo can be used are found on https://github.com/logos, and the terms for the Wikidata logo are found on the following Wikimedia page: https://foundation.wikimedia.org/wiki/Policy:Trademark_policy. This SERVICE uses the copyrighted images in a way that matches these criteria, with the only deviation being a rotation of the GitHub logo that is common in the open-source community to indicate that there is a link to the GitHub website. + This SERVICE contains images that are copyrighted by third-parties. Specifically, this app includes a copy of the logos of GitHub, Inc and Wikidata, trademarked by Wikimedia Foundation, Inc. The terms by which the GitHub logo can be used are found on https://github.com/logos, and the terms for the Wikidata logo are found on the following Wikimedia page: https://foundation.wikimedia.org/wiki/Policy:Trademark_policy. This SERVICE uses the copyrighted images in a way that matches these criteria, with the only deviation being a rotation of the GitHub logo that is common in the open-source community to indicate that there is a link to the GitHub website. -8. Content Notice + 8. Content Notice -This SERVICE allows USERS to access linguistic content (CONTENT). Some of this CONTENT could be deemed inappropriate for children and legal minors. Accessing CONTENT using the SERVICE is done in a way that the information is unavailable unless explicitly known. Specifically, USERS "can" translate words, conjugate verbs, and access other grammatical features of CONTENT that may be sexual, violent, or otherwise inappropriate in nature. USERS "cannot" translate words, conjugate verbs, and access other grammatical features of CONTENT that may be sexual, violent, or otherwise inappropriate in nature if they do not already know about the nature of this CONTENT. SCRIBE takes no responsibility for the access of such CONTENT. + This SERVICE allows USERS to access linguistic content (CONTENT). Some of this CONTENT could be deemed inappropriate for children and legal minors. Accessing CONTENT using the SERVICE is done in a way that the information is unavailable unless explicitly known. Specifically, USERS "can" translate words, conjugate verbs, and access other grammatical features of CONTENT that may be sexual, violent, or otherwise inappropriate in nature. USERS "cannot" translate words, conjugate verbs, and access other grammatical features of CONTENT that may be sexual, violent, or otherwise inappropriate in nature if they do not already know about the nature of this CONTENT. SCRIBE takes no responsibility for the access of such CONTENT. -9. Changes + 9. Changes -This POLICY is subject to change. Updates to this POLICY will replace all prior instances, and if deemed material will further be clearly stated in the next applicable update to the SERVICE. SCRIBE encourages USERS to periodically review this POLICY for the latest information on our privacy practices and to familiarize themselves with any changes. + This POLICY is subject to change. Updates to this POLICY will replace all prior instances, and if deemed material will further be clearly stated in the next applicable update to the SERVICE. SCRIBE encourages USERS to periodically review this POLICY for the latest information on our privacy practices and to familiarize themselves with any changes. -10. Contact + 10. Contact -If you have any questions, concerns, or suggestions about this POLICY, do not hesitate to visit https://github.com/scribe-org or contact SCRIBE at scribe.langauge@gmail.com. The person responsible for such inquiries is Andrew Tavis McAllister. + If you have any questions, concerns, or suggestions about this POLICY, do not hesitate to visit https://github.com/scribe-org or contact SCRIBE at scribe.langauge@gmail.com. The person responsible for such inquiries is Andrew Tavis McAllister. -11. Effective Date + 11. Effective Date -This POLICY is effective as of the 24th of May, 2022. -""", comment: "")) - textView.textColor = keyCharColor - textView.linkTextAttributes = [ - NSAttributedString.Key.foregroundColor: linkBlueColor - ] - } + This POLICY is effective as of the 24th of May, 2022. + """, comment: "" + ) + ) + textView.textColor = keyCharColor + textView.linkTextAttributes = [ + NSAttributedString.Key.foregroundColor: linkBlueColor + ] + } - func setupLicensesPage() { - navigationItem.title = thirdPartyLicensesTitle - headingLabel.attributedText = NSMutableAttributedString( - string: thirdPartyLicensesCaption, - attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: fontSize * 1.1)] - ) - textView.attributedText = setThirdPartyLicenses( - fontSize: fontSize, text: thirdPartyLicensesText - ) - textView.textColor = keyCharColor - textView.linkTextAttributes = [ - NSAttributedString.Key.foregroundColor: linkBlueColor - ] - } + func setupLicensesPage() { + navigationItem.title = thirdPartyLicensesTitle + headingLabel.attributedText = NSMutableAttributedString( + string: thirdPartyLicensesCaption, + attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: fontSize * 1.1)] + ) + textView.attributedText = setThirdPartyLicenses( + fontSize: fontSize, text: thirdPartyLicensesText + ) + textView.textColor = keyCharColor + textView.linkTextAttributes = [ + NSAttributedString.Key.foregroundColor: linkBlueColor + ] + } - func setupWikimediaAndScribePage() { - navigationItem.title = wikimediaAndScribeTitle - headingLabel.attributedText = NSMutableAttributedString( - string: wikimediaAndScribeCaption, - attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: fontSize * 1.1)] - ) - textView.attributedText = setWikimediaAndScribe( - text: wikimediaAndScribeText, fontSize: fontSize, imageWidth: contentContainerView.frame.width * 0.6 - ) - textView.textColor = keyCharColor - } + func setupWikimediaAndScribePage() { + navigationItem.title = wikimediaAndScribeTitle + headingLabel.attributedText = NSMutableAttributedString( + string: wikimediaAndScribeCaption, + attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: fontSize * 1.1)] + ) + textView.attributedText = setWikimediaAndScribe( + text: wikimediaAndScribeText, fontSize: fontSize, + imageWidth: contentContainerView.frame.width * 0.6 + ) + textView.textColor = keyCharColor + } } diff --git a/Scribe/AppDelegate.swift b/Scribe/AppDelegate.swift index 5f7b51f5..b00aa906 100644 --- a/Scribe/AppDelegate.swift +++ b/Scribe/AppDelegate.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Class of methods to manage Scribe's behaviors. */ @@ -9,145 +9,145 @@ import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - - func application( - _: UIApplication, - didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - initializeFontSize() - // Override point for customization after application launch. - if #available(iOS 13.0, *) { - let appearance = UITabBarAppearance() - appearance.configureWithOpaqueBackground() - if UITraitCollection.current.userInterfaceStyle == .dark { - appearance.backgroundColor = .black - } else { - appearance.backgroundColor = .white - } - UITabBar.appearance().standardAppearance = appearance - UITabBar.appearance().scrollEdgeAppearance = appearance - } else { - UITabBar.appearance().isTranslucent = false - UITabBar.appearance().barTintColor = .white // default non-transparent color + var window: UIWindow? + + func application( + _: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + initializeFontSize() + // Override point for customization after application launch. + if #available(iOS 13.0, *) { + let appearance = UITabBarAppearance() + appearance.configureWithOpaqueBackground() + if UITraitCollection.current.userInterfaceStyle == .dark { + appearance.backgroundColor = .black + } else { + appearance.backgroundColor = .white + } + UITabBar.appearance().standardAppearance = appearance + UITabBar.appearance().scrollEdgeAppearance = appearance + } else { + UITabBar.appearance().isTranslucent = false + UITabBar.appearance().barTintColor = .white // default non-transparent color + } + + return true } - return true - } - - func applicationWillResignActive(_: UIApplication) { - /* - Sent when the application is about to move from active to inactive state. - This can occur for certain types of temporary interruptions: - - Incoming phone call or SMS message - - When the user quits the application and it begins the transition to the background state - Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. - Games should use this method to pause the game. - */ - } - - func applicationDidEnterBackground(_: UIApplication) { - /* - Use this method to: - - Release shared resources - - Save user data - - Invalidate timers - - Store enough application state information to restore your application to its current state - - This is in case it is terminated later - - If your application supports background execution: - - This method is called instead of applicationWillTerminate: when the user quits - */ - - /// Hacky fix to update the installed keyboard list that needs viewWillAppear to update. - /// If we redirect the user to the first screen when leaving the app. - /// viewWillAppear will always be called when going back to the Settings screen. - /// Also set for the About screen for consistency. - if let tabBarController = window?.rootViewController as? UITabBarController { - if tabBarController.selectedIndex != 0 { - tabBarController.selectedIndex = 0 - } - - // Pop to root in Installation tab's navigation controller. - if let navController = tabBarController.viewControllers?[0] as? UINavigationController { - navController.popToRootViewController(animated: false) - } + func applicationWillResignActive(_: UIApplication) { + /* + Sent when the application is about to move from active to inactive state. + This can occur for certain types of temporary interruptions: + - Incoming phone call or SMS message + - When the user quits the application and it begins the transition to the background state + Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. + Games should use this method to pause the game. + */ } - } - - func applicationWillEnterForeground(_: UIApplication) { - /* - Called as part of the transition from the background to the active state. - Here you can undo many of the changes made on entering the background. - */ - } - - func applicationDidBecomeActive(_: UIApplication) { - /* - Restart any tasks that were paused (or not yet started) while the application was inactive. - If the application was previously in the background, optionally refresh the user interface. - */ - } - - func applicationWillTerminate(_: UIApplication) { - /* - Called when the application is about to terminate. - Save data if appropriate. - See also applicationDidEnterBackground:. - Saves changes in the application's managed object context before the application terminates. - */ - saveContext() - } - - // MARK: Core Data stack - - lazy var persistentContainer: NSPersistentContainer = { - /* - The persistent container for the application. This implementation - creates and returns a container, having loaded the store for the - application to it. This property is optional since there are legitimate - error conditions that could cause the creation of the store to fail. - */ - let container = NSPersistentContainer(name: "Scribe") - container.loadPersistentStores(completionHandler: { _, error in - if let error = error as NSError? { + + func applicationDidEnterBackground(_: UIApplication) { /* - Replace this implementation with code to handle the error appropriately. - fatalError() causes the application to generate a crash log and terminate. - You should not use this function in a shipping application, although it may be useful during development. + Use this method to: + - Release shared resources + - Save user data + - Invalidate timers + - Store enough application state information to restore your application to its current state + - This is in case it is terminated later + + If your application supports background execution: + - This method is called instead of applicationWillTerminate: when the user quits */ + // Hacky fix to update the installed keyboard list that needs viewWillAppear to update. + // If we redirect the user to the first screen when leaving the app. + // viewWillAppear will always be called when going back to the Settings screen. + // Also set for the About screen for consistency. + if let tabBarController = window?.rootViewController as? UITabBarController { + if tabBarController.selectedIndex != 0 { + tabBarController.selectedIndex = 0 + } + + // Pop to root in Installation tab's navigation controller. + if let navController = tabBarController.viewControllers?[0] as? UINavigationController { + navController.popToRootViewController(animated: false) + } + } + } + + func applicationWillEnterForeground(_: UIApplication) { /* - Typical reasons for an error here include: - - The parent directory does not exist, cannot be created, or disallows writing. - - The persistent store is not accessible, due to permissions or data protection when the device is locked. - - The device is out of space. - - The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. + Called as part of the transition from the background to the active state. + Here you can undo many of the changes made on entering the background. */ + } - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - return container - }() + func applicationDidBecomeActive(_: UIApplication) { + /* + Restart any tasks that were paused (or not yet started) while the application was inactive. + If the application was previously in the background, optionally refresh the user interface. + */ + } + + func applicationWillTerminate(_: UIApplication) { + /* + Called when the application is about to terminate. + Save data if appropriate. + See also applicationDidEnterBackground:. + Saves changes in the application's managed object context before the application terminates. + */ + saveContext() + } - // MARK: Core Data Saving support + // MARK: Core Data stack - func saveContext() { - let context = persistentContainer.viewContext - if context.hasChanges { - do { - try context.save() - } catch { + lazy var persistentContainer: NSPersistentContainer = { /* - Replace this implementation with code to handle the error appropriately. - fatalError() causes the application to generate a crash log and terminate. - You should not use this function in a shipping application, although it may be useful during development. + The persistent container for the application. This implementation + creates and returns a container, having loaded the store for the + application to it. This property is optional since there are legitimate + error conditions that could cause the creation of the store to fail. */ - let nserror = error as NSError - fatalError("Unresolved error \(nserror), \(nserror.userInfo)") - } + let container = NSPersistentContainer(name: "Scribe") + container.loadPersistentStores(completionHandler: { _, error in + if let error = error as NSError? { + /* + Replace this implementation with code to handle the error appropriately. + fatalError() causes the application to generate a crash log and terminate. + You should not use this function in a shipping application, although it may be useful during development. + */ + + /* + Typical reasons for an error here include: + - The parent directory does not exist, cannot be created, or disallows writing. + - The persistent store is not accessible, due to permissions or data protection when the device is locked. + - The device is out of space. + - The store could not be migrated to the current model version. + Check the error message to determine what the actual problem was. + */ + + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + return container + }() + + // MARK: Core Data Saving support + + func saveContext() { + let context = persistentContainer.viewContext + if context.hasChanges { + do { + try context.save() + } catch { + /* + Replace this implementation with code to handle the error appropriately. + fatalError() causes the application to generate a crash log and terminate. + You should not use this function in a shipping application, although it may be useful during development. + */ + let nserror = error as NSError + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } + } } - } } diff --git a/Scribe/AppExtensions.swift b/Scribe/AppExtensions.swift index 11360acb..6d048453 100644 --- a/Scribe/AppExtensions.swift +++ b/Scribe/AppExtensions.swift @@ -1,58 +1,58 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Extensions for the Scribe app. */ import UIKit extension UIApplication { - var foregroundActiveScene: UIWindowScene? { - connectedScenes - .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene - } + var foregroundActiveScene: UIWindowScene? { + connectedScenes + .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene + } } extension UIImage { - static func availableIconImage(with imageString: String) -> UIImage { - if let image = UIImage(named: imageString) { - return image - } else { - if let image = UIImage(systemName: imageString) { - return image - } else { - guard let infoCircleSymbol = UIImage(systemName: "info.circle") else { - fatalError("Failed to create info circle symbol image.") + static func availableIconImage(with imageString: String) -> UIImage { + if let image = UIImage(named: imageString) { + return image + } else { + if let image = UIImage(systemName: imageString) { + return image + } else { + guard let infoCircleSymbol = UIImage(systemName: "info.circle") + else { + fatalError("Failed to create info circle symbol image.") + } + return infoCircleSymbol + } } - return infoCircleSymbol - } } - } } extension UIView { - var parentViewController: UIViewController? { - var currentResponder: UIResponder? = self - - while let responder = currentResponder { - if let viewController = responder as? UIViewController { - return viewController - } - currentResponder = responder.next - } + var parentViewController: UIViewController? { + var currentResponder: UIResponder? = self + + while let responder = currentResponder { + if let viewController = responder as? UIViewController { + return viewController + } + currentResponder = responder.next + } - return nil - } + return nil + } } extension Locale { - static var userSystemLanguage: String { - return String(Locale.preferredLanguages[0].prefix(2)).uppercased() - } + static var userSystemLanguage: String { + return String(Locale.preferredLanguages[0].prefix(2)).uppercased() + } } extension Notification.Name { - static let keyboardsUpdatedNotification = Notification.Name("keyboardsHaveUpdated") - static let fontSizeUpdatedNotification = Notification.Name("fontSizeHasUpdated") - + static let keyboardsUpdatedNotification = Notification.Name("keyboardsHaveUpdated") + static let fontSizeUpdatedNotification = Notification.Name("fontSizeHasUpdated") } diff --git a/Scribe/AppStyling.swift b/Scribe/AppStyling.swift index 9657447c..279ea3a1 100644 --- a/Scribe/AppStyling.swift +++ b/Scribe/AppStyling.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Functions to style app elements. */ @@ -11,10 +11,10 @@ import UIKit /// - Parameters /// - elem: the element to have shadows added to. func applyShadowEffects(elem: AnyObject) { - elem.layer.shadowColor = UIColor(ScribeColor.keyShadow).light.cgColor - elem.layer.shadowOffset = CGSize(width: 0.0, height: 3.0) - elem.layer.shadowOpacity = 1.0 - elem.layer.shadowRadius = 3.0 + elem.layer.shadowColor = UIColor(ScribeColor.keyShadow).light.cgColor + elem.layer.shadowOffset = CGSize(width: 0.0, height: 3.0) + elem.layer.shadowOpacity = 1.0 + elem.layer.shadowRadius = 3.0 } /// Applies a corner radius to a given UI element. @@ -22,6 +22,6 @@ func applyShadowEffects(elem: AnyObject) { /// - Parameters /// - elem: the element to have shadows added to. func applyCornerRadius(elem: AnyObject, radius: CGFloat) { - elem.layer.masksToBounds = false - elem.layer.cornerRadius = radius + elem.layer.masksToBounds = false + elem.layer.cornerRadius = radius } diff --git a/Scribe/AppTexts/AppTextStyling.swift b/Scribe/AppTexts/AppTextStyling.swift index 378d85f7..4e24831d 100644 --- a/Scribe/AppTexts/AppTextStyling.swift +++ b/Scribe/AppTexts/AppTextStyling.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Functions returning styled text elements for the app screen. */ @@ -10,33 +10,34 @@ let preferredLanguage = Locale.preferredLanguages[0] var fontSize = CGFloat(0) func initializeFontSize() { - if DeviceType.isPhone { - if UIScreen.main.bounds.width > 413 || UIScreen.main.bounds.width <= 375 { - fontSize = UIScreen.main.bounds.height / 59 - } else { - fontSize = UIScreen.main.bounds.height / 50 + if DeviceType.isPhone { + if UIScreen.main.bounds.width > 413 || UIScreen.main.bounds.width <= 375 { + fontSize = UIScreen.main.bounds.height / 59 + } else { + fontSize = UIScreen.main.bounds.height / 50 + } + } else if DeviceType.isPad { + fontSize = UIScreen.main.bounds.height / 50 + } + + let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! + if userDefaults.bool(forKey: "increaseTextSize") { + fontSize *= 1.25 } - } else if DeviceType.isPad { - fontSize = UIScreen.main.bounds.height / 50 - } - - let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! - if userDefaults.bool(forKey: "increaseTextSize") { - fontSize *= 1.25 - } } + /// Concatenates attributed strings. /// /// - Parameters /// - left: the left attributed string to concatenate. /// - right: the right attributed string to concatenate. func concatAttributedStrings( - left: NSAttributedString, right: NSAttributedString + left: NSAttributedString, right: NSAttributedString ) -> NSMutableAttributedString { - let result = NSMutableAttributedString() - result.append(left) - result.append(right) - return result + let result = NSMutableAttributedString() + result.append(left) + result.append(right) + return result } /// Returns an attributed text that is hyperlinked. @@ -44,21 +45,26 @@ func concatAttributedStrings( /// - Parameters /// - originalText: the original text that hyperlinks will be added to. /// - links: a dictionary of strings and the link to which they should link. -func addHyperLinks(originalText: String, links: [String: String], fontSize: CGFloat) -> NSMutableAttributedString { - let style = NSMutableParagraphStyle() - style.alignment = .left - let attributedOriginalText = NSMutableAttributedString( - string: originalText, - attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: fontSize)] - ) - for (hyperLink, urlString) in links { - let linkRange = attributedOriginalText.mutableString.range(of: hyperLink) - let fullRange = NSRange(location: 0, length: attributedOriginalText.length) - attributedOriginalText.addAttribute(NSAttributedString.Key.link, value: urlString, range: linkRange) - attributedOriginalText.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: fullRange) - } - - return attributedOriginalText +func addHyperLinks(originalText: String, links: [String: String], fontSize: CGFloat) + -> NSMutableAttributedString { + let style = NSMutableParagraphStyle() + style.alignment = .left + let attributedOriginalText = NSMutableAttributedString( + string: originalText, + attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: fontSize)] + ) + for (hyperLink, urlString) in links { + let linkRange = attributedOriginalText.mutableString.range(of: hyperLink) + let fullRange = NSRange(location: 0, length: attributedOriginalText.length) + attributedOriginalText.addAttribute( + NSAttributedString.Key.link, value: urlString, range: linkRange + ) + attributedOriginalText.addAttribute( + NSAttributedString.Key.paragraphStyle, value: style, range: fullRange + ) + } + + return attributedOriginalText } /// Returns an attributed text that has indentation for list items. @@ -66,22 +72,23 @@ func addHyperLinks(originalText: String, links: [String: String], fontSize: CGFl /// - Parameters /// - attributedOriginalText: the original text that hyperlinks will be added to passed as a NSMutableAttributedString. /// - links: a array of strings with the list items text. -func addTabStops(attributedOriginalText: NSMutableAttributedString, links: [String]) -> NSMutableAttributedString { - let textToAddIndentation = attributedOriginalText - - let listParagraphStyle = NSMutableParagraphStyle() - listParagraphStyle.headIndent = 46 - listParagraphStyle.firstLineHeadIndent = 36 - listParagraphStyle.lineBreakMode = .byCharWrapping - - for element in links { - let linkRange = textToAddIndentation.mutableString.range(of: element) - textToAddIndentation.addAttribute( - NSAttributedString.Key.paragraphStyle, value: listParagraphStyle, range: linkRange - ) - } +func addTabStops(attributedOriginalText: NSMutableAttributedString, links: [String]) + -> NSMutableAttributedString { + let textToAddIndentation = attributedOriginalText + + let listParagraphStyle = NSMutableParagraphStyle() + listParagraphStyle.headIndent = 46 + listParagraphStyle.firstLineHeadIndent = 36 + listParagraphStyle.lineBreakMode = .byCharWrapping + + for element in links { + let linkRange = textToAddIndentation.mutableString.range(of: element) + textToAddIndentation.addAttribute( + NSAttributedString.Key.paragraphStyle, value: listParagraphStyle, range: linkRange + ) + } - return textToAddIndentation + return textToAddIndentation } /// Formats and returns an arrow icon for the app texts. @@ -89,15 +96,17 @@ func addTabStops(attributedOriginalText: NSMutableAttributedString, links: [Stri /// - Parameters /// - fontSize: the size of the font derived for the app text given screen dimensions. func getArrowIcon(fontSize: CGFloat) -> NSAttributedString { - // The down right arrow character as a text attachment. - let arrowAttachment = NSTextAttachment() - let selectArrowIconConfig = UIImage.SymbolConfiguration(pointSize: fontSize, weight: .medium, scale: .medium) - arrowAttachment.image = UIImage( - systemName: "arrow.turn.down.right", - withConfiguration: selectArrowIconConfig - )?.withTintColor(.init(ScribeColor.keyChar)) - - return NSAttributedString(attachment: arrowAttachment) + // The down right arrow character as a text attachment. + let arrowAttachment = NSTextAttachment() + let selectArrowIconConfig = UIImage.SymbolConfiguration( + pointSize: fontSize, weight: .medium, scale: .medium + ) + arrowAttachment.image = UIImage( + systemName: "arrow.turn.down.right", + withConfiguration: selectArrowIconConfig + )?.withTintColor(.init(ScribeColor.keyChar)) + + return NSAttributedString(attachment: arrowAttachment) } /// Formats and returns an arrow icon for the app texts. @@ -105,15 +114,17 @@ func getArrowIcon(fontSize: CGFloat) -> NSAttributedString { /// - Parameters /// - fontSize: the size of the font derived for the app text given screen dimensions. func getGlobeIcon(fontSize: CGFloat) -> NSAttributedString { - // The globe character as a text attachment. - let globeAttachment = NSTextAttachment() - let selectGlobeIconConfig = UIImage.SymbolConfiguration(pointSize: fontSize, weight: .medium, scale: .medium) - globeAttachment.image = UIImage( - systemName: "globe", - withConfiguration: selectGlobeIconConfig - )?.withTintColor(.init(ScribeColor.keyChar)) - - return NSAttributedString(attachment: globeAttachment) + // The globe character as a text attachment. + let globeAttachment = NSTextAttachment() + let selectGlobeIconConfig = UIImage.SymbolConfiguration( + pointSize: fontSize, weight: .medium, scale: .medium + ) + globeAttachment.image = UIImage( + systemName: "globe", + withConfiguration: selectGlobeIconConfig + )?.withTintColor(.init(ScribeColor.keyChar)) + + return NSAttributedString(attachment: globeAttachment) } /// Returns image as an attachment to a NSMutableAttributedString which can be appended with the rest of the text. @@ -121,28 +132,31 @@ func getGlobeIcon(fontSize: CGFloat) -> NSAttributedString { /// - Parameters /// - imageString: name of the image asset to add as attachment. /// - imageWidth: width to scale the image appropriately to fit in text view. -func getCenteredImagesForWikimediaAndScribe(imageString: String, imageWidth: CGFloat) -> NSMutableAttributedString { - if let image = UIImage(named: imageString) { - let imageParagraphStyle = NSMutableParagraphStyle() - imageParagraphStyle.alignment = .center - - let imageAttachment = NSTextAttachment() - let aspectRatio = image.size.width / image.size.height - let imageHeight = imageWidth / aspectRatio - imageAttachment.image = image - imageAttachment.bounds = CGRect(origin: .zero, size: CGSize(width: imageWidth, height: imageHeight)) - - let imageAttributedString = NSMutableAttributedString(attachment: imageAttachment) - imageAttributedString.addAttribute( - NSAttributedString.Key.paragraphStyle, - value: imageParagraphStyle, - range: imageAttributedString.mutableString.range(of: imageAttributedString.string) - ) - - return imageAttributedString - } +func getCenteredImagesForWikimediaAndScribe(imageString: String, imageWidth: CGFloat) + -> NSMutableAttributedString { + if let image = UIImage(named: imageString) { + let imageParagraphStyle = NSMutableParagraphStyle() + imageParagraphStyle.alignment = .center + + let imageAttachment = NSTextAttachment() + let aspectRatio = image.size.width / image.size.height + let imageHeight = imageWidth / aspectRatio + imageAttachment.image = image + imageAttachment.bounds = CGRect( + origin: .zero, size: CGSize(width: imageWidth, height: imageHeight) + ) + + let imageAttributedString = NSMutableAttributedString(attachment: imageAttachment) + imageAttributedString.addAttribute( + NSAttributedString.Key.paragraphStyle, + value: imageParagraphStyle, + range: imageAttributedString.mutableString.range(of: imageAttributedString.string) + ) + + return imageAttributedString + } - return NSMutableAttributedString(string: "") + return NSMutableAttributedString(string: "") } /// Function that deals with switching image when the device theme changes. @@ -150,104 +164,107 @@ func getCenteredImagesForWikimediaAndScribe(imageString: String, imageWidth: CGF /// /// - Parameters /// - attributedString: attributed string with the image in it as an attachment. -func switchAttachmentOnThemeChange(for attributedString: NSAttributedString) -> NSMutableAttributedString { - let mutableString = NSMutableAttributedString(attributedString: attributedString) - - mutableString.enumerateAttribute(.attachment, in: NSRange(location: 0, length: mutableString.length), options: []) { attachment, range, _ in - guard let attachment = attachment as? NSTextAttachment, - let asset = attachment.image?.imageAsset else { return } - - attachment.image = asset.image(with: .current) - - let imageParagraphStyle = NSMutableParagraphStyle() - imageParagraphStyle.alignment = .center - let imageAttributedString = NSMutableAttributedString(attachment: attachment) - imageAttributedString.addAttribute( - NSAttributedString.Key.paragraphStyle, - value: imageParagraphStyle, - range: imageAttributedString.mutableString.range(of: imageAttributedString.string) - ) - - mutableString.replaceCharacters(in: range, with: imageAttributedString) - } +func switchAttachmentOnThemeChange(for attributedString: NSAttributedString) + -> NSMutableAttributedString { + let mutableString = NSMutableAttributedString(attributedString: attributedString) + + mutableString.enumerateAttribute( + .attachment, in: NSRange(location: 0, length: mutableString.length), options: [] + ) { attachment, range, _ in + guard let attachment = attachment as? NSTextAttachment, + let asset = attachment.image?.imageAsset + else { return } + + attachment.image = asset.image(with: .current) + + let imageParagraphStyle = NSMutableParagraphStyle() + imageParagraphStyle.alignment = .center + let imageAttributedString = NSMutableAttributedString(attachment: attachment) + imageAttributedString.addAttribute( + NSAttributedString.Key.paragraphStyle, + value: imageParagraphStyle, + range: imageAttributedString.mutableString.range(of: imageAttributedString.string) + ) + + mutableString.replaceCharacters(in: range, with: imageAttributedString) + } - return mutableString + return mutableString } /// Formats and returns the text of the Scribe privacy policy with links activated. func setPrivacyPolicy(fontSize: CGFloat, text: String) -> NSMutableAttributedString { - let links = [ - "https://www.wikidata.org/wiki/Wikidata:Licensing", - "https://en.wikipedia.org/wiki/Wikipedia:Reusing_Wikipedia_content", - "https://www.unicode.org/license.txt", - "https://github.com/huggingface/transformers/blob/master/LICENSE", - "https://github.com/scribe-org", - "scribe.langauge@gmail.com", - "https://github.com/logos", - "https://foundation.wikimedia.org/wiki/Policy:Trademark_policy", - "https://github.com/EthanSK/CustomKeyboard/blob/master/LICENSE" - ] - - let privacyPolicyTextWithLinks = addHyperLinks( - originalText: text, - links: pairLinks(links), - fontSize: fontSize - ) - - return privacyPolicyTextWithLinks + let links = [ + "https://www.wikidata.org/wiki/Wikidata:Licensing", + "https://en.wikipedia.org/wiki/Wikipedia:Reusing_Wikipedia_content", + "https://www.unicode.org/license.txt", + "https://github.com/huggingface/transformers/blob/master/LICENSE", + "https://github.com/scribe-org", + "scribe.langauge@gmail.com", + "https://github.com/logos", + "https://foundation.wikimedia.org/wiki/Policy:Trademark_policy", + "https://github.com/EthanSK/CustomKeyboard/blob/master/LICENSE" + ] + + return addHyperLinks( + originalText: text, + links: pairLinks(links), + fontSize: fontSize + ) } /// Formats and returns the text of the Scribe third-party licenses with links activated and list indents. func setThirdPartyLicenses(fontSize: CGFloat, text: String) -> NSMutableAttributedString { - let links = [ - "https://github.com/EthanSK/CustomKeyboard/blob/master/LICENSE", - "https://github.com/SimpleMobileTools/Simple-Keyboard/blob/main/LICENSE" - ] - let thirdPartyLicensesTextWithLink = addHyperLinks( - originalText: text, - links: pairLinks(links), - fontSize: fontSize - ) - - return thirdPartyLicensesTextWithLink + let links = [ + "https://github.com/EthanSK/CustomKeyboard/blob/master/LICENSE", + "https://github.com/SimpleMobileTools/Simple-Keyboard/blob/main/LICENSE" + ] + return addHyperLinks( + originalText: text, + links: pairLinks(links), + fontSize: fontSize + ) } /// Formats and returns the text of the Wikimedia and Scribe screen with images. -func setWikimediaAndScribe(text: [String], fontSize: CGFloat, imageWidth: CGFloat) -> NSMutableAttributedString { - var attributedTextBySections = [NSMutableAttributedString]() +func setWikimediaAndScribe(text: [String], fontSize: CGFloat, imageWidth: CGFloat) + -> NSMutableAttributedString { + var attributedTextBySections = [NSMutableAttributedString]() + + for section in text { + let attributedSection = NSMutableAttributedString( + string: section, + attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: fontSize)] + ) + attributedTextBySections.append(attributedSection) + } - for section in text { - let attributedSection = NSMutableAttributedString( - string: section, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: fontSize)] + let wikidataLogoString = getCenteredImagesForWikimediaAndScribe( + imageString: "wikidata.logo", imageWidth: imageWidth ) - attributedTextBySections.append(attributedSection) - } - let wikidataLogoString = getCenteredImagesForWikimediaAndScribe( - imageString: "wikidata.logo", imageWidth: imageWidth - ) - - let wikipediaLogoString = getCenteredImagesForWikimediaAndScribe( - imageString: "wikipedia.logo", imageWidth: imageWidth - ) + let wikipediaLogoString = getCenteredImagesForWikimediaAndScribe( + imageString: "wikipedia.logo", imageWidth: imageWidth + ) - let wikimediaAndScribeTextWithImages = attributedTextBySections[0] - wikimediaAndScribeTextWithImages.append(wikidataLogoString) - wikimediaAndScribeTextWithImages.append(attributedTextBySections[1]) - wikimediaAndScribeTextWithImages.append(wikipediaLogoString) - wikimediaAndScribeTextWithImages.append(attributedTextBySections[2]) + let wikimediaAndScribeTextWithImages = attributedTextBySections[0] + wikimediaAndScribeTextWithImages.append(wikidataLogoString) + wikimediaAndScribeTextWithImages.append(attributedTextBySections[1]) + wikimediaAndScribeTextWithImages.append(wikipediaLogoString) + wikimediaAndScribeTextWithImages.append(attributedTextBySections[2]) - return wikimediaAndScribeTextWithImages + return wikimediaAndScribeTextWithImages } func pairLinks(_ linkList: [String]) -> [String: String] { - var pairedLinks: [String: String] = [:] - for hyperlink in linkList { - if hyperlink.contains("@") && hyperlink.range(of: #"https?://"#, options: .regularExpression) == nil { - pairedLinks[hyperlink] = "mailto:\(hyperlink)" - } else { - pairedLinks[hyperlink] = hyperlink + var pairedLinks: [String: String] = [:] + for hyperlink in linkList { + if hyperlink.contains("@"), + hyperlink.range(of: #"https?://"#, options: .regularExpression) == nil { + pairedLinks[hyperlink] = "mailto:\(hyperlink)" + } else { + pairedLinks[hyperlink] = hyperlink + } } - } - return pairedLinks + return pairedLinks } diff --git a/Scribe/AppTexts/InstallScreen.swift b/Scribe/AppTexts/InstallScreen.swift index 36fb60dc..4206e246 100644 --- a/Scribe/AppTexts/InstallScreen.swift +++ b/Scribe/AppTexts/InstallScreen.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * The app text for the Scribe app's keyboard installation screen. */ @@ -13,33 +13,75 @@ let fourthLineNumber = preferredLanguage.prefix(2) == "ar" ? "\n\n٤. " : "\n\n4 /// Formats and returns the directions of the installation guidelines. func getInstallationDirections(fontSize: CGFloat) -> NSMutableAttributedString { - let globeString = getGlobeIcon(fontSize: fontSize) + let globeString = getGlobeIcon(fontSize: fontSize) - let startOfBody = NSMutableAttributedString(string: firstLineNumber, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: fontSize)]) + let startOfBody = NSMutableAttributedString( + string: firstLineNumber, + attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: fontSize)] + ) - var settingsLink = NSMutableAttributedString() - let linkText = NSLocalizedString("i18n.app.installation.keyboard.scribe_settings", value: "Open Scribe settings", comment: "") - settingsLink = addHyperLinks( - originalText: linkText, - links: [linkText: "MakeTextLink"], // placeholder as there's a button over it - fontSize: fontSize - ) + var settingsLink = NSMutableAttributedString() + let linkText = NSLocalizedString( + "i18n.app.installation.keyboard.scribe_settings", value: "Open Scribe settings", comment: "" + ) + settingsLink = addHyperLinks( + originalText: linkText, + links: [linkText: "MakeTextLink"], // placeholder as there's a button over it + fontSize: fontSize + ) - let installStart = concatAttributedStrings(left: startOfBody, right: settingsLink) + let installStart = concatAttributedStrings(left: startOfBody, right: settingsLink) - let installDirections = NSMutableAttributedString(string: secondLineNumber + NSLocalizedString("i18n.app.installation.keyboard.text_1", value: "Select", comment: "") + " ", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: fontSize)]) + let installDirections = NSMutableAttributedString( + string: secondLineNumber + + NSLocalizedString( + "i18n.app.installation.keyboard.text_1", value: "Select", comment: "" + ) + + " ", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: fontSize)] + ) - let boldText = NSMutableAttributedString(string: NSLocalizedString("i18n.app.installation.keyboard.keyboards_bold", value: "Keyboards", comment: ""), attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: fontSize)]) - boldText.addAttribute(NSAttributedString.Key.font, value: UIFont.boldSystemFont(ofSize: fontSize), range: NSRange(location: 0, length: boldText.length)) - installDirections.append(boldText) + let boldText = NSMutableAttributedString( + string: NSLocalizedString( + "i18n.app.installation.keyboard.keyboards_bold", value: "Keyboards", comment: "" + ), + attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: fontSize)] + ) + boldText.addAttribute( + NSAttributedString.Key.font, value: UIFont.boldSystemFont(ofSize: fontSize), + range: NSRange(location: 0, length: boldText.length) + ) + installDirections.append(boldText) - installDirections.append(NSMutableAttributedString(string: thirdLineNumber + NSLocalizedString("i18n.app.installation.keyboard.text_2", value: "Activate keyboards that you want to use", comment: "") + fourthLineNumber + NSLocalizedString("i18n.app.installation.keyboard.text_3", value: "When typing, press", comment: "") + " ", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: fontSize)])) + installDirections.append( + NSMutableAttributedString( + string: thirdLineNumber + + NSLocalizedString( + "i18n.app.installation.keyboard.text_2", + value: "Activate keyboards that you want to use", + comment: "" + ) + fourthLineNumber + + NSLocalizedString( + "i18n.app.installation.keyboard.text_3", value: "When typing, press", + comment: "" + ) + " ", + attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: fontSize)] + ) + ) - installDirections.append(globeString) + installDirections.append(globeString) - installDirections.append(NSMutableAttributedString(string: " " + NSLocalizedString("i18n.app.installation.keyboard.text_4", value: "to select keyboards", comment: ""), attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: fontSize)])) + installDirections.append( + NSMutableAttributedString( + string: " " + + NSLocalizedString( + "i18n.app.installation.keyboard.text_4", value: "to select keyboards", + comment: "" + ), + attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: fontSize)] + ) + ) - return concatAttributedStrings(left: installStart, right: installDirections) + return concatAttributedStrings(left: installStart, right: installDirections) } /// Formats and returns the full text for the installation guidelines. @@ -47,5 +89,5 @@ func getInstallationDirections(fontSize: CGFloat) -> NSMutableAttributedString { /// - Parameters /// - fontSize: the size of the font derived for the app text given screen dimensions. func setInstallation(fontSize: CGFloat) -> NSMutableAttributedString { - return getInstallationDirections(fontSize: fontSize) + return getInstallationDirections(fontSize: fontSize) } diff --git a/Scribe/AppTexts/ThirdPartyLicense.swift b/Scribe/AppTexts/ThirdPartyLicense.swift index 74594424..7f725732 100644 --- a/Scribe/AppTexts/ThirdPartyLicense.swift +++ b/Scribe/AppTexts/ThirdPartyLicense.swift @@ -1,33 +1,48 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Text displayed on the Third Party Licenses view. */ import SwiftUI -let thirdPartyLicensesTitle = NSLocalizedString("i18n.app.about.legal.third_party", - value: "Third-party licenses", - comment: "") -let thirdPartyLicensesCaption = NSLocalizedString("i18n.app.about.legal.third_party.caption", - value: "Whose code we used", - comment: "") +let thirdPartyLicensesTitle = NSLocalizedString( + "i18n.app.about.legal.third_party", + value: "Third-party licenses", + comment: "" +) +let thirdPartyLicensesCaption = NSLocalizedString( + "i18n.app.about.legal.third_party.caption", + value: "Whose code we used", + comment: "" +) -let thirdPartyLicensesText = NSLocalizedString("i18n.app.about.legal.third_party.text", - value: """ -The Scribe developers (SCRIBE) built the iOS application "Scribe - Language Keyboards" (SERVICE) using third party code. All source code used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. This section lists the source code on which the SERVICE was based as well as the coinciding licenses of each. +let thirdPartyLicensesText = + NSLocalizedString( + "i18n.app.about.legal.third_party.text", + value: """ + The Scribe developers (SCRIBE) built the iOS application "Scribe - Language Keyboards" (SERVICE) using third party code. All source code used in the creation of this SERVICE comes from sources that allow its full use in the manner done so by the SERVICE. This section lists the source code on which the SERVICE was based as well as the coinciding licenses of each. -The following is a list of all used source code, the main author or authors of the code, the license under which it was released at time of usage, and a link to the license. -""", - comment: "") + "\n\n1. " + NSLocalizedString("i18n.app.about.legal.third_party.entry_custom_keyboard", value: """ -Custom Keyboard -• Author: EthanSK -• License: MIT -• Link: https://github.com/EthanSK/CustomKeyboard/blob/master/LICENSE -""", - comment: "") + "\n\n2. " + NSLocalizedString("i18n.app.about.legal.third_party.entry_simple_keyboard", value: """ -Simple Keyboard -• Author: Simple Mobile Tools -• License: GPL-3.0 -• Link: https://github.com/SimpleMobileTools/Simple-Keyboard/blob/main/LICENSE -""", comment: "") + The following is a list of all used source code, the main author or authors of the code, the license under which it was released at time of usage, and a link to the license. + """, + comment: "" + ) + "\n\n1. " + + NSLocalizedString( + "i18n.app.about.legal.third_party.entry_custom_keyboard", + value: """ + Custom Keyboard + • Author: EthanSK + • License: MIT + • Link: https://github.com/EthanSK/CustomKeyboard/blob/master/LICENSE + """, + comment: "" + ) + "\n\n2. " + + NSLocalizedString( + "i18n.app.about.legal.third_party.entry_simple_keyboard", + value: """ + Simple Keyboard + • Author: Simple Mobile Tools + • License: GPL-3.0 + • Link: https://github.com/SimpleMobileTools/Simple-Keyboard/blob/main/LICENSE + """, comment: "" + ) diff --git a/Scribe/AppTexts/WikimediaAndScribe.swift b/Scribe/AppTexts/WikimediaAndScribe.swift index 7df3d4d3..f590ef76 100644 --- a/Scribe/AppTexts/WikimediaAndScribe.swift +++ b/Scribe/AppTexts/WikimediaAndScribe.swift @@ -1,16 +1,42 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Text displayed on the English Wikimedia and Scribe view. */ import SwiftUI -let wikimediaAndScribeTitle = NSLocalizedString("i18n.app.about.community.wikimedia", value: "Wikimedia and Scribe", comment: "") -let wikimediaAndScribeCaption = NSLocalizedString("i18n.app.about.community.wikimedia.caption", value: "How we work together", comment: "") +let wikimediaAndScribeTitle = NSLocalizedString( + "i18n.app.about.community.wikimedia", value: "Wikimedia and Scribe", comment: "" +) +let wikimediaAndScribeCaption = NSLocalizedString( + "i18n.app.about.community.wikimedia.caption", value: "How we work together", comment: "" +) -let wikimediaAndScribeText1 = NSLocalizedString("i18n.app.about.community.wikimedia.text_1", value: "Scribe would not be possible without countless contributions by Wikimedia contributors to the many projects that they support. Specifically Scribe makes use of data from the Wikidata Lexicographical data community, as well as data from Wikipedia for each language that Scribe supports.", comment: "") + "\n\n" -let wikimediaAndScribeText2 = "\n\n" + NSLocalizedString("i18n.app.about.community.wikimedia.text_2", value: "Wikidata is a collaboratively edited multilingual knowledge graph hosted by the Wikimedia Foundation. It provides freely available data that anyone can use under a Creative Commons Public Domain license (CC0). Scribe uses language data from Wikidata to provide users with verb conjugations, noun-form annotations, noun plurals, and many other features.", comment: "") + "\n\n" -let wikimediaAndScribeText3 = "\n\n" + NSLocalizedString("i18n.app.about.community.wikimedia.text_3", value: "Wikipedia is a multilingual free online encyclopedia written and maintained by a community of volunteers through open collaboration and a wiki-based editing system. Scribe uses data from Wikipedia to produce autosuggestions by deriving the most common words in a language as well as the most common words that follow them.", comment: "") + "\n" +let wikimediaAndScribeText1 = + NSLocalizedString( + "i18n.app.about.community.wikimedia.text_1", + value: + "Scribe would not be possible without countless contributions by Wikimedia contributors to the many projects that they support. Specifically Scribe makes use of data from the Wikidata Lexicographical data community, as well as data from Wikipedia for each language that Scribe supports.", + comment: "" + ) + "\n\n" +let wikimediaAndScribeText2 = + "\n\n" + + NSLocalizedString( + "i18n.app.about.community.wikimedia.text_2", + value: + "Wikidata is a collaboratively edited multilingual knowledge graph hosted by the Wikimedia Foundation. It provides freely available data that anyone can use under a Creative Commons Public Domain license (CC0). Scribe uses language data from Wikidata to provide users with verb conjugations, noun-form annotations, noun plurals, and many other features.", + comment: "" + ) + "\n\n" +let wikimediaAndScribeText3 = + "\n\n" + + NSLocalizedString( + "i18n.app.about.community.wikimedia.text_3", + value: + "Wikipedia is a multilingual free online encyclopedia written and maintained by a community of volunteers through open collaboration and a wiki-based editing system. Scribe uses data from Wikipedia to produce autosuggestions by deriving the most common words in a language as well as the most common words that follow them.", + comment: "" + ) + "\n" -let wikimediaAndScribeText = [wikimediaAndScribeText1, wikimediaAndScribeText2, wikimediaAndScribeText3] +let wikimediaAndScribeText = [ + wikimediaAndScribeText1, wikimediaAndScribeText2, wikimediaAndScribeText3 +] diff --git a/Scribe/AppUISymbols.swift b/Scribe/AppUISymbols.swift index bd921701..8c89ee1b 100644 --- a/Scribe/AppUISymbols.swift +++ b/Scribe/AppUISymbols.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Functions returning symbols for the app UI. */ @@ -11,25 +11,29 @@ import UIKit /// - Parameters /// - fontSize: the size of the font derived for the app text given screen dimensions. func getSettingsSymbol(fontSize: CGFloat) -> UIImage { - var settingsSymbolConfig = UIImage.SymbolConfiguration( - pointSize: fontSize * 0.2, weight: .medium, scale: .medium - ) - if DeviceType.isPad { - if UIScreen.main.bounds.height < UIScreen.main.bounds.width { - settingsSymbolConfig = UIImage.SymbolConfiguration( - pointSize: fontSize * 0.05, weight: .medium, scale: .medium - ) - } else { - settingsSymbolConfig = UIImage.SymbolConfiguration( - pointSize: fontSize * 0.15, weight: .medium, scale: .medium - ) + var settingsSymbolConfig = UIImage.SymbolConfiguration( + pointSize: fontSize * 0.2, weight: .medium, scale: .medium + ) + if DeviceType.isPad { + if UIScreen.main.bounds.height < UIScreen.main.bounds.width { + settingsSymbolConfig = UIImage.SymbolConfiguration( + pointSize: fontSize * 0.05, weight: .medium, scale: .medium + ) + } else { + settingsSymbolConfig = UIImage.SymbolConfiguration( + pointSize: fontSize * 0.15, weight: .medium, scale: .medium + ) + } + } + guard + let settingsSymbol = UIImage( + systemName: "gearshape", withConfiguration: settingsSymbolConfig + ) + else { + fatalError("Failed to create settings symbol image.") } - } - guard let settingsSymbol = UIImage(systemName: "gearshape", withConfiguration: settingsSymbolConfig) else { - fatalError("Failed to create settings symbol image.") - } - return settingsSymbol + return settingsSymbol } /// Formats and returns the privacy symbol for the app UI. @@ -37,50 +41,55 @@ func getSettingsSymbol(fontSize: CGFloat) -> UIImage { /// - Parameters /// - fontSize: the size of the font derived for the app text given screen dimensions. func getPrivacySymbol(fontSize: CGFloat) -> UIImage { - var privacySymbolConfig = UIImage.SymbolConfiguration( - pointSize: fontSize * 0.25, weight: .medium, scale: .medium - ) - if DeviceType.isPad { - if UIScreen.main.bounds.height < UIScreen.main.bounds.width { - privacySymbolConfig = UIImage.SymbolConfiguration( - pointSize: fontSize * 0.15, weight: .medium, scale: .medium - ) - } else { - privacySymbolConfig = UIImage.SymbolConfiguration( - pointSize: fontSize * 0.2, weight: .medium, scale: .medium - ) + var privacySymbolConfig = UIImage.SymbolConfiguration( + pointSize: fontSize * 0.25, weight: .medium, scale: .medium + ) + if DeviceType.isPad { + if UIScreen.main.bounds.height < UIScreen.main.bounds.width { + privacySymbolConfig = UIImage.SymbolConfiguration( + pointSize: fontSize * 0.15, weight: .medium, scale: .medium + ) + } else { + privacySymbolConfig = UIImage.SymbolConfiguration( + pointSize: fontSize * 0.2, weight: .medium, scale: .medium + ) + } + } + guard + let privacySymbol = UIImage( + systemName: "lock.shield", withConfiguration: privacySymbolConfig + ) + else { + fatalError("Failed to create privacy symbol image.") } - } - guard let privacySymbol = UIImage( - systemName: "lock.shield", withConfiguration: privacySymbolConfig - ) else { - fatalError("Failed to create privacy symbol image.") - } - return privacySymbol + return privacySymbol } func getRequiredIconForMenu(fontSize: CGFloat, imageName: String) -> UIImage { - if let image = UIImage(named: imageName) { - return image - } else { - var iconSymbolConfig = UIImage.SymbolConfiguration( - pointSize: fontSize * 0.25, weight: .medium, scale: .medium - ) - if DeviceType.isPad { - if UIScreen.main.bounds.height < UIScreen.main.bounds.width { - iconSymbolConfig = UIImage.SymbolConfiguration( - pointSize: fontSize * 0.15, weight: .medium, scale: .medium - ) - } else { - iconSymbolConfig = UIImage.SymbolConfiguration( - pointSize: fontSize * 0.2, weight: .medium, scale: .medium + if let image = UIImage(named: imageName) { + return image + } else { + var iconSymbolConfig = UIImage.SymbolConfiguration( + pointSize: fontSize * 0.25, weight: .medium, scale: .medium ) - } - } + if DeviceType.isPad { + if UIScreen.main.bounds.height < UIScreen.main.bounds.width { + iconSymbolConfig = UIImage.SymbolConfiguration( + pointSize: fontSize * 0.15, weight: .medium, scale: .medium + ) + } else { + iconSymbolConfig = UIImage.SymbolConfiguration( + pointSize: fontSize * 0.2, weight: .medium, scale: .medium + ) + } + } - guard let image = UIImage(systemName: imageName, withConfiguration: iconSymbolConfig) else { return UIImage() } + guard let image = UIImage(systemName: imageName, withConfiguration: iconSymbolConfig) + else { + return UIImage() + } - return image - } + return image + } } diff --git a/Scribe/Button/CTAButton.swift b/Scribe/Button/CTAButton.swift index 5df92f7d..65b3aaab 100644 --- a/Scribe/Button/CTAButton.swift +++ b/Scribe/Button/CTAButton.swift @@ -1,15 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Call to Action Button. */ import SwiftUI struct CTAButton: View { - @AppStorage("increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")) + @AppStorage( + "increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + ) var increaseTextSize: Bool = false - var textSizeMultiplier: CGFloat { increaseTextSize ? 1.25 : 1.0 } + var textSizeMultiplier: CGFloat { + increaseTextSize ? 1.25 : 1.0 + } let title: String let action: () -> Void @@ -26,7 +30,9 @@ struct CTAButton: View { .cornerRadius(12) .overlay( RoundedRectangle(cornerRadius: 12) - .stroke(colorScheme == .dark ? Color("scribeCTA") : Color.clear, lineWidth: 1) + .stroke( + colorScheme == .dark ? Color("scribeCTA") : Color.clear, lineWidth: 1 + ) ) .shadow( color: Color(red: 0.247, green: 0.247, blue: 0.275, opacity: 0.25), diff --git a/Scribe/Button/DownloadButton.swift b/Scribe/Button/DownloadButton.swift index e37cf7b5..e80c63dc 100644 --- a/Scribe/Button/DownloadButton.swift +++ b/Scribe/Button/DownloadButton.swift @@ -1,110 +1,114 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Download Button for making calls for updated data for each language. */ import SwiftUI enum ButtonState { - case ready - case downloading - case updated - case update + case ready + case downloading + case updated + case update - var config: ButtonConfig { - switch self { - case .ready: - return ButtonConfig( - text: NSLocalizedString( - "i18n.app._global.download_data", - value: "Download data", - comment: "" - ), - icon: "icloud.and.arrow.down", - backgroundColor: Color("buttonOrange"), - foregroundColor: Color("lightTextDarkCTA") - ) - case .downloading: - return ButtonConfig( - text: NSLocalizedString( - "i18n.app.download.menu_ui.download_data.downloading", - value: "Downloading", - comment: "" - ), - icon: "arrow.clockwise.circle.fill", - backgroundColor: Color("buttonOrange"), - foregroundColor: Color("lightTextDarkCTA") - ) - case .updated: - return ButtonConfig( - text: NSLocalizedString( - "i18n.app.download.menu_ui.download_data.up_to_date", - value: "Up to date", - comment: "" - ), - icon: "checkmark.circle.fill", - backgroundColor: Color("buttonGreen"), - foregroundColor: Color("lightTextDarkGreen") - ) - case .update: - return ButtonConfig( - text: NSLocalizedString( - "i18n.app.download.menu_ui.update_data", - value: "Update data", - comment: "" - ), - icon: "icloud.and.arrow.down", - backgroundColor: Color("buttonOrange"), - foregroundColor: Color("lightTextDarkCTA") - ) + var config: ButtonConfig { + switch self { + case .ready: + return ButtonConfig( + text: NSLocalizedString( + "i18n.app._global.download_data", + value: "Download data", + comment: "" + ), + icon: "icloud.and.arrow.down", + backgroundColor: Color("buttonOrange"), + foregroundColor: Color("lightTextDarkCTA") + ) + case .downloading: + return ButtonConfig( + text: NSLocalizedString( + "i18n.app.download.menu_ui.download_data.downloading", + value: "Downloading", + comment: "" + ), + icon: "arrow.clockwise.circle.fill", + backgroundColor: Color("buttonOrange"), + foregroundColor: Color("lightTextDarkCTA") + ) + case .updated: + return ButtonConfig( + text: NSLocalizedString( + "i18n.app.download.menu_ui.download_data.up_to_date", + value: "Up to date", + comment: "" + ), + icon: "checkmark.circle.fill", + backgroundColor: Color("buttonGreen"), + foregroundColor: Color("lightTextDarkGreen") + ) + case .update: + return ButtonConfig( + text: NSLocalizedString( + "i18n.app.download.menu_ui.update_data", + value: "Update data", + comment: "" + ), + icon: "icloud.and.arrow.down", + backgroundColor: Color("buttonOrange"), + foregroundColor: Color("lightTextDarkCTA") + ) + } } - } } struct ButtonConfig { - let text: String - let icon: String - let backgroundColor: Color - let foregroundColor: Color + let text: String + let icon: String + let backgroundColor: Color + let foregroundColor: Color } struct DownloadButton: View { - @AppStorage("increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")) - var increaseTextSize: Bool = false - var textSizeMultiplier: CGFloat { increaseTextSize ? 1.25 : 1.0 } + @AppStorage( + "increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + ) + var increaseTextSize: Bool = false + var textSizeMultiplier: CGFloat { + increaseTextSize ? 1.25 : 1.0 + } - let state: ButtonState - let action: () -> Void - @Environment(\.colorScheme) var colorScheme + let state: ButtonState + let action: () -> Void + @Environment(\.colorScheme) var colorScheme - var body: some View { - Button(action: action) { - HStack(spacing: 8) { - Text(state.config.text) - if state == .downloading { - ProgressView() - .tint(state.config.foregroundColor) - .scaleEffect(0.8) - } else { - Image(systemName: state.config.icon) + var body: some View { + Button(action: action) { + HStack(spacing: 8) { + Text(state.config.text) + if state == .downloading { + ProgressView() + .tint(state.config.foregroundColor) + .scaleEffect(0.8) + } else { + Image(systemName: state.config.icon) + } + } + .font(.system(size: 12 * textSizeMultiplier, weight: .semibold)) + .foregroundColor(state.config.foregroundColor) + .frame(width: 120, height: 20) + .padding(.vertical, 6) + .padding(.horizontal, 10) + .background(state.config.backgroundColor) + .cornerRadius(6) + .overlay( + RoundedRectangle(cornerRadius: 6) + .stroke( + colorScheme == .dark ? state.config.foregroundColor : Color.clear, + lineWidth: 1 + ) + ) } - } - .font(.system(size: 12 * textSizeMultiplier, weight: .semibold)) - .foregroundColor(state.config.foregroundColor) - .frame(width: 120, height: 20) - .padding(.vertical, 6) - .padding(.horizontal, 10) - .background(state.config.backgroundColor) - .cornerRadius(6) - .overlay( - RoundedRectangle(cornerRadius: 6) - .stroke( - colorScheme == .dark ? state.config.foregroundColor : Color.clear, - lineWidth: 1 - ) - ) + .animation(.easeInOut(duration: 0.2), value: state) } - .animation(.easeInOut(duration: 0.2), value: state) - } } diff --git a/Scribe/Colors/ColorVariables.swift b/Scribe/Colors/ColorVariables.swift index 1dfdfa77..0962c681 100644 --- a/Scribe/Colors/ColorVariables.swift +++ b/Scribe/Colors/ColorVariables.swift @@ -1,12 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Variables associated with coloration for Scribe keyboards. */ import UIKit -// The Scribe key icon that changes based on light and dark mode as well as device. +/// The Scribe key icon that changes based on light and dark mode as well as device. var scribeKeyIcon = UIImage(named: "scribeKeyIcon") // Initialize all colors. diff --git a/Scribe/Colors/ScribeColor.swift b/Scribe/Colors/ScribeColor.swift index 90bf5c94..24197091 100644 --- a/Scribe/Colors/ScribeColor.swift +++ b/Scribe/Colors/ScribeColor.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Converts strings for colors into the corresponding color. */ @@ -8,32 +8,32 @@ import UIKit /// All the colors defined in `Assets.xcassets/Colors`. enum ScribeColor: String { - case annotateRed - case annotateBlue - case annotatePurple - case annotateGreen - case annotateOrange - case annotateTitle - case appBtn - case commandBar - case commandBarPlaceholder - case commandKey - case key - case keyboardBackground - case keyChar - case keyPressed - case keyShadow - case keySpecial - case lightTextDarkCTA - case lightWhiteDarkBlack - case linkBlue - case menuOption - case scribeAppBackground - case scribeBlue - case scribeCTA + case annotateRed + case annotateBlue + case annotatePurple + case annotateGreen + case annotateOrange + case annotateTitle + case appBtn + case commandBar + case commandBarPlaceholder + case commandKey + case key + case keyboardBackground + case keyChar + case keyPressed + case keyShadow + case keySpecial + case lightTextDarkCTA + case lightWhiteDarkBlack + case linkBlue + case menuOption + case scribeAppBackground + case scribeBlue + case scribeCTA - /// `UIColor` object for the given - var color: UIColor { - .init(self) - } + /// `UIColor` object for the given + var color: UIColor { + .init(self) + } } diff --git a/Scribe/Colors/UIColor+ScribeColors.swift b/Scribe/Colors/UIColor+ScribeColors.swift index 81e5dca5..db3ce524 100644 --- a/Scribe/Colors/UIColor+ScribeColors.swift +++ b/Scribe/Colors/UIColor+ScribeColors.swift @@ -1,34 +1,34 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Adds Scribe colors to the UIColor pool. */ import UIKit extension UIColor { - // MARK: Init from ScribeColor + // MARK: Init from ScribeColor - /// Creates `UIColor` from passed `ScribeColor` - /// - Parameter color: The `UIColor`. - /// - /// Defaults to `UIColor.red` if passed color was not found in assets. - convenience init(_ color: ScribeColor) { - if UIColor(named: color.rawValue) != nil { - self.init(named: color.rawValue)! - } else { - print("Unable to find color named: \(color.rawValue)") - self.init(red: 1, green: 0, blue: 0, alpha: 1) + /// Creates `UIColor` from passed `ScribeColor` + /// - Parameter color: The `UIColor`. + /// + /// Defaults to `UIColor.red` if passed color was not found in assets. + convenience init(_ color: ScribeColor) { + if UIColor(named: color.rawValue) != nil { + self.init(named: color.rawValue)! + } else { + print("Unable to find color named: \(color.rawValue)") + self.init(red: 1, green: 0, blue: 0, alpha: 1) + } } - } - /// Convenience computed property for light mode variant of the color. - var light: UIColor { - resolvedColor(with: .init(userInterfaceStyle: .light)) - } + /// Convenience computed property for light mode variant of the color. + var light: UIColor { + resolvedColor(with: .init(userInterfaceStyle: .light)) + } - /// Convenience computed property for dark mode variant of the color. - var dark: UIColor { - resolvedColor(with: .init(userInterfaceStyle: .light)) - } + /// Convenience computed property for dark mode variant of the color. + var dark: UIColor { + resolvedColor(with: .init(userInterfaceStyle: .light)) + } } diff --git a/Scribe/ConfirmDialog/ConfirmDialogView.swift b/Scribe/ConfirmDialog/ConfirmDialogView.swift index ff023170..db904c21 100644 --- a/Scribe/ConfirmDialog/ConfirmDialogView.swift +++ b/Scribe/ConfirmDialog/ConfirmDialogView.swift @@ -1,115 +1,135 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Confirmation Dialog tooltip. */ import SwiftUI struct ConfirmDialogView: View { - @AppStorage("increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")) - var increaseTextSize: Bool = false - var textSizeMultiplier: CGFloat { increaseTextSize ? 1.25 : 1.0 } + @AppStorage( + "increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + ) + var increaseTextSize: Bool = false + var textSizeMultiplier: CGFloat { + increaseTextSize ? 1.25 : 1.0 + } - private let cardCornerRadius: CGFloat = 10 - private let iconSize: CGFloat = 30.0 - private let cardPadding: CGFloat = 16 - private let externalPadding: CGFloat = 5 - private let shadowColor = Color(red: 63/255, green: 63/255, blue: 70/255).opacity(0.25) + private let cardCornerRadius: CGFloat = 10 + private let iconSize: CGFloat = 30.0 + private let cardPadding: CGFloat = 16 + private let externalPadding: CGFloat = 5 + private let shadowColor = Color(red: 63 / 255, green: 63 / 255, blue: 70 / 255).opacity(0.25) - var infoText: String - var changeButtonText: String - var confirmButtonText: String - var onDismiss: () -> Void - var onChange: () -> Void - var onConfirm: () -> Void + var infoText: String + var changeButtonText: String + var confirmButtonText: String + var onDismiss: () -> Void + var onChange: () -> Void + var onConfirm: () -> Void - var body: some View { - ZStack { - Color.clear - .contentShape(Rectangle()) - .onTapGesture { - onDismiss() - } - VStack(spacing: 10) { - HStack(alignment: .center) { - Image(systemName: "info.circle") - .resizable() - .frame(width: iconSize, height: iconSize) - .foregroundColor(Color.scribeCTA) + var body: some View { + ZStack { + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + onDismiss() + } + VStack(spacing: 10) { + HStack(alignment: .center) { + Image(systemName: "info.circle") + .resizable() + .frame(width: iconSize, height: iconSize) + .foregroundColor(Color.scribeCTA) - Text(infoText) - .font(.system(size: DeviceType.isPad ? 22 * textSizeMultiplier : 17 * textSizeMultiplier)) - .fixedSize(horizontal: false, vertical: true) - } + Text(infoText) + .font( + .system( + size: DeviceType.isPad + ? 22 * textSizeMultiplier : 17 * textSizeMultiplier + ) + ) + .fixedSize(horizontal: false, vertical: true) + } - HStack { - Spacer() - Button( - action: onChange, - label: { - Text(changeButtonText) - .font(.system(size: DeviceType.isPad ? 22 * textSizeMultiplier : 17 * textSizeMultiplier)) - .foregroundColor(Color.keyChar) - }) - .buttonStyle(.borderedProminent) - .tint(Color.keySpecial) - .shadow( - color: shadowColor, - radius: 1, - x: 0, - y: 3 - ) - Button( - action: onConfirm, - label: { - Text(confirmButtonText) - .font(.system(size: DeviceType.isPad ? 22 : 0)) - .font(.system(size: DeviceType.isPad ? 22 * textSizeMultiplier : 17 * textSizeMultiplier)) - .foregroundColor(Color.keyChar) - }) - .buttonStyle(.borderedProminent) - .tint(Color.scribeBlue) - .shadow( - color: shadowColor, - radius: 3, - x: 0, - y: 3 + HStack { + Spacer() + Button( + action: onChange, + label: { + Text(changeButtonText) + .font( + .system( + size: DeviceType.isPad + ? 22 * textSizeMultiplier : 17 * textSizeMultiplier + ) + ) + .foregroundColor(Color.keyChar) + } + ) + .buttonStyle(.borderedProminent) + .tint(Color.keySpecial) + .shadow( + color: shadowColor, + radius: 1, + x: 0, + y: 3 + ) + Button( + action: onConfirm, + label: { + Text(confirmButtonText) + .font(.system(size: DeviceType.isPad ? 22 : 0)) + .font( + .system( + size: DeviceType.isPad + ? 22 * textSizeMultiplier : 17 * textSizeMultiplier + ) + ) + .foregroundColor(Color.keyChar) + } + ) + .buttonStyle(.borderedProminent) + .tint(Color.scribeBlue) + .shadow( + color: shadowColor, + radius: 3, + x: 0, + y: 3 + ) + } + } + .padding(cardPadding) + .background( + RoundedRectangle(cornerRadius: cardCornerRadius) + .fill(Color.lightWhiteDarkBlack) + .shadow( + color: shadowColor, + radius: 3, + x: 0, + y: 4 + ) ) + .padding(externalPadding) } - } - .padding(cardPadding) - .background( - RoundedRectangle(cornerRadius: cardCornerRadius) - .fill(Color.lightWhiteDarkBlack) - .shadow( - color: shadowColor, - radius: 3, - x: 0, - y: 4 - ) - ) - .padding(externalPadding) - } - } } struct ConfirmTranslationSource: View { - var infoText: String - var changeButtonText: String - var confirmButtonText: String - var onDismiss: () -> Void - var onChange: () -> Void - var onConfirm: () -> Void - var body: some View { - ConfirmDialogView( - infoText: infoText, - changeButtonText: changeButtonText, - confirmButtonText: confirmButtonText, - onDismiss: onDismiss, - onChange: onChange, - onConfirm: onConfirm - ) - } + var infoText: String + var changeButtonText: String + var confirmButtonText: String + var onDismiss: () -> Void + var onChange: () -> Void + var onConfirm: () -> Void + var body: some View { + ConfirmDialogView( + infoText: infoText, + changeButtonText: changeButtonText, + confirmButtonText: confirmButtonText, + onDismiss: onDismiss, + onChange: onChange, + onConfirm: onConfirm + ) + } } diff --git a/Scribe/Extensions/UIDeviceExtensions.swift b/Scribe/Extensions/UIDeviceExtensions.swift index fadd6e24..a8d2c50f 100644 --- a/Scribe/Extensions/UIDeviceExtensions.swift +++ b/Scribe/Extensions/UIDeviceExtensions.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Checks if the device has a home button or not via safe area checks. */ @@ -8,18 +8,20 @@ import UIKit extension UIDevice { public static var hasNotch: Bool { - guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { - return false - } - if windowScene.windows.count == 0 { return false } - let top = scene.windows.first?.safeAreaInsets.top ?? 0 - return top > 24 + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene + else { + return false + } + if windowScene.windows.count == 0 { return false } + let top = scene.windows.first?.safeAreaInsets.top ?? 0 + return top > 24 } private static var scene: UIWindowScene { - guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { - fatalError("No connected scenes.") - } - return windowScene + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene + else { + fatalError("No connected scenes.") + } + return windowScene } } diff --git a/Scribe/Extensions/UIEdgeInsetsExtensions.swift b/Scribe/Extensions/UIEdgeInsetsExtensions.swift index 2e6d68d0..229dffda 100644 --- a/Scribe/Extensions/UIEdgeInsetsExtensions.swift +++ b/Scribe/Extensions/UIEdgeInsetsExtensions.swift @@ -1,16 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Extensions for UIEdgeInsets. */ import UIKit extension UIEdgeInsets { + // MARK: Initialization - // MARK: Initialisation - - init(vertical: CGFloat, horizontal: CGFloat) { - self.init(top: vertical, left: horizontal, bottom: vertical, right: horizontal) - } + init(vertical: CGFloat, horizontal: CGFloat) { + self.init(top: vertical, left: horizontal, bottom: vertical, right: horizontal) + } } diff --git a/Scribe/InstallationTab/DownloadDataScreen.swift b/Scribe/InstallationTab/DownloadDataScreen.swift index c1b5b3c1..ae0aae35 100644 --- a/Scribe/InstallationTab/DownloadDataScreen.swift +++ b/Scribe/InstallationTab/DownloadDataScreen.swift @@ -2,414 +2,445 @@ import SwiftUI -/// Download data UI for getting new data for keyboards. +// Download data UI for getting new data for keyboards. enum CheckDataState { - case idle - case checking - case checked + case idle + case checking + case checked } struct CheckDataSpinner: View { - @Binding var state: CheckDataState - @State private var rotation: Double = 0 - @State private var spinnerTimer: Timer? - - var body: some View { - ZStack { - switch state { - case .idle: - // Empty gray circle - Circle() - .stroke(Color.gray, lineWidth: 2) - .frame(width: 28, height: 28) - .contentShape(Circle()) - .onTapGesture { startChecking() } - - case .checking: - // Rotating arc (orange) with X cancel - ZStack { - Circle() - .stroke(Color.gray.opacity(0.3), lineWidth: 2.5) - .frame(width: 28, height: 28) - - Circle() - .trim(from: 0, to: 0.7) - .stroke(Color("scribeCTA"), style: StrokeStyle(lineWidth: 2.5, lineCap: .round)) - .frame(width: 28, height: 28) - .rotationEffect(.degrees(rotation)) - .onAppear { startRotation() } - .onDisappear { stopRotation() } - - Image(systemName: "xmark") - .font(.system(size: 10, weight: .bold)) - .foregroundColor(Color("scribeCTA")) - } - .contentShape(Circle()) - .onTapGesture { cancelChecking() } + @Binding var state: CheckDataState + @State private var rotation: Double = 0 + @State private var spinnerTimer: Timer? - case .checked: - // Filled orange circle with checkmark + var body: some View { ZStack { - Circle() - .fill(Color("scribeCTA")) - .frame(width: 28, height: 28) + switch state { + case .idle: + // Empty gray circle as idle state. + Circle() + .stroke(Color.gray, lineWidth: 2) + .frame(width: 28, height: 28) + .contentShape(Circle()) + .onTapGesture { startChecking() } + + case .checking: + // Rotating arc (orange) with X cancel. + ZStack { + Circle() + .stroke(Color.gray.opacity(0.3), lineWidth: 2.5) + .frame(width: 28, height: 28) + + Circle() + .trim(from: 0, to: 0.7) + .stroke( + Color("scribeCTA"), style: StrokeStyle(lineWidth: 2.5, lineCap: .round) + ) + .frame(width: 28, height: 28) + .rotationEffect(.degrees(rotation)) + .onAppear { startRotation() } + .onDisappear { stopRotation() } + + Image(systemName: "xmark") + .font(.system(size: 10, weight: .bold)) + .foregroundColor(Color("scribeCTA")) + } + .contentShape(Circle()) + .onTapGesture { cancelChecking() } + + case .checked: + // Filled orange circle with checkmark + ZStack { + Circle() + .fill(Color("scribeCTA")) + .frame(width: 28, height: 28) + + Image(systemName: "checkmark") + .font(.system(size: 12, weight: .bold)) + .foregroundColor(.white) + } + .contentShape(Circle()) + .onTapGesture { resetToIdle() } + } + } + .animation(.easeInOut(duration: 0.2), value: state == .idle) + } - Image(systemName: "checkmark") - .font(.system(size: 12, weight: .bold)) - .foregroundColor(.white) + private func startChecking() { + withAnimation { state = .checking } + // Simulate a check completing after 2 seconds. + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + guard state == .checking else { return } + stopRotation() + withAnimation { state = .checked } } - .contentShape(Circle()) - .onTapGesture { resetToIdle() } - } } - .animation(.easeInOut(duration: 0.2), value: state == .idle) - } - - private func startChecking() { - withAnimation { state = .checking } - // Simulate a check completing after 2 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - guard state == .checking else { return } - stopRotation() - withAnimation { state = .checked } + + private func cancelChecking() { + stopRotation() + withAnimation { state = .idle } } - } - - private func cancelChecking() { - stopRotation() - withAnimation { state = .idle } - } - - private func resetToIdle() { - withAnimation { state = .idle } - } - - private func startRotation() { - rotation = 0 - spinnerTimer?.invalidate() - spinnerTimer = Timer.scheduledTimer(withTimeInterval: 0.016, repeats: true) { _ in - rotation += 4 + + private func resetToIdle() { + withAnimation { state = .idle } + } + + private func startRotation() { + rotation = 0 + spinnerTimer?.invalidate() + spinnerTimer = Timer.scheduledTimer(withTimeInterval: 0.016, repeats: true) { _ in + rotation += 4 + } } - } - private func stopRotation() { - spinnerTimer?.invalidate() - spinnerTimer = nil - } + private func stopRotation() { + spinnerTimer?.invalidate() + spinnerTimer = nil + } } struct UpdateDataCardView: View { - @AppStorage("increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")) - var increaseTextSize: Bool = false - var textSizeMultiplier: CGFloat { increaseTextSize ? 1.25 : 1.0 } - - var languages: [Section] - var onInitializeStates: () -> Void - private let title = NSLocalizedString( - "i18n.app.download.menu_ui.update_data", - value: "Update data", - comment: "" - ) - private let checkText = NSLocalizedString( - "i18n.app.download.menu_ui.update_data.check_new", - value: "Check for new data", - comment: "" - ) - private let regularUpdateText = NSLocalizedString( - "i18n.app.download.menu_ui.update_data.regular_update", - value: "Regularly update data", - comment: "" - ) - @State private var checkState: CheckDataState = .idle - @State private var isRegularUpdate = false - - var body: some View { - VStack(alignment: .leading, spacing: 6) { - Text(title) - .font(.system(size: 19 * textSizeMultiplier, weight: .semibold)) - .foregroundColor(.primary) - - VStack(alignment: .leading, spacing: 12) { - if !languages.isEmpty { - HStack { - Text(checkText) - .font(.system(size: 17 * textSizeMultiplier)) - .foregroundColor(.primary) - - Spacer() - - CheckDataSpinner(state: $checkState) - } - Divider() - } + @AppStorage( + "increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + ) + var increaseTextSize: Bool = false + var textSizeMultiplier: CGFloat { + increaseTextSize ? 1.25 : 1.0 + } + + var languages: [Section] + var onInitializeStates: () -> Void + private let title = NSLocalizedString( + "i18n.app.download.menu_ui.update_data", + value: "Update data", + comment: "" + ) + private let checkText = NSLocalizedString( + "i18n.app.download.menu_ui.update_data.check_new", + value: "Check for new data", + comment: "" + ) + private let regularUpdateText = NSLocalizedString( + "i18n.app.download.menu_ui.update_data.regular_update", + value: "Regularly update data", + comment: "" + ) + @State private var checkState: CheckDataState = .idle + @State private var isRegularUpdate = false + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text(title) + .font(.system(size: 19 * textSizeMultiplier, weight: .semibold)) + .foregroundColor(.primary) + + VStack(alignment: .leading, spacing: 12) { + if !languages.isEmpty { + HStack { + Text(checkText) + .font(.system(size: 17 * textSizeMultiplier)) + .foregroundColor(.primary) + + Spacer() + + CheckDataSpinner(state: $checkState) + } + Divider() + } - Toggle(isOn: $isRegularUpdate) { - HStack { - Text(regularUpdateText) - } + Toggle(isOn: $isRegularUpdate) { + HStack { + Text(regularUpdateText) + } + } + .tint(Color.scribeCTA) + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .padding(.horizontal, 16) } - .tint(Color.scribeCTA) - } - .padding() - .background(Color(.systemBackground)) - .cornerRadius(12) - .padding(.horizontal, 16) } - } } struct LanguageDownloadCard: View { - @AppStorage("increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")) - var increaseTextSize: Bool = false - var textSizeMultiplier: CGFloat { increaseTextSize ? 1.25 : 1.0 } - let language: String - let state: ButtonState - let action: () -> Void - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - HStack { - Text(language) - .font(.system(size: 17 * textSizeMultiplier)) - .foregroundColor(.primary) - - Spacer() - - DownloadButton( - state: state, - action: action - ) - } + @AppStorage( + "increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + ) + var increaseTextSize: Bool = false + var textSizeMultiplier: CGFloat { + increaseTextSize ? 1.25 : 1.0 + } + + let language: String + let state: ButtonState + let action: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Text(language) + .font(.system(size: 17 * textSizeMultiplier)) + .foregroundColor(.primary) + + Spacer() + + DownloadButton( + state: state, + action: action + ) + } + } } - } } struct EmptyStateView: View { - private var noKeyboardText = NSLocalizedString( - "i18n.app.download.menu_ui.no_keyboards_installed", - value: "You currently do not have any Scribe keyboard installed. Please click the Install keyboards button below to install a Scribe keyboard and then come back to download the needed data.", - comment: "" - ) - - private var installText = NSLocalizedString( - "i18n.app.settings.button_install_keyboards", - value: "Install keyboards", - comment: "") - - func openSettingsApp() { - guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { - return + private var noKeyboardText = NSLocalizedString( + "i18n.app.download.menu_ui.no_keyboards_installed", + value: + "You currently do not have any Scribe keyboard installed. Please click the Install keyboards button below to install a Scribe keyboard and then come back to download the needed data.", + comment: "" + ) + + private var installText = NSLocalizedString( + "i18n.app.settings.button_install_keyboards", + value: "Install keyboards", + comment: "" + ) + + func openSettingsApp() { + guard let settingsURL = URL(string: UIApplication.openSettingsURLString) + else { + return + } + UIApplication.shared.open(settingsURL) } - UIApplication.shared.open(settingsURL) - } - - var body: some View { - VStack(alignment: .leading, spacing: 24) { - Text(noKeyboardText) - .foregroundColor(.primary) - .frame(maxWidth: .infinity, alignment: .leading) - .padding() - .background(Color(.systemBackground)) - .cornerRadius(12) - - CTAButton(title: installText, action: {openSettingsApp()}) + + var body: some View { + VStack(alignment: .leading, spacing: 24) { + Text(noKeyboardText) + .foregroundColor(.primary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + + CTAButton(title: installText, action: { openSettingsApp() }) + } + .padding(.horizontal, 16) } - .padding(.horizontal, 16) - } } struct LanguageListView: View { - var onNavigateToTranslationSource: ((String, String) -> Void)? - var languages: [Section] - @AppStorage("increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")) - var increaseTextSize: Bool = false - var textSizeMultiplier: CGFloat { increaseTextSize ? 1.25 : 1.0 } - - @ObservedObject private var stateManager = DownloadStateManager.shared - - private let title = NSLocalizedString( - "i18n.app.download.menu_ui.download_data.title", - value: "Select data to download", - comment: "" - ) - - @State private var showConfirmDialog = false - @State private var targetLanguage = "" - @State private var selectedLanguageCode = "" - let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! - - private func handleUpdateAllLanguages() { - let toDownload = stateManager.downloadStates.keys.filter { - stateManager.downloadStates[$0] != .updated && stateManager.downloadStates[$0] != .downloading + var onNavigateToTranslationSource: ((String, String) -> Void)? + var languages: [Section] + @AppStorage( + "increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + ) + var increaseTextSize: Bool = false + var textSizeMultiplier: CGFloat { + increaseTextSize ? 1.25 : 1.0 } - for lang in toDownload { + @ObservedObject private var stateManager = DownloadStateManager.shared + + private let title = NSLocalizedString( + "i18n.app.download.menu_ui.download_data.title", + value: "Select data to download", + comment: "" + ) + + @State private var showConfirmDialog = false + @State private var targetLanguage = "" + @State private var selectedLanguageCode = "" + let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! + + private func handleUpdateAllLanguages() { + let toDownload = stateManager.downloadStates.keys.filter { + stateManager.downloadStates[$0] != .updated + && stateManager.downloadStates[$0] != .downloading + } + + for lang in toDownload { stateManager.handleDownloadAction(key: lang) } - } + } - private func handleButtonClick(targetLang: String, langCode: String) { + private func handleButtonClick(targetLang: String, langCode: String) { + targetLanguage = targetLang + selectedLanguageCode = langCode + let currentState = stateManager.downloadStates[langCode] ?? .ready + if currentState == .ready { + showConfirmDialog = true + } else { + stateManager.handleDownloadAction(key: langCode) + } + } - targetLanguage = targetLang - selectedLanguageCode = langCode - let currentState = stateManager.downloadStates[langCode] ?? .ready - if currentState == .ready { - showConfirmDialog = true - } else { - stateManager.handleDownloadAction(key: langCode) + /// Determines the button state for the "All languages" option based on the states of individual languages. + private var allLanguagesState: ButtonState { + let states = stateManager.downloadStates.values + if states.allSatisfy({ $0 == .updated }) { return .updated } + if states.allSatisfy({ $0 == .downloading }) { return .downloading } + let actionable = states.filter { $0 != .updated } + if actionable.allSatisfy({ $0 == .update }) { return .update } + return .ready } - } - - // Determines the button state for the "All languages" option based on the states of individual languages. - private var allLanguagesState: ButtonState { - let states = stateManager.downloadStates.values - if states.allSatisfy({ $0 == .updated }) { return .updated } - if states.allSatisfy({ $0 == .downloading }) { return .downloading } - let actionable = states.filter({ $0 != .updated }) - if actionable.allSatisfy({ $0 == .update }) { return .update } - return .ready - } - - var body: some View { - ZStack { - VStack(alignment: .leading, spacing: 6) { - Text(title) - .font(.system(size: 19 * textSizeMultiplier, weight: .semibold)) - .foregroundColor(.primary) - if languages.isEmpty { - EmptyStateView() - } else { - VStack(spacing: 0) { - HStack { - Spacer() - Text(NSLocalizedString( - "i18n.app.download.menu_ui.download_data.update_all", - value: "Update all", - comment: "" - )) - .foregroundColor(Color("linkBlue")) - .font(.system(size: 20, weight: .medium)) - .padding(.horizontal, 12) - .padding(.bottom, 10) - .onTapGesture { - handleUpdateAllLanguages() - } - } - .padding(.horizontal, 4) - ForEach(Array(languages.enumerated()), id: \.offset) { index, section in - let langCode: String = { - if case let .specificLang(code) = section.sectionState { - return code - } - return "" - }() - - LanguageDownloadCard( - language: section.sectionTitle, - state: stateManager.downloadStates[langCode] ?? .ready, - action: { - handleButtonClick(targetLang: section.sectionTitle, langCode: langCode) + + var body: some View { + ZStack { + VStack(alignment: .leading, spacing: 6) { + Text(title) + .font(.system(size: 19 * textSizeMultiplier, weight: .semibold)) + .foregroundColor(.primary) + if languages.isEmpty { + EmptyStateView() + } else { + VStack(spacing: 0) { + HStack { + Spacer() + Text( + NSLocalizedString( + "i18n.app.download.menu_ui.download_data.update_all", + value: "Update all", + comment: "" + ) + ) + .foregroundColor(Color("linkBlue")) + .font(.system(size: 20, weight: .medium)) + .padding(.horizontal, 12) + .padding(.bottom, 10) + .onTapGesture { + handleUpdateAllLanguages() + } + } + .padding(.horizontal, 4) + ForEach(Array(languages.enumerated()), id: \.offset) { index, section in + let langCode: String = { + if case let .specificLang(code) = section.sectionState { + return code + } + return "" + }() + + LanguageDownloadCard( + language: section.sectionTitle, + state: stateManager.downloadStates[langCode] ?? .ready, + action: { + handleButtonClick( + targetLang: section.sectionTitle, langCode: langCode + ) + } + ) + + if index < languages.count - 1 { + Divider() + .padding(.vertical, 8) + } + } + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .padding(.horizontal, 16) } - ) + } - if index < languages.count - 1 { - Divider() - .padding(.vertical, 8) - } + if showConfirmDialog { + confirmDialogView } - } - .padding() - .background(Color(.systemBackground)) - .cornerRadius(12) - .padding(.horizontal, 16) } - } - - if showConfirmDialog { - confirmDialogView - } } - } - private var confirmDialogView: some View { - let languageCode = selectedLanguageCode.isEmpty ? "en" : selectedLanguageCode - let selectedSourceLang = userDefaults.string(forKey: languageCode + "TranslateLanguage") ?? "en" - let sourceLanguage = getKeyInDict(givenValue: selectedSourceLang, dict: languagesAbbrDict) + private var confirmDialogView: some View { + let languageCode = selectedLanguageCode.isEmpty ? "en" : selectedLanguageCode + let selectedSourceLang = + userDefaults.string(forKey: languageCode + "TranslateLanguage") ?? "en" + let sourceLanguage = getKeyInDict(givenValue: selectedSourceLang, dict: languagesAbbrDict) - let localizedSourceLanguage = NSLocalizedString( - "i18n.app._global." + sourceLanguage.lowercased(), - value: sourceLanguage, - comment: "" - ) + let localizedSourceLanguage = NSLocalizedString( + "i18n.app._global." + sourceLanguage.lowercased(), + value: sourceLanguage, + comment: "" + ) - return ConfirmTranslationSource( - infoText: NSLocalizedString( - "i18n.app.download.menu_ui.translation_source_tooltip.download_warning", - value: "The data you will download will allow you to translate from {source_language} to {target_language}. Do you want to change the language you'll translate from?", - comment: "" - ) - .replacingOccurrences(of: "{source_language}", with: localizedSourceLanguage) - .replacingOccurrences(of: "{target_language}", with: targetLanguage), - changeButtonText: NSLocalizedString( - "i18n.app.download.menu_ui.translation_source_tooltip.change_language", - value: "Change language", - comment: "" - ), - confirmButtonText: NSLocalizedString( - "i18n.app.download.menu_ui.translation_source_tooltip.use_source_language", - value: "Use {source_language}", - comment: "" - ) - .replacingOccurrences(of: "{source_language}", with: localizedSourceLanguage), - onDismiss: { - showConfirmDialog = false - }, - onChange: { - showConfirmDialog = false - onNavigateToTranslationSource?(selectedLanguageCode, targetLanguage) - }, - onConfirm: { - showConfirmDialog = false - stateManager.handleDownloadAction(key: selectedLanguageCode) - } - ) - } + return ConfirmTranslationSource( + infoText: NSLocalizedString( + "i18n.app.download.menu_ui.translation_source_tooltip.download_warning", + value: + "The data you will download will allow you to translate from {source_language} to {target_language}. Do you want to change the language you'll translate from?", + comment: "" + ) + .replacingOccurrences(of: "{source_language}", with: localizedSourceLanguage) + .replacingOccurrences(of: "{target_language}", with: targetLanguage), + changeButtonText: NSLocalizedString( + "i18n.app.download.menu_ui.translation_source_tooltip.change_language", + value: "Change language", + comment: "" + ), + confirmButtonText: NSLocalizedString( + "i18n.app.download.menu_ui.translation_source_tooltip.use_source_language", + value: "Use {source_language}", + comment: "" + ) + .replacingOccurrences(of: "{source_language}", with: localizedSourceLanguage), + onDismiss: { + showConfirmDialog = false + }, + onChange: { + showConfirmDialog = false + onNavigateToTranslationSource?(selectedLanguageCode, targetLanguage) + }, + onConfirm: { + showConfirmDialog = false + stateManager.handleDownloadAction(key: selectedLanguageCode) + } + ) + } } struct DownloadDataScreen: View { - var onNavigateToTranslationSource: ((String, String) -> Void)? - @State private var languages = SettingsTableData.getInstalledKeyboardsSections() - @StateObject private var stateManager = DownloadStateManager.shared - - // Initializes the download states for all languages based on the currently installed keyboards. - private func initializeLanguageStates() { - // Extract language abbreviations from sections. - let languageKeys = languages.compactMap { section -> String? in - if case .specificLang(let abbreviation) = section.sectionState { - return abbreviation.lowercased() + var onNavigateToTranslationSource: ((String, String) -> Void)? + @State private var languages = SettingsTableData.getInstalledKeyboardsSections() + @StateObject private var stateManager = DownloadStateManager.shared + + /// Initializes the download states for all languages based on the currently installed keyboards. + private func initializeLanguageStates() { + // Extract language abbreviations from sections. + let languageKeys = languages.compactMap { section -> String? in + if case let .specificLang(abbreviation) = section.sectionState { + return abbreviation.lowercased() + } + return nil } - return nil - } - stateManager.initializeStates(languages: languageKeys) - } - - var body: some View { - ScrollView { - VStack(spacing: 20) { - UpdateDataCardView(languages: languages, onInitializeStates: initializeLanguageStates) - LanguageListView(onNavigateToTranslationSource: onNavigateToTranslationSource, languages: languages) - } - .padding() - .background(Color(UIColor.scribeAppBackground)) - } - .toast(manager: stateManager) - .onAppear { - initializeLanguageStates() + stateManager.initializeStates(languages: languageKeys) } - .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in - // Refresh when returning from Settings - languages = SettingsTableData.getInstalledKeyboardsSections() + + var body: some View { + ScrollView { + VStack(spacing: 20) { + UpdateDataCardView( + languages: languages, onInitializeStates: initializeLanguageStates + ) + LanguageListView( + onNavigateToTranslationSource: onNavigateToTranslationSource, + languages: languages + ) + } + .padding() + .background(Color(UIColor.scribeAppBackground)) + } + .toast(manager: stateManager) + .onAppear { + initializeLanguageStates() + } + .onReceive( + NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification) + ) { _ in + // Refresh when returning from Settings. + languages = SettingsTableData.getInstalledKeyboardsSections() + } } - } } diff --git a/Scribe/InstallationTab/DownloadStateManager.swift b/Scribe/InstallationTab/DownloadStateManager.swift index 2a6d60d1..ea57582d 100644 --- a/Scribe/InstallationTab/DownloadStateManager.swift +++ b/Scribe/InstallationTab/DownloadStateManager.swift @@ -3,121 +3,122 @@ import Foundation import SwiftUI -/// Manages data download states and actions. +// Manages data download states and actions. @MainActor class DownloadStateManager: ObservableObject { - static let shared = DownloadStateManager() + static let shared = DownloadStateManager() - @Published var downloadStates: [String: ButtonState] = [:] - @Published var toastMessage: String? - @Published var showToast: Bool = false + @Published var downloadStates: [String: ButtonState] = [:] + @Published var toastMessage: String? + @Published var showToast: Bool = false - private var downloadTasks: [String: Task] = [:] - private let service = LanguageDataService.shared - private let userDefaults = UserDefaults.standard + private var downloadTasks: [String: Task] = [:] + private let service = LanguageDataService.shared + private let userDefaults = UserDefaults.standard - private init() {} + private init() {} - /// Initialize download states for languages. - func initializeStates(languages: [String]) { - for language in languages { - if downloadStates[language] != nil { continue } + /// Initialize download states for languages. + func initializeStates(languages: [String]) { + for language in languages { + if downloadStates[language] != nil { continue } - // Check if data exists locally. - if service.hasData(for: language) { - downloadStates[language] = .updated - } else { - downloadStates[language] = .ready - } - } - - // Check for updates on downloaded languages. - checkAllForUpdates() - } - - /// Handles the download action based on the current state. - func handleDownloadAction(key: String, forceDownload: Bool = false) { - let currentState = downloadStates[key] ?? .ready - let displayName = getKeyInDict(givenValue: key, dict: languagesAbbrDict) + // Check if data exists locally. + if service.hasData(for: language) { + downloadStates[language] = .updated + } else { + downloadStates[language] = .ready + } + } - // Block if already downloading. - if currentState == .downloading { - return + // Check for updates on downloaded languages. + checkAllForUpdates() } - // Block if updated and not forcing. - if currentState == .updated && !forceDownload { - showToastMessage("\(displayName) data is already up to date") - return - } + /// Handles the download action based on the current state. + func handleDownloadAction(key: String, forceDownload: Bool = false) { + let currentState = downloadStates[key] ?? .ready + let displayName = getKeyInDict(givenValue: key, dict: languagesAbbrDict) - // Proceed with download. - downloadStates[key] = .downloading + // Block if already downloading. + if currentState == .downloading { + return + } - downloadTasks[key] = Task { - do { - try await service.downloadData(language: key, forceDownload: forceDownload) + // Block if updated and not forcing. + if currentState == .updated, !forceDownload { + showToastMessage("\(displayName) data is already up to date") + return + } - if !Task.isCancelled { - downloadStates[key] = .updated - showToastMessage("\(displayName) data downloaded successfully!") + // Proceed with download. + downloadStates[key] = .downloading + + downloadTasks[key] = Task { + do { + try await service.downloadData(language: key, forceDownload: forceDownload) + + if !Task.isCancelled { + downloadStates[key] = .updated + showToastMessage("\(displayName) data downloaded successfully!") + } + } catch is CancellationError { + downloadStates[key] = .ready + } catch let error as NetworkError { + downloadStates[key] = .ready + showToastMessage("Network error: \(error.localizedDescription)") + } catch { + downloadStates[key] = .ready + showToastMessage("Download failed: \(error.localizedDescription)") + } + + downloadTasks.removeValue(forKey: key) } - } catch is CancellationError { - downloadStates[key] = .ready - } catch let error as NetworkError { - downloadStates[key] = .ready - showToastMessage("Network error: \(error.localizedDescription)") - } catch { - downloadStates[key] = .ready - showToastMessage("Download failed: \(error.localizedDescription)") - } - - downloadTasks.removeValue(forKey: key) } - } - /// Check for updates for a specific language. - func checkForUpdates(language: String) { - let currentState = downloadStates[language] ?? .ready + /// Check for updates for a specific language. + func checkForUpdates(language: String) { + let currentState = downloadStates[language] ?? .ready - // Don't check while downloading - if currentState == .downloading { return } + // Don't check while downloading. + if currentState == .downloading { return } - // Only check if already downloaded - guard currentState == .updated else { return } + // Only check if already downloaded. + guard currentState == .updated else { return } - Task { - do { - let hasUpdate = try await service.checkForUpdates(language: language) + Task { + do { + let hasUpdate = try await service.checkForUpdates(language: language) - if !Task.isCancelled { - downloadStates[language] = hasUpdate ? .update : .updated + if !Task.isCancelled { + downloadStates[language] = hasUpdate ? .update : .updated + } + } catch { + print("Error checking updates for \(language): \(error.localizedDescription)") + } } - } catch { - print("Error checking updates for \(language): \(error.localizedDescription)") - } } - } - /// Check all downloaded languages for updates. - func checkAllForUpdates() { - for (language, state) in downloadStates where state == .updated { - checkForUpdates(language: language) + /// Check all downloaded languages for updates. + func checkAllForUpdates() { + for (language, state) in downloadStates where state == .updated { + checkForUpdates(language: language) + } } - } - - // MARK: Toast Helper - private func showToastMessage(_ message: String) { - toastMessage = message - showToast = true - - // Auto-hide after 3 seconds. - Task { - try? await Task.sleep(nanoseconds: 3_000_000_000) - if toastMessage == message { - showToast = false - } + + // MARK: Toast Helper + + private func showToastMessage(_ message: String) { + toastMessage = message + showToast = true + + // Auto-hide after 3 seconds. + Task { + try? await Task.sleep(nanoseconds: 3_000_000_000) + if toastMessage == message { + showToast = false + } + } } - } } diff --git a/Scribe/InstallationTab/InstallationDownload.swift b/Scribe/InstallationTab/InstallationDownload.swift index 47d5a4af..c20056f0 100644 --- a/Scribe/InstallationTab/InstallationDownload.swift +++ b/Scribe/InstallationTab/InstallationDownload.swift @@ -1,63 +1,74 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Download button for the Installation tab. */ import SwiftUI struct CardView: View { - let title: String - let mainText: String - let subtitle: String - let action: () -> Void - @AppStorage("increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")) - var increaseTextSize: Bool = false - - var textSizeMultiplier: CGFloat { - increaseTextSize ? 1.25 : 1.0 + let title: String + let mainText: String + let subtitle: String + let action: () -> Void + @AppStorage( + "increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + ) + var increaseTextSize: Bool = false + + var textSizeMultiplier: CGFloat { + increaseTextSize ? 1.25 : 1.0 } - var body: some View { - VStack(alignment: .leading, spacing: 6) { - Text(title) - .font(.system(size: 19 * textSizeMultiplier, weight: .semibold)) - .foregroundColor(.primary) + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text(title) + .font(.system(size: 19 * textSizeMultiplier, weight: .semibold)) + .foregroundColor(.primary) - VStack(alignment: .leading, spacing: 6) { - HStack { - Text(mainText) - .font(.system(size: 17 * textSizeMultiplier)) - .foregroundColor(.primary) + VStack(alignment: .leading, spacing: 6) { + HStack { + Text(mainText) + .font(.system(size: 17 * textSizeMultiplier)) + .foregroundColor(.primary) - Spacer() + Spacer() - Image(systemName: "chevron.right") - .foregroundColor(.gray) - } + Image(systemName: "chevron.right") + .foregroundColor(.gray) + } - Text(subtitle) - .font(.system(size: 15 * textSizeMultiplier)) - .foregroundColor(.secondary) - } - .padding() - .background(Color(.systemBackground)) - .cornerRadius(12) - .onTapGesture(perform: action) + Text(subtitle) + .font(.system(size: 15 * textSizeMultiplier)) + .foregroundColor(.secondary) + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .onTapGesture(perform: action) + } + .padding(.horizontal) } - .padding(.horizontal) - } } struct InstallationDownload: View { - var onDownloadTapped: (() -> Void) - var body: some View { - CardView( - title: NSLocalizedString("i18n.app.download.menu_option.scribe_title", value: "Language data", comment: ""), - mainText: NSLocalizedString("i18n.app.download.menu_option.scribe_download_data", value: "Download keyboard data", comment: ""), - subtitle: NSLocalizedString("i18n.app.download.menu_option.scribe_description", value: "Add new data to Scribe keyboards.", comment: "") - ) { - onDownloadTapped() + var onDownloadTapped: () -> Void + var body: some View { + CardView( + title: NSLocalizedString( + "i18n.app.download.menu_option.scribe_title", value: "Language data", comment: "" + ), + mainText: NSLocalizedString( + "i18n.app.download.menu_option.scribe_download_data", + value: "Download keyboard data", + comment: "" + ), + subtitle: NSLocalizedString( + "i18n.app.download.menu_option.scribe_description", + value: "Add new data to Scribe keyboards.", comment: "" + ) + ) { + onDownloadTapped() + } } - } } diff --git a/Scribe/InstallationTab/InstallationVC.swift b/Scribe/InstallationTab/InstallationVC.swift index d8c98826..7a44b06d 100644 --- a/Scribe/InstallationTab/InstallationVC.swift +++ b/Scribe/InstallationTab/InstallationVC.swift @@ -1,524 +1,566 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * The ViewController for the Installation screen of the Scribe app. */ -import UIKit import SwiftUI +import UIKit /// A UIViewController that provides instructions on how to install Keyboards as well as information about Scribe. class InstallationVC: UIViewController { - - // Variables linked to elements in AppScreen.storyboard. - @IBOutlet var appTextViewPhone: UITextView! - @IBOutlet var appTextViewPad: UITextView! - var appTextView: UITextView! - - @IBOutlet var appTextBackgroundPhone: UIView! - @IBOutlet var appTextBackgroundPad: UIView! - var appTextBackground: UIView! - - @IBOutlet var topIconPhone: UIImageView! - @IBOutlet var topIconPad: UIImageView! - var topIcon: UIImageView! - - @IBOutlet var settingsBtnPhone: UIButton! - @IBOutlet var settingsBtnPad: UIButton! - var settingsBtn: UIButton! - - @IBOutlet var settingsCornerPhone: UIImageView! - @IBOutlet var settingsCornerPad: UIImageView! - var settingsCorner: UIImageView! - - @IBOutlet var settingCornerWidthConstraintPhone: NSLayoutConstraint! - @IBOutlet var settingCornerWidthConstraintPad: NSLayoutConstraint! - var settingCornerWidthConstraint: NSLayoutConstraint! - - // Spacing views to size app screen proportionally. - @IBOutlet var topSpace: UIView! - @IBOutlet var logoSpace: UIView! - - @IBOutlet var installationHeaderLabel: UILabel! - private var downloadButtonController: UIHostingController? - - private let installationTipCardState: Bool = { - let userDefault = UserDefaults.standard - let state = userDefault.object(forKey: "installationTipCardState") as? Bool ?? true - return state - }() - - func setAppTextView() { - if DeviceType.isPad { - appTextView = appTextViewPad - appTextBackground = appTextBackgroundPad - topIcon = topIconPad - settingsBtn = settingsBtnPad - settingsCorner = settingsCornerPad - settingCornerWidthConstraint = settingCornerWidthConstraintPad - - appTextViewPhone.removeFromSuperview() - appTextBackgroundPhone.removeFromSuperview() - topIconPhone.removeFromSuperview() - settingsBtnPhone.removeFromSuperview() - settingsCornerPhone.removeFromSuperview() - } else { - appTextView = appTextViewPhone - appTextBackground = appTextBackgroundPhone - topIcon = topIconPhone - settingsBtn = settingsBtnPhone - settingsCorner = settingsCornerPhone - settingCornerWidthConstraint = settingCornerWidthConstraintPhone - - appTextViewPad.removeFromSuperview() - appTextBackgroundPad.removeFromSuperview() - topIconPad.removeFromSuperview() - settingsBtnPad.removeFromSuperview() - settingsCornerPad.removeFromSuperview() + // Variables linked to elements in AppScreen.storyboard. + @IBOutlet var appTextViewPhone: UITextView! + @IBOutlet var appTextViewPad: UITextView! + var appTextView: UITextView! + + @IBOutlet var appTextBackgroundPhone: UIView! + @IBOutlet var appTextBackgroundPad: UIView! + var appTextBackground: UIView! + + @IBOutlet var topIconPhone: UIImageView! + @IBOutlet var topIconPad: UIImageView! + var topIcon: UIImageView! + + @IBOutlet var settingsBtnPhone: UIButton! + @IBOutlet var settingsBtnPad: UIButton! + var settingsBtn: UIButton! + + @IBOutlet var settingsCornerPhone: UIImageView! + @IBOutlet var settingsCornerPad: UIImageView! + var settingsCorner: UIImageView! + + @IBOutlet var settingCornerWidthConstraintPhone: NSLayoutConstraint! + @IBOutlet var settingCornerWidthConstraintPad: NSLayoutConstraint! + var settingCornerWidthConstraint: NSLayoutConstraint! + + // Spacing views to size app screen proportionally. + @IBOutlet var topSpace: UIView! + @IBOutlet var logoSpace: UIView! + + @IBOutlet var installationHeaderLabel: UILabel! + private var downloadButtonController: UIHostingController? + + private let installationTipCardState: Bool = { + let userDefault = UserDefaults.standard + return userDefault.object(forKey: "installationTipCardState") as? Bool ?? true + }() + + func setAppTextView() { + if DeviceType.isPad { + appTextView = appTextViewPad + appTextBackground = appTextBackgroundPad + topIcon = topIconPad + settingsBtn = settingsBtnPad + settingsCorner = settingsCornerPad + settingCornerWidthConstraint = settingCornerWidthConstraintPad + + appTextViewPhone.removeFromSuperview() + appTextBackgroundPhone.removeFromSuperview() + topIconPhone.removeFromSuperview() + settingsBtnPhone.removeFromSuperview() + settingsCornerPhone.removeFromSuperview() + } else { + appTextView = appTextViewPhone + appTextBackground = appTextBackgroundPhone + topIcon = topIconPhone + settingsBtn = settingsBtnPhone + settingsCorner = settingsCornerPhone + settingCornerWidthConstraint = settingCornerWidthConstraintPhone + + appTextViewPad.removeFromSuperview() + appTextBackgroundPad.removeFromSuperview() + topIconPad.removeFromSuperview() + settingsBtnPad.removeFromSuperview() + settingsCornerPad.removeFromSuperview() + } } - } - - /// Includes a call to checkDarkModeSetColors to set brand colors and a call to set the UI for the app screen. - override func viewDidLoad() { - super.viewDidLoad() - - navigationController?.setNavigationBarHidden(true, animated: false) - edgesForExtendedLayout = .all - extendedLayoutIncludesOpaqueBars = true - - self.tabBarController?.viewControllers?[0].title = NSLocalizedString( - "i18n.app.installation.title", value: "Installation", comment: "" - ) - self.tabBarController?.viewControllers?[1].title = NSLocalizedString( - "i18n.app.settings.title", value: "Settings", comment: "" - ) - self.tabBarController?.viewControllers?[2].title = NSLocalizedString( - "i18n.app.about.title", value: "About", comment: "" - ) - - setCurrentUI() - showTipCardView() - showDownloadButton() - showCTAButton() - - NotificationCenter.default.addObserver( - self, - selector: #selector(handleNavigateToDownloadScreen), - name: NSNotification.Name("NavigateToDownloadScreen"), - object: nil - ) - NotificationCenter.default.addObserver( - self, - selector: #selector(handleFontSizeUpdate), - name: .fontSizeUpdatedNotification, - object: nil + + /// Includes a call to checkDarkModeSetColors to set brand colors and a call to set the UI for the app screen. + override func viewDidLoad() { + super.viewDidLoad() + + navigationController?.setNavigationBarHidden(true, animated: false) + edgesForExtendedLayout = .all + extendedLayoutIncludesOpaqueBars = true + + tabBarController?.viewControllers?[0].title = NSLocalizedString( + "i18n.app.installation.title", value: "Installation", comment: "" + ) + tabBarController?.viewControllers?[1].title = NSLocalizedString( + "i18n.app.settings.title", value: "Settings", comment: "" + ) + tabBarController?.viewControllers?[2].title = NSLocalizedString( + "i18n.app.about.title", value: "About", comment: "" ) - } - @objc func handleFontSizeUpdate() { - DispatchQueue.main.async { - self.setCurrentUI() - } - } - - @objc private func handleNavigateToDownloadScreen() { - navigateToDownloadDataScreen() - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - /// Includes a call to checkDarkModeSetColors to set brand colors and a call to set the UI for the app screen. - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - setCurrentUI() - } - - /// Includes a call to set the UI for the app screen. - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.setNavigationBarHidden(true, animated: animated) - setCurrentUI() - } - - /// Includes a call to set the UI for the app screen. - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - setCurrentUI() - } - - /// Includes a call to set the UI for the app screen. - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - setCurrentUI() - } - - // Lock the device into portrait mode to avoid resizing issues. - var orientations = UIInterfaceOrientationMask.portrait - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - get { return orientations } - set { orientations = newValue } - } - - /// Sets the top icon for the app screen given the device to assure that it's oriented correctly to its background. - func setTopIcon() { - if DeviceType.isPad { - topIconPhone.isHidden = true - topIconPad.isHidden = false - for constraint in settingsCorner.constraints where constraint.identifier == "settingsCorner" { - constraint.constant = 125 - } - } else { - topIconPhone.isHidden = false - topIconPad.isHidden = true - for constraint in settingsCorner.constraints where constraint.identifier == "settingsCorner" { - constraint.constant = 70 - } + + setCurrentUI() + showTipCardView() + showDownloadButton() + showCTAButton() + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleNavigateToDownloadScreen), + name: NSNotification.Name("NavigateToDownloadScreen"), + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(handleFontSizeUpdate), + name: .fontSizeUpdatedNotification, + object: nil + ) + } + + @objc func handleFontSizeUpdate() { + DispatchQueue.main.async { + self.setCurrentUI() + } + } + + @objc private func handleNavigateToDownloadScreen() { + navigateToDownloadDataScreen() } - } - - /// Sets the functionality of the button over the keyboard installation guide that opens Settings. - func setSettingsBtn() { - settingsBtn.addTarget(self, action: #selector(openSettingsApp), for: .touchUpInside) - settingsBtn.addTarget(self, action: #selector(keyTouchDown), for: .touchDown) - } - - /// Sets constant properties for the app screen. - func setUIConstantProperties() { - // Set the scroll bar so that it appears on a white background regardless of light or dark mode. - let scrollbarAppearance = UINavigationBarAppearance() - scrollbarAppearance.configureWithOpaqueBackground() - - // Disable spacing views. - let allSpacingViews: [UIView] = [topSpace, logoSpace] - for view in allSpacingViews { - view.isUserInteractionEnabled = false - view.backgroundColor = .clear + + deinit { + NotificationCenter.default.removeObserver(self) + } + + /// Includes a call to checkDarkModeSetColors to set brand colors and a call to set the UI for the app screen. + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + setCurrentUI() + } + + /// Includes a call to set the UI for the app screen. + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.setNavigationBarHidden(true, animated: animated) + setCurrentUI() + } + + /// Includes a call to set the UI for the app screen. + override func viewWillTransition( + to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator + ) { + super.viewWillTransition(to: size, with: coordinator) + setCurrentUI() + } + + /// Includes a call to set the UI for the app screen. + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + setCurrentUI() + } + + // Lock the device into portrait mode to avoid resizing issues. + var orientations = UIInterfaceOrientationMask.portrait + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + get { return orientations } + set { orientations = newValue } + } + + /// Sets the top icon for the app screen given the device to assure that it's oriented correctly to its background. + func setTopIcon() { + if DeviceType.isPad { + topIconPhone.isHidden = true + topIconPad.isHidden = false + for constraint in settingsCorner.constraints + where constraint.identifier == "settingsCorner" { + constraint.constant = 125 + } + } else { + topIconPhone.isHidden = false + topIconPad.isHidden = true + for constraint in settingsCorner.constraints + where constraint.identifier == "settingsCorner" { + constraint.constant = 70 + } + } + } + + /// Sets the functionality of the button over the keyboard installation guide that opens Settings. + func setSettingsBtn() { + settingsBtn.addTarget(self, action: #selector(openSettingsApp), for: .touchUpInside) + settingsBtn.addTarget(self, action: #selector(keyTouchDown), for: .touchDown) } - } - - /// Sets properties for the app screen given the current device. - func setUIDeviceProperties() { - // Flips coloured corner with settings icon based on orientation of text. - settingsCorner.image = settingsCorner.image?.imageFlippedForRightToLeftLayoutDirection() - if UIView.userInterfaceLayoutDirection(for: appTextView.semanticContentAttribute) == .rightToLeft { - settingsCorner.layer.maskedCorners = .layerMinXMinYCorner // "top-left" - } else { - settingsCorner.layer.maskedCorners = .layerMaxXMinYCorner // "top-right" + + /// Sets constant properties for the app screen. + func setUIConstantProperties() { + // Set the scroll bar so that it appears on a white background regardless of light or dark mode. + let scrollbarAppearance = UINavigationBarAppearance() + scrollbarAppearance.configureWithOpaqueBackground() + + // Disable spacing views. + let allSpacingViews: [UIView] = [topSpace, logoSpace] + for view in allSpacingViews { + view.isUserInteractionEnabled = false + view.backgroundColor = .clear + } } - settingsCorner.layer.cornerRadius = DeviceType.isPad ? appTextBackground.frame.width * 0.02 : appTextBackground.frame.width * 0.05 - settingsBtn.setTitle("", for: .normal) - settingsBtn.clipsToBounds = true - settingsBtn.layer.masksToBounds = false - settingsBtn.layer.cornerRadius = DeviceType.isPad ? appTextBackground.frame.width * 0.02 : appTextBackground.frame.width * 0.05 + /// Sets properties for the app screen given the current device. + func setUIDeviceProperties() { + // Flips coloured corner with settings icon based on orientation of text. + settingsCorner.image = settingsCorner.image?.imageFlippedForRightToLeftLayoutDirection() + if UIView.userInterfaceLayoutDirection(for: appTextView.semanticContentAttribute) + == .rightToLeft { + settingsCorner.layer.maskedCorners = .layerMinXMinYCorner // "top-left" + } else { + settingsCorner.layer.maskedCorners = .layerMaxXMinYCorner // "top-right" + } + settingsCorner.layer.cornerRadius = + DeviceType.isPad + ? appTextBackground.frame.width * 0.02 : appTextBackground.frame.width * 0.05 + + settingsBtn.setTitle("", for: .normal) + settingsBtn.clipsToBounds = true + settingsBtn.layer.masksToBounds = false + settingsBtn.layer.cornerRadius = + DeviceType.isPad + ? appTextBackground.frame.width * 0.02 : appTextBackground.frame.width * 0.05 + + let allTextViews: [UITextView] = [appTextView] + + // Disable text views. + for textView in allTextViews { + textView.isUserInteractionEnabled = false + textView.backgroundColor = .clear + textView.isEditable = false + } + + // Set backgrounds and corner radii. + appTextBackground.isUserInteractionEnabled = true + appTextBackground.clipsToBounds = true + applyCornerRadius( + elem: appTextBackground, + radius: DeviceType.isPad + ? appTextBackground.frame.width * 0.02 : appTextBackground.frame.width * 0.05 + ) - let allTextViews: [UITextView] = [appTextView] + // Set link attributes for all textViews. + for textView in allTextViews { + textView.linkTextAttributes = [ + NSAttributedString.Key.foregroundColor: linkBlueColor, + NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue + ] + } + } - // Disable text views. - for textView in allTextViews { - textView.isUserInteractionEnabled = false - textView.backgroundColor = .clear - textView.isEditable = false + /// Sets the necessary properties for the installation UI including calling text generation functions. + func setInstallationUI() { + let settingsSymbol: UIImage = getSettingsSymbol(fontSize: fontSize * 0.9) + topIconPhone.image = settingsSymbol + topIconPad.image = settingsSymbol + topIconPhone.tintColor = + UITraitCollection.current.userInterfaceStyle == .dark ? scribeCTAColor : keyCharColor + topIconPad.tintColor = + UITraitCollection.current.userInterfaceStyle == .dark ? scribeCTAColor : keyCharColor + + // Enable installation directions and GitHub notice elements. + settingsBtn.isUserInteractionEnabled = true + appTextBackground.backgroundColor = lightWhiteDarkBlackColor + + // Set the texts for the fields. + appTextView.attributedText = setInstallation(fontSize: fontSize) + appTextView.textColor = keyCharColor } - // Set backgrounds and corner radii. - appTextBackground.isUserInteractionEnabled = true - appTextBackground.clipsToBounds = true - applyCornerRadius( - elem: appTextBackground, - radius: DeviceType.isPad ? appTextBackground.frame.width * 0.02 : appTextBackground.frame.width * 0.05 - ) - - // Set link attributes for all textViews. - for textView in allTextViews { - textView.linkTextAttributes = [ - NSAttributedString.Key.foregroundColor: linkBlueColor, - NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue - ] + /// Creates the current app UI by applying constraints and calling child UI functions. + func setCurrentUI() { + // Sets the font size for the text in the app screen and corresponding UIImage icons. + initializeFontSize() + + installationHeaderLabel.text = NSLocalizedString( + "i18n.app.installation.keyboard.title", value: "Keyboard installation", comment: "" + ) + installationHeaderLabel.font = UIFont.boldSystemFont(ofSize: fontSize * 1.1) + + setAppTextView() + setTopIcon() + setSettingsBtn() + setUIConstantProperties() + setUIDeviceProperties() + setInstallationUI() } - } - - /// Sets the necessary properties for the installation UI including calling text generation functions. - func setInstallationUI() { - let settingsSymbol: UIImage = getSettingsSymbol(fontSize: fontSize * 0.9) - topIconPhone.image = settingsSymbol - topIconPad.image = settingsSymbol - topIconPhone.tintColor = UITraitCollection.current.userInterfaceStyle == .dark ? scribeCTAColor : keyCharColor - topIconPad.tintColor = UITraitCollection.current.userInterfaceStyle == .dark ? scribeCTAColor : keyCharColor - - // Enable installation directions and GitHub notice elements. - settingsBtn.isUserInteractionEnabled = true - appTextBackground.backgroundColor = lightWhiteDarkBlackColor - - // Set the texts for the fields. - appTextView.attributedText = setInstallation(fontSize: fontSize) - appTextView.textColor = keyCharColor - } - - /// Creates the current app UI by applying constraints and calling child UI functions. - func setCurrentUI() { - - // Sets the font size for the text in the app screen and corresponding UIImage icons. - initializeFontSize() - - installationHeaderLabel.text = NSLocalizedString( - "i18n.app.installation.keyboard.title", value: "Keyboard installation", comment: "" - ) - installationHeaderLabel.font = UIFont.boldSystemFont(ofSize: fontSize * 1.1) - - setAppTextView() - setTopIcon() - setSettingsBtn() - setUIConstantProperties() - setUIDeviceProperties() - setInstallationUI() - } - - /// Function to open the settings app that is targeted by settingsBtn. - @objc func openSettingsApp() { - guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { - fatalError("Failed to create settings URL.") + + /// Function to open the settings app that is targeted by settingsBtn. + @objc func openSettingsApp() { + guard let settingsURL = URL(string: UIApplication.openSettingsURLString) + else { + fatalError("Failed to create settings URL.") + } + UIApplication.shared.open(settingsURL) } - UIApplication.shared.open(settingsURL) - } - - /// Function to change the key coloration given a touch down. - /// - /// - Parameters - /// - sender: the button that has been pressed. - @objc func keyTouchDown(_ sender: UIButton) { - sender.backgroundColor = UITraitCollection.current.userInterfaceStyle == .dark ? .white : .black - sender.alpha = 0.2 - topIcon.alpha = 0.2 - - // Bring sender's opacity back up to fully opaque and replace the background color. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in - sender.backgroundColor = .clear - sender.alpha = 1.0 - self?.topIcon.alpha = 1.0 + + /// Function to change the key coloration given a touch down. + /// + /// - Parameters + /// - sender: the button that has been pressed. + @objc func keyTouchDown(_ sender: UIButton) { + sender.backgroundColor = + UITraitCollection.current.userInterfaceStyle == .dark ? .white : .black + sender.alpha = 0.2 + topIcon.alpha = 0.2 + + // Bring sender's opacity back up to fully opaque and replace the background color. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in + sender.backgroundColor = .clear + sender.alpha = 1.0 + self?.topIcon.alpha = 1.0 + } } - } } // MARK: TipHintView extension InstallationVC { - private func showTipCardView() { - let overlayView = InstallationTipCardView( - installationTipCardState: installationTipCardState - ) - - let hostingController = UIHostingController(rootView: overlayView) - hostingController.view.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: 178) - hostingController.view.backgroundColor = .clear - - if !UIDevice.hasNotch { - startGlowingEffect(on: hostingController.view) - addChild(hostingController) - view.addSubview(hostingController.view) - hostingController.view.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - hostingController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 30), - hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor) - ]) - - } else { - startGlowingEffect(on: hostingController.view) - addChild(hostingController) - view.addSubview(hostingController.view) + private func showTipCardView() { + let overlayView = InstallationTipCardView( + installationTipCardState: installationTipCardState + ) + let hostingController = UIHostingController(rootView: overlayView) + hostingController.view.frame = CGRect( + x: 0, y: 0, width: view.bounds.width, height: 178 + ) + hostingController.view.backgroundColor = .clear + + if !UIDevice.hasNotch { + startGlowingEffect(on: hostingController.view) + addChild(hostingController) + view.addSubview(hostingController.view) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 30 + ), + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) + } else { + startGlowingEffect(on: hostingController.view) + addChild(hostingController) + view.addSubview(hostingController.view) + } + hostingController.didMove(toParent: self) } - hostingController.didMove(toParent: self) - } - - func startGlowingEffect(on view: UIView, duration: TimeInterval = 1.0) { - view.layer.shadowColor = UIColor.scribeCTA.cgColor - view.layer.shadowRadius = 8 - view.layer.shadowOpacity = 0.0 - view.layer.shadowOffset = CGSize(width: 0, height: 0) - - UIView.animate( - withDuration: duration, - delay: 0, - options: [.curveEaseOut, .autoreverse], - animations: { - view.layer.shadowOpacity = 0.6 - }, completion: nil - ) - } - - private func showDownloadButton() { - let downloadButton = InstallationDownload(onDownloadTapped: { [weak self] in - self?.navigateToDownloadDataScreen() - }) - - let hostingController = UIHostingController(rootView: downloadButton) - hostingController.view.backgroundColor = .clear - - addChild(hostingController) - view.addSubview(hostingController.view) - - hostingController.view.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - hostingController.view.topAnchor.constraint(equalTo: appTextBackground.bottomAnchor, constant: 20), - hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), - hostingController.view.heightAnchor.constraint(equalToConstant: 120) - ]) - - hostingController.didMove(toParent: self) - self.downloadButtonController = hostingController - } - - private func showCTAButton() { - let ctaButton = CTAButton( - title: NSLocalizedString("i18n.app.installation.button_quick_tutorial", value: "Quick tutorial", comment: ""), - action: { - } - ) - - let hostingController = UIHostingController(rootView: ctaButton) - hostingController.view.backgroundColor = .clear - - addChild(hostingController) - view.addSubview(hostingController.view) - - hostingController.view.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - hostingController.view.topAnchor.constraint(equalTo: downloadButtonController!.view.bottomAnchor, constant: 20), - hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - hostingController.view.heightAnchor.constraint(equalToConstant: 60) - ]) - - hostingController.didMove(toParent: self) - } - - private func navigateToDownloadDataScreen() { - let downloadDataView = DownloadDataScreen( - onNavigateToTranslationSource: { [weak self] languageCode, languageName in - self?.navigateToTranslationSourceSelection(languageCode: languageCode, languageName: languageName) - } - ) - let hostingController = UIHostingController(rootView: downloadDataView) - - hostingController.view.backgroundColor = scribeAppBackgroundColor - - hostingController.edgesForExtendedLayout = .all - hostingController.extendedLayoutIncludesOpaqueBars = true - hostingController.additionalSafeAreaInsets = .zero - - hostingController.title = NSLocalizedString( - "i18n.app._global.download_data", - value: "Download data", - comment: "" - ) - - self.navigationItem.backButtonTitle = NSLocalizedString( - "i18n.app.installation.title", - value: "Installation", - comment: "" - ) - - if let settingsNavController = self.tabBarController?.viewControllers?[1] as? UINavigationController { - self.navigationController?.navigationBar.standardAppearance = settingsNavController.navigationBar.standardAppearance - self.navigationController?.navigationBar.scrollEdgeAppearance = settingsNavController.navigationBar.scrollEdgeAppearance - self.navigationController?.navigationBar.tintColor = settingsNavController.navigationBar.tintColor - self.navigationController?.navigationBar.prefersLargeTitles = settingsNavController.navigationBar.prefersLargeTitles + + func startGlowingEffect(on view: UIView, duration: TimeInterval = 1.0) { + view.layer.shadowColor = UIColor.scribeCTA.cgColor + view.layer.shadowRadius = 8 + view.layer.shadowOpacity = 0.0 + view.layer.shadowOffset = CGSize(width: 0, height: 0) + + UIView.animate( + withDuration: duration, + delay: 0, + options: [.curveEaseOut, .autoreverse], + animations: { + view.layer.shadowOpacity = 0.6 + }, completion: nil + ) } - hostingController.navigationItem.largeTitleDisplayMode = .always + private func showDownloadButton() { + let downloadButton = InstallationDownload(onDownloadTapped: { [weak self] in + self?.navigateToDownloadDataScreen() + }) - self.navigationController?.setNavigationBarHidden(false, animated: false) - self.navigationController?.pushViewController(hostingController, animated: true) - } + let hostingController = UIHostingController(rootView: downloadButton) + hostingController.view.backgroundColor = .clear - private func navigateToTranslationSourceSelection(languageCode: String, languageName: String) { - guard let selectionVC = self.storyboard?.instantiateViewController( - identifier: "SelectionViewTemplateViewController" - ) as? SelectionViewTemplateViewController else { - return - } + addChild(hostingController) + view.addSubview(hostingController.view) + + hostingController.view.translatesAutoresizingMaskIntoConstraints = false - if let hostingController = self.navigationController?.viewControllers.last as? UIHostingController { - hostingController.navigationItem.backButtonTitle = NSLocalizedString( - "i18n.app._global." + languageName.lowercased(), - value: languageName, - comment: "" - ) + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint( + equalTo: appTextBackground.bottomAnchor, constant: 20 + ), + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingController.view.heightAnchor.constraint(equalToConstant: 120) + ]) + + hostingController.didMove(toParent: self) + downloadButtonController = hostingController } - var translateData = SettingsTableData.translateLangSettingsData + private func showCTAButton() { + let ctaButton = CTAButton( + title: NSLocalizedString( + "i18n.app.installation.button_quick_tutorial", value: "Quick tutorial", comment: "" + ), + action: {} + ) + + let hostingController = UIHostingController(rootView: ctaButton) + hostingController.view.backgroundColor = .clear + + addChild(hostingController) + view.addSubview(hostingController.view) - // Remove the current keyboard language from translation options. - if let langCodeIndex = translateData[0].section.firstIndex(where: { s in - s.sectionState == .specificLang(languageCode) - }) { - translateData[0].section.remove(at: langCodeIndex) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint( + equalTo: downloadButtonController!.view.bottomAnchor, constant: 20 + ), + hostingController.view.leadingAnchor.constraint( + equalTo: view.leadingAnchor, constant: 16 + ), + hostingController.view.trailingAnchor.constraint( + equalTo: view.trailingAnchor, constant: -16 + ), + hostingController.view.heightAnchor.constraint(equalToConstant: 60) + ]) + + hostingController.didMove(toParent: self) } - let parentSection = Section( - sectionTitle: languageName, - imageString: nil, - hasToggle: false, - hasNestedNavigation: true, - sectionState: .translateLang, - shortDescription: nil, - externalLink: false - ) - - selectionVC.configureTable(for: translateData, parentSection: parentSection, langCode: languageCode) - selectionVC.edgesForExtendedLayout = .all - - // Copy navigation bar appearance from Settings tab. - if let settingsNavController = self.tabBarController?.viewControllers?[1] as? UINavigationController { - self.navigationController?.navigationBar.standardAppearance = settingsNavController.navigationBar.standardAppearance - self.navigationController?.navigationBar.scrollEdgeAppearance = settingsNavController.navigationBar.scrollEdgeAppearance - self.navigationController?.navigationBar.tintColor = settingsNavController.navigationBar.tintColor - self.navigationController?.navigationBar.barTintColor = settingsNavController.navigationBar.barTintColor + private func navigateToDownloadDataScreen() { + let downloadDataView = DownloadDataScreen( + onNavigateToTranslationSource: { [weak self] languageCode, languageName in + self?.navigateToTranslationSourceSelection( + languageCode: languageCode, languageName: languageName + ) + } + ) + let hostingController = UIHostingController(rootView: downloadDataView) + + hostingController.view.backgroundColor = scribeAppBackgroundColor + + hostingController.edgesForExtendedLayout = .all + hostingController.extendedLayoutIncludesOpaqueBars = true + hostingController.additionalSafeAreaInsets = .zero + + hostingController.title = NSLocalizedString( + "i18n.app._global.download_data", + value: "Download data", + comment: "" + ) + + navigationItem.backButtonTitle = NSLocalizedString( + "i18n.app.installation.title", + value: "Installation", + comment: "" + ) + + if let settingsNavController = tabBarController?.viewControllers?[1] + as? UINavigationController { + navigationController?.navigationBar.standardAppearance = + settingsNavController.navigationBar.standardAppearance + navigationController?.navigationBar.scrollEdgeAppearance = + settingsNavController.navigationBar.scrollEdgeAppearance + navigationController?.navigationBar.tintColor = + settingsNavController.navigationBar.tintColor + navigationController?.navigationBar.prefersLargeTitles = + settingsNavController.navigationBar.prefersLargeTitles + } + + hostingController.navigationItem.largeTitleDisplayMode = .always + + navigationController?.setNavigationBarHidden(false, animated: false) + navigationController?.pushViewController(hostingController, animated: true) } - self.navigationController?.setNavigationBarHidden(false, animated: false) - self.navigationController?.pushViewController(selectionVC, animated: true) - } + private func navigateToTranslationSourceSelection(languageCode: String, languageName: String) { + guard + let selectionVC = storyboard?.instantiateViewController( + identifier: "SelectionViewTemplateViewController" + ) as? SelectionViewTemplateViewController + else { + return + } + + if let hostingController = navigationController?.viewControllers.last + as? UIHostingController { + hostingController.navigationItem.backButtonTitle = NSLocalizedString( + "i18n.app._global." + languageName.lowercased(), + value: languageName, + comment: "" + ) + } + + var translateData = SettingsTableData.translateLangSettingsData + + // Remove the current keyboard language from translation options. + if let langCodeIndex = translateData[0].section.firstIndex(where: { s in + s.sectionState == .specificLang(languageCode) + }) { + translateData[0].section.remove(at: langCodeIndex) + } + + let parentSection = Section( + sectionTitle: languageName, + imageString: nil, + hasToggle: false, + hasNestedNavigation: true, + sectionState: .translateLang, + shortDescription: nil, + externalLink: false + ) + + selectionVC.configureTable( + for: translateData, parentSection: parentSection, langCode: languageCode + ) + selectionVC.edgesForExtendedLayout = .all + + // Copy navigation bar appearance from Settings tab. + if let settingsNavController = tabBarController?.viewControllers?[1] + as? UINavigationController { + navigationController?.navigationBar.standardAppearance = + settingsNavController.navigationBar.standardAppearance + navigationController?.navigationBar.scrollEdgeAppearance = + settingsNavController.navigationBar.scrollEdgeAppearance + navigationController?.navigationBar.tintColor = + settingsNavController.navigationBar.tintColor + navigationController?.navigationBar.barTintColor = + settingsNavController.navigationBar.barTintColor + } + + navigationController?.setNavigationBarHidden(false, animated: false) + navigationController?.pushViewController(selectionVC, animated: true) + } } extension InstallationVC { - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if let tabBar = self.tabBarController?.tabBar { - // Remove existing gesture recognizers to avoid duplicates. - tabBar.gestureRecognizers?.forEach { tabBar.removeGestureRecognizer($0) } - - // Add tap gesture recognizer. - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTabBarTap(_:))) - tapGesture.cancelsTouchesInView = false - tabBar.addGestureRecognizer(tapGesture) + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let tabBar = tabBarController?.tabBar { + // Remove existing gesture recognizers to avoid duplicates. + tabBar.gestureRecognizers?.forEach { tabBar.removeGestureRecognizer($0) } + + // Add tap gesture recognizer. + let tapGesture = UITapGestureRecognizer( + target: self, action: #selector(handleTabBarTap(_:)) + ) + tapGesture.cancelsTouchesInView = false + tabBar.addGestureRecognizer(tapGesture) + } } - } - @objc private func handleTabBarTap(_ gesture: UITapGestureRecognizer) { - guard let tabBar = self.tabBarController?.tabBar else { return } + @objc private func handleTabBarTap(_ gesture: UITapGestureRecognizer) { + guard let tabBar = tabBarController?.tabBar else { return } - let location = gesture.location(in: tabBar) + let location = gesture.location(in: tabBar) - // Calculate which tab was tapped. - let tabWidth = tabBar.bounds.width / CGFloat(tabBar.items?.count ?? 1) - let tappedIndex = Int(location.x / tabWidth) + // Calculate which tab was tapped. + let tabWidth = tabBar.bounds.width / CGFloat(tabBar.items?.count ?? 1) + let tappedIndex = Int(location.x / tabWidth) - // If Installation tab (index 0) was tapped and we're already on it. - if tappedIndex == 0 && self.tabBarController?.selectedIndex == 0 { - if let navController = self.navigationController, - navController.viewControllers.count > 1 { - navController.popToRootViewController(animated: true) - navController.setNavigationBarHidden(true, animated: true) - } + // If Installation tab (index 0) was tapped and we're already on it. + if tappedIndex == 0, tabBarController?.selectedIndex == 0 { + if let navController = navigationController, + navController.viewControllers.count > 1 { + navController.popToRootViewController(animated: true) + navController.setNavigationBarHidden(true, animated: true) + } + } } - } } diff --git a/Scribe/ParentTableCellModel.swift b/Scribe/ParentTableCellModel.swift index 6fd18dcb..1c87cfea 100644 --- a/Scribe/ParentTableCellModel.swift +++ b/Scribe/ParentTableCellModel.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Contains structs that control how buttons in the About tab are built. */ @@ -8,72 +8,72 @@ import Foundation /// Struct used for interpolating the parent and children table of the About section. struct ParentTableCellModel { - let headingTitle: String - var section: [Section] - let hasDynamicData: DynamicDataTableInstance? + let headingTitle: String + var section: [Section] + let hasDynamicData: DynamicDataTableInstance? } struct Section { - let sectionTitle: String - let imageString: String? - let hasToggle: Bool - let hasNestedNavigation: Bool - let sectionState: SectionState - let shortDescription: String? - let externalLink: Bool? + let sectionTitle: String + let imageString: String? + let hasToggle: Bool + let hasNestedNavigation: Bool + let sectionState: SectionState + let shortDescription: String? + let externalLink: Bool? - init( - sectionTitle: String, - imageString: String? = nil, - hasToggle: Bool = false, - hasNestedNavigation: Bool = false, - sectionState: SectionState, - shortDescription: String? = nil, - externalLink: Bool? = false - ) { - self.sectionTitle = sectionTitle - self.imageString = imageString - self.hasToggle = hasToggle - self.hasNestedNavigation = hasNestedNavigation - self.sectionState = sectionState - self.shortDescription = shortDescription - self.externalLink = externalLink - } + init( + sectionTitle: String, + imageString: String? = nil, + hasToggle: Bool = false, + hasNestedNavigation: Bool = false, + sectionState: SectionState, + shortDescription: String? = nil, + externalLink: Bool? = false + ) { + self.sectionTitle = sectionTitle + self.imageString = imageString + self.hasToggle = hasToggle + self.hasNestedNavigation = hasNestedNavigation + self.sectionState = sectionState + self.shortDescription = shortDescription + self.externalLink = externalLink + } } enum SectionState: Equatable { - case github - case matrix - case mastodon - case scribeApps - case wikimedia - case shareScribe - case rateScribe - case bugReport - case email - case version - // case downloadData - // case checkData - case appHints - case privacyPolicy - case licenses - case appLang - case specificLang(String) - case translateLang - case externalLink - case none(UserInteractiveState) + case github + case matrix + case mastodon + case scribeApps + case wikimedia + case shareScribe + case rateScribe + case bugReport + case email + case version + // case downloadData + // case checkData + case appHints + case privacyPolicy + case licenses + case appLang + case specificLang(String) + case translateLang + case externalLink + case none(UserInteractiveState) } enum UserInteractiveState { - case toggleCommaAndPeriod - case doubleSpacePeriods - case autosuggestEmojis - case toggleAccentCharacters - case toggleWordForWordDeletion - case increaseTextSize - case none + case toggleCommaAndPeriod + case doubleSpacePeriods + case autosuggestEmojis + case toggleAccentCharacters + case toggleWordForWordDeletion + case increaseTextSize + case none } enum DynamicDataTableInstance { - case installedKeyboards + case installedKeyboards } diff --git a/Scribe/SettingsTab/SettingsTableData.swift b/Scribe/SettingsTab/SettingsTableData.swift index 32265e9f..cfbe92d8 100644 --- a/Scribe/SettingsTab/SettingsTableData.swift +++ b/Scribe/SettingsTab/SettingsTableData.swift @@ -1,149 +1,225 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Controls data displayed in the Settings tab. */ import Foundation enum SettingsTableData { - static let settingsTableData: [ParentTableCellModel] = [ - ParentTableCellModel( - headingTitle: NSLocalizedString("i18n.app.settings.menu.title", value: "App settings", comment: ""), - section: [ - Section( - sectionTitle: NSLocalizedString("i18n.app.settings.menu.app_language", value: "App language", comment: ""), - hasToggle: false, - sectionState: .appLang, - shortDescription: NSLocalizedString("i18n.app.settings.keyboard.translation.select_source", value: "Select language for app texts.", comment: "") + static let settingsTableData: [ParentTableCellModel] = [ + ParentTableCellModel( + headingTitle: NSLocalizedString( + "i18n.app.settings.menu.title", value: "App settings", comment: "" + ), + section: [ + Section( + sectionTitle: NSLocalizedString( + "i18n.app.settings.menu.app_language", value: "App language", comment: "" + ), + hasToggle: false, + sectionState: .appLang, + shortDescription: NSLocalizedString( + "i18n.app.settings.keyboard.translation.select_source", + value: "Select language for app texts.", comment: "" + ) + ), + Section( + sectionTitle: NSLocalizedString( + "i18n.app.settings.menu.increase_text_size", + value: "Increase app text size", + comment: "" + ), + hasToggle: true, + sectionState: .none(.increaseTextSize), + shortDescription: NSLocalizedString( + "i18n.app.settings.menu.increase_text_size_description", + value: "Increase text sizes for better readability", comment: "" + ) + ) + + ], + hasDynamicData: nil ), - Section( - sectionTitle: NSLocalizedString("i18n.app.settings.menu.increase_text_size", value: "Increase app text size", comment: ""), - hasToggle: true, - sectionState: .none(.increaseTextSize), - shortDescription: NSLocalizedString("i18n.app.settings.menu.increase_text_size_description", value: "Increase text sizes for better readability", comment: "") + ParentTableCellModel( + headingTitle: NSLocalizedString( + "i18n.app.settings.keyboard.title", value: "Select installed keyboard", comment: "" + ), + section: [], + hasDynamicData: .installedKeyboards ) - - ], - hasDynamicData: nil - ), - ParentTableCellModel( - headingTitle: NSLocalizedString("i18n.app.settings.keyboard.title", value: "Select installed keyboard", comment: ""), - section: [], - hasDynamicData: .installedKeyboards - ) - ] - - static let languageSettingsData: [ParentTableCellModel] = [ - ParentTableCellModel( - headingTitle: NSLocalizedString("i18n.app.settings.translation", value: "Translation", comment: ""), - section: [ - Section( - sectionTitle: NSLocalizedString("i18n.app.settings.keyboard.translation.title", value: "Translation source language", comment: ""), - sectionState: .translateLang, - shortDescription: NSLocalizedString("i18n.app.settings.keyboard.translation.select_source_description", value: "Change the language to translate from.", comment: "") - ) - ], - hasDynamicData: nil - ), - ParentTableCellModel( - headingTitle: NSLocalizedString("i18n.app.settings.keyboard.layout.title", value: "Layout", comment: ""), - section: [ - Section( - sectionTitle: NSLocalizedString("i18n.app.settings.keyboard.layout.period_and_comma", value: "Period and comma on ABC", comment: ""), - hasToggle: true, - sectionState: .none(.toggleCommaAndPeriod), - shortDescription: NSLocalizedString("i18n.app.settings.keyboard.layout.period_and_comma_description", value: "Include comma and period keys on the main keyboard for convenient typing.", comment: "") - ), - Section( - sectionTitle: NSLocalizedString("i18n.app.settings.keyboard.layout.disable_accent_characters", value: "Disable accent characters", comment: ""), - imageString: "info.circle", - hasToggle: true, - sectionState: .none(.toggleAccentCharacters), - shortDescription: NSLocalizedString("i18n.app.settings.keyboard.layout.disable_accent_characters_description", value: "Remove accented letter keys on the primary keyboard layout.", comment: "") - ) - ], - hasDynamicData: nil - ), - ParentTableCellModel( - headingTitle: NSLocalizedString("i18n.app.settings.keyboard.functionality.title", value: "Functionality", comment: ""), - section: [ - Section( - sectionTitle: NSLocalizedString("i18n.app.settings.keyboard.functionality.double_space_period", value: "Double space periods", comment: ""), - hasToggle: true, - sectionState: .none(.doubleSpacePeriods), - shortDescription: NSLocalizedString("i18n.app.settings.keyboard.functionality.double_space_period_description", value: "Automatically insert a period when the space key is pressed twice.", comment: "") + ] + + static let languageSettingsData: [ParentTableCellModel] = [ + ParentTableCellModel( + headingTitle: NSLocalizedString( + "i18n.app.settings.translation", value: "Translation", comment: "" + ), + section: [ + Section( + sectionTitle: NSLocalizedString( + "i18n.app.settings.keyboard.translation.title", + value: "Translation source language", + comment: "" + ), + sectionState: .translateLang, + shortDescription: NSLocalizedString( + "i18n.app.settings.keyboard.translation.select_source_description", + value: "Change the language to translate from.", comment: "" + ) + ) + ], + hasDynamicData: nil ), - Section( - sectionTitle: NSLocalizedString("i18n.app.settings.keyboard.functionality.auto_suggest_emoji", value: "Autosuggest emojis", comment: ""), - hasToggle: true, - sectionState: .none(.autosuggestEmojis), - shortDescription: NSLocalizedString("i18n.app.settings.keyboard.functionality.auto_suggest_emoji_description", value: "Turn on emoji suggestions and completions for more expressive typing.", comment: "") + ParentTableCellModel( + headingTitle: NSLocalizedString( + "i18n.app.settings.keyboard.layout.title", value: "Layout", comment: "" + ), + section: [ + Section( + sectionTitle: NSLocalizedString( + "i18n.app.settings.keyboard.layout.period_and_comma", + value: "Period and comma on ABC", + comment: "" + ), + hasToggle: true, + sectionState: .none(.toggleCommaAndPeriod), + shortDescription: NSLocalizedString( + "i18n.app.settings.keyboard.layout.period_and_comma_description", + value: + "Include comma and period keys on the main keyboard for convenient typing.", + comment: "" + ) + ), + Section( + sectionTitle: NSLocalizedString( + "i18n.app.settings.keyboard.layout.disable_accent_characters", + value: "Disable accent characters", comment: "" + ), + imageString: "info.circle", + hasToggle: true, + sectionState: .none(.toggleAccentCharacters), + shortDescription: NSLocalizedString( + "i18n.app.settings.keyboard.layout.disable_accent_characters_description", + value: "Remove accented letter keys on the primary keyboard layout.", + comment: "" + ) + ) + ], + hasDynamicData: nil ), - Section( - sectionTitle: NSLocalizedString("i18n.app.settings.keyboard.functionality.delete_word_by_word", value: "Word for word deletion on long press", comment: ""), - hasToggle: true, - sectionState: .none(.toggleWordForWordDeletion), - shortDescription: NSLocalizedString("i18n.app.settings.keyboard.functionality.delete_word_by_word_description", value: "Delete text word by word when the delete key is pressed and held.", comment: "") + ParentTableCellModel( + headingTitle: NSLocalizedString( + "i18n.app.settings.keyboard.functionality.title", value: "Functionality", + comment: "" + ), + section: [ + Section( + sectionTitle: NSLocalizedString( + "i18n.app.settings.keyboard.functionality.double_space_period", + value: "Double space periods", comment: "" + ), + hasToggle: true, + sectionState: .none(.doubleSpacePeriods), + shortDescription: NSLocalizedString( + "i18n.app.settings.keyboard.functionality.double_space_period_description", + value: "Automatically insert a period when the space key is pressed twice.", + comment: "" + ) + ), + Section( + sectionTitle: NSLocalizedString( + "i18n.app.settings.keyboard.functionality.auto_suggest_emoji", + value: "Autosuggest emojis", comment: "" + ), + hasToggle: true, + sectionState: .none(.autosuggestEmojis), + shortDescription: NSLocalizedString( + "i18n.app.settings.keyboard.functionality.auto_suggest_emoji_description", + value: + "Turn on emoji suggestions and completions for more expressive typing.", + comment: "" + ) + ), + Section( + sectionTitle: NSLocalizedString( + "i18n.app.settings.keyboard.functionality.delete_word_by_word", + value: "Word for word deletion on long press", comment: "" + ), + hasToggle: true, + sectionState: .none(.toggleWordForWordDeletion), + shortDescription: NSLocalizedString( + "i18n.app.settings.keyboard.functionality.delete_word_by_word_description", + value: "Delete text word by word when the delete key is pressed and held.", + comment: "" + ) + ) + ], + hasDynamicData: nil ) - ], - hasDynamicData: nil - ) - ] - - static let translateLangSettingsData: [ParentTableCellModel] = [ - ParentTableCellModel( - headingTitle: NSLocalizedString("i18n.app.settings.keyboard.translation.select_source.caption", value: "What the source language is", comment: ""), - section: getTranslateLanguages(), - hasDynamicData: nil - ) - ] - - static func getInstalledKeyboardsSections() -> [Section] { - var installedKeyboards = [String]() - - guard let appBundleIdentifier = Bundle.main.bundleIdentifier else { return [] } - - guard let keyboards = UserDefaults.standard.dictionaryRepresentation()["AppleKeyboards"] as? [String] else { return [] } - - let customKeyboardExtension = appBundleIdentifier + "." - for keyboard in keyboards where keyboard.hasPrefix(customKeyboardExtension) { - let lang = keyboard.replacingOccurrences(of: customKeyboardExtension, with: "") - installedKeyboards.append(lang.capitalized) - } + ] + + static let translateLangSettingsData: [ParentTableCellModel] = [ + ParentTableCellModel( + headingTitle: NSLocalizedString( + "i18n.app.settings.keyboard.translation.select_source.caption", + value: "What the source language is", comment: "" + ), + section: getTranslateLanguages(), + hasDynamicData: nil + ) + ] - var sections = [Section]() + static func getInstalledKeyboardsSections() -> [Section] { + var installedKeyboards = [String]() - for lang in installedKeyboards { - guard let abbreviation = languagesAbbrDict[lang] else { - fatalError("Abbreviation not found for language: \(lang)") - } - let newSection = Section( - sectionTitle: languagesStringDict[lang]!, - sectionState: .specificLang(abbreviation) - ) + guard let appBundleIdentifier = Bundle.main.bundleIdentifier else { return [] } - sections.append(newSection) - } + guard + let keyboards = UserDefaults.standard.dictionaryRepresentation()["AppleKeyboards"] + as? [String] + else { return [] } + + let customKeyboardExtension = appBundleIdentifier + "." + for keyboard in keyboards where keyboard.hasPrefix(customKeyboardExtension) { + let lang = keyboard.replacingOccurrences(of: customKeyboardExtension, with: "") + installedKeyboards.append(lang.capitalized) + } - return sections - } + var sections = [Section]() - static func getTranslateLanguages() -> [Section] { - var sections = [Section]() + for lang in installedKeyboards { + guard let abbreviation = languagesAbbrDict[lang] + else { + fatalError("Abbreviation not found for language: \(lang)") + } + let newSection = Section( + sectionTitle: languagesStringDict[lang]!, + sectionState: .specificLang(abbreviation) + ) - for lang in languagesAbbrDict.keys.sorted() { - guard let abbreviation = languagesAbbrDict[lang] else { - fatalError("Abbreviation not found for language: \(lang)") - } - let newSection = Section( - sectionTitle: languagesStringDict[lang]!, - sectionState: .specificLang(abbreviation) - ) + sections.append(newSection) + } - sections.append(newSection) + return sections } - return sections - } + static func getTranslateLanguages() -> [Section] { + var sections = [Section]() + + for lang in languagesAbbrDict.keys.sorted() { + guard let abbreviation = languagesAbbrDict[lang] + else { + fatalError("Abbreviation not found for language: \(lang)") + } + let newSection = Section( + sectionTitle: languagesStringDict[lang]!, + sectionState: .specificLang(abbreviation) + ) + + sections.append(newSection) + } + + return sections + } } diff --git a/Scribe/SettingsTab/SettingsViewController.swift b/Scribe/SettingsTab/SettingsViewController.swift index 12361da3..e653c0ee 100644 --- a/Scribe/SettingsTab/SettingsViewController.swift +++ b/Scribe/SettingsTab/SettingsViewController.swift @@ -1,338 +1,377 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Functions for the Settings tab. */ -import UIKit import SwiftUI +import UIKit final class SettingsViewController: UIViewController { - // MARK: Constants - - private var sectionHeaderHeight: CGFloat = 0 - private let separatorInset = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0) - private let cornerRadius: CGFloat = 12 - - private let settingsTipCardState: Bool = { - let userDefault = UserDefaults.standard - let state = userDefault.object(forKey: "settingsTipCardState") as? Bool ?? true - return state - }() - - func setHeaderHeight() { - if DeviceType.isPad { - sectionHeaderHeight = 42 - } else { - sectionHeaderHeight = 32 + // MARK: Constants + + private var sectionHeaderHeight: CGFloat = 0 + private let separatorInset = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0) + private let cornerRadius: CGFloat = 12 + + private let settingsTipCardState: Bool = { + let userDefault = UserDefaults.standard + return userDefault.object(forKey: "settingsTipCardState") as? Bool ?? true + }() + + func setHeaderHeight() { + if DeviceType.isPad { + sectionHeaderHeight = 42 + } else { + sectionHeaderHeight = 32 + } } - } - // MARK: Properties + // MARK: Properties - @IBOutlet var footerFrame: UIView! - @IBOutlet var footerButton: UIButton! - @IBOutlet var parentTable: UITableView! + @IBOutlet var footerFrame: UIView! + @IBOutlet var footerButton: UIButton! + @IBOutlet var parentTable: UITableView! - var tableData = SettingsTableData.settingsTableData + var tableData = SettingsTableData.settingsTableData - // MARK: Functions + // MARK: Functions - override func viewDidLoad() { - super.viewDidLoad() + override func viewDidLoad() { + super.viewDidLoad() - setHeaderHeight() - showTipCardView() + setHeaderHeight() + showTipCardView() - title = NSLocalizedString("i18n.app.settings.title", value: "Settings", comment: "") - navigationItem.backButtonTitle = title + title = NSLocalizedString("i18n.app.settings.title", value: "Settings", comment: "") + navigationItem.backButtonTitle = title - parentTable.register( - WrapperCell.self, - forCellReuseIdentifier: WrapperCell.reuseIdentifier - ) + parentTable.register( + WrapperCell.self, + forCellReuseIdentifier: WrapperCell.reuseIdentifier + ) - parentTable.register( - UINib(nibName: "InfoChildTableViewCell", bundle: nil), - forCellReuseIdentifier: InfoChildTableViewCell.reuseIdentifier - ) - parentTable.dataSource = self - parentTable.delegate = self - parentTable.backgroundColor = .clear - parentTable.sectionHeaderHeight = sectionHeaderHeight - parentTable.separatorInset = separatorInset + parentTable.register( + UINib(nibName: "InfoChildTableViewCell", bundle: nil), + forCellReuseIdentifier: InfoChildTableViewCell.reuseIdentifier + ) + parentTable.dataSource = self + parentTable.delegate = self + parentTable.backgroundColor = .clear + parentTable.sectionHeaderHeight = sectionHeaderHeight + parentTable.separatorInset = separatorInset - setFooterButtonView() + setFooterButtonView() - DispatchQueue.main.async { - self.parentTable.reloadData() + DispatchQueue.main.async { + self.parentTable.reloadData() - self.commonMethodToRefresh() - } - NotificationCenter.default.addObserver( - self, - selector: #selector(handleFontSizeUpdate), - name: .fontSizeUpdatedNotification, - object: nil - ) - } - @objc func handleFontSizeUpdate() { - DispatchQueue.main.async { - self.parentTable.reloadData() - self.setFooterButtonView() - } + self.commonMethodToRefresh() + } + NotificationCenter.default.addObserver( + self, + selector: #selector(handleFontSizeUpdate), + name: .fontSizeUpdatedNotification, + object: nil + ) } - deinit { - NotificationCenter.default.removeObserver(self) + @objc func handleFontSizeUpdate() { + DispatchQueue.main.async { + self.parentTable.reloadData() + self.setFooterButtonView() + } } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - commonMethodToRefresh() - } + deinit { + NotificationCenter.default.removeObserver(self) + } - func commonMethodToRefresh() { - DispatchQueue.main.async { - self.tableData[1].section = SettingsTableData.getInstalledKeyboardsSections() - self.parentTable.reloadData() - self.setFooterButtonView() + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + commonMethodToRefresh() } - let notification = Notification(name: .keyboardsUpdatedNotification, object: nil, userInfo: nil) - NotificationCenter.default.post(notification) - } + func commonMethodToRefresh() { + DispatchQueue.main.async { + self.tableData[1].section = SettingsTableData.getInstalledKeyboardsSections() + self.parentTable.reloadData() + self.setFooterButtonView() + } - func setFooterButtonView() { - if tableData.count > 1 && tableData[1].section.count != 0 { - parentTable.tableFooterView?.isHidden = true - } else { - parentTable.tableFooterView?.isHidden = false + let notification = Notification( + name: .keyboardsUpdatedNotification, object: nil, userInfo: nil + ) + NotificationCenter.default.post(notification) } - footerButton.setTitle(NSLocalizedString("i18n.app.settings.button_install_keyboards", value: "Install keyboards", comment: ""), for: .normal) - footerButton.titleLabel?.font = .systemFont(ofSize: fontSize * 1.5, weight: .bold) + func setFooterButtonView() { + if tableData.count > 1, tableData[1].section.count != 0 { + parentTable.tableFooterView?.isHidden = true + } else { + parentTable.tableFooterView?.isHidden = false + } - footerButton.backgroundColor = appBtnColor - if UITraitCollection.current.userInterfaceStyle == .dark { - footerButton.layer.borderWidth = 1 - footerButton.layer.borderColor = scribeCTAColor.cgColor + footerButton.setTitle( + NSLocalizedString( + "i18n.app.settings.button_install_keyboards", value: "Install keyboards", + comment: "" + ), + for: .normal + ) + footerButton.titleLabel?.font = .systemFont(ofSize: fontSize * 1.5, weight: .bold) + + footerButton.backgroundColor = appBtnColor + if UITraitCollection.current.userInterfaceStyle == .dark { + footerButton.layer.borderWidth = 1 + footerButton.layer.borderColor = scribeCTAColor.cgColor + } + footerButton.setTitleColor(lightTextDarkCTA, for: .normal) + footerFrame.layer.cornerRadius = footerFrame.frame.width * 0.025 + footerButton.layer.cornerRadius = footerFrame.frame.width * 0.025 + footerButton.layer.shadowColor = + UIColor(red: 0.247, green: 0.247, blue: 0.275, alpha: 0.25).cgColor + footerButton.layer.shadowOffset = CGSize(width: 0.0, height: 3.0) + footerButton.layer.shadowOpacity = 1.0 + footerButton.layer.masksToBounds = false + + footerButton.addTarget(self, action: #selector(openSettingsApp), for: .touchUpInside) + footerButton.addTarget(self, action: #selector(keyTouchDown), for: .touchDown) } - footerButton.setTitleColor(lightTextDarkCTA, for: .normal) - footerFrame.layer.cornerRadius = footerFrame.frame.width * 0.025 - footerButton.layer.cornerRadius = footerFrame.frame.width * 0.025 - footerButton.layer.shadowColor = UIColor(red: 0.247, green: 0.247, blue: 0.275, alpha: 0.25).cgColor - footerButton.layer.shadowOffset = CGSize(width: 0.0, height: 3.0) - footerButton.layer.shadowOpacity = 1.0 - footerButton.layer.masksToBounds = false - - footerButton.addTarget(self, action: #selector(openSettingsApp), for: .touchUpInside) - footerButton.addTarget(self, action: #selector(keyTouchDown), for: .touchDown) - } } // MARK: UITableViewDataSource extension SettingsViewController: UITableViewDataSource { - func numberOfSections(in _: UITableView) -> Int { - tableData.count - } - - func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { - tableData[section].section.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell( - withIdentifier: WrapperCell.reuseIdentifier, - for: indexPath - ) as? WrapperCell else { - fatalError("Failed to dequeue WrapperCell") + func numberOfSections(in _: UITableView) -> Int { + tableData.count + } + + func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { + tableData[section].section.count } - let section = tableData[indexPath.section] - let setting = section.section[indexPath.row] + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard + let cell = tableView.dequeueReusableCell( + withIdentifier: WrapperCell.reuseIdentifier, + for: indexPath + ) as? WrapperCell + else { + fatalError("Failed to dequeue WrapperCell") + } + + let section = tableData[indexPath.section] + let setting = section.section[indexPath.row] - cell.configure(withCellNamed: "InfoChildTableViewCell", section: setting) + cell.configure(withCellNamed: "InfoChildTableViewCell", section: setting) - let isFirstRow = indexPath.row == 0 - let isLastRow = indexPath.row == section.section.count - 1 - WrapperCell.applyCornerRadius(to: cell, isFirst: isFirstRow, isLast: isLastRow) + let isFirstRow = indexPath.row == 0 + let isLastRow = indexPath.row == section.section.count - 1 + WrapperCell.applyCornerRadius(to: cell, isFirst: isFirstRow, isLast: isLastRow) - return cell - } + return cell + } } // MARK: UITableViewDelegate extension SettingsViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let tableSection = tableData[indexPath.section] - let section = tableSection.section[indexPath.row] - - switch section.sectionState { - case .appLang: - let preferredLanguages = NSLocale.preferredLanguages - if preferredLanguages.count == 1 { - let alert = UIAlertController( - title: NSLocalizedString("i18n.app.settings.menu.app_language.one_device_language_warning.title", - value: "No languages installed", - comment: ""), - message: NSLocalizedString("i18n.app.settings.menu.app_language.one_device_language_warning.message", value: "You only have one language installed on your device. Please install more languages in Settings and then you can select different localizations of Scribe.", comment: ""), - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - present(alert, animated: true) - } else { - openSettingsApp() - } - - case .specificLang: - if let viewController = storyboard?.instantiateViewController( - identifier: "TableViewTemplateViewController" - ) as? TableViewTemplateViewController { - // Copy base settings. - var data = SettingsTableData.languageSettingsData - // Check if the device is an iPad to remove the Layout Section. - if DeviceType.isPad { - for menuOption in data[1].section { - if menuOption.sectionState == .none(.toggleAccentCharacters) || - menuOption.sectionState == .none(.toggleCommaAndPeriod) { - data[1].section.remove(at: 0) + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let tableSection = tableData[indexPath.section] + let section = tableSection.section[indexPath.row] + + switch section.sectionState { + case .appLang: + let preferredLanguages = NSLocale.preferredLanguages + if preferredLanguages.count == 1 { + let alert = UIAlertController( + title: NSLocalizedString( + "i18n.app.settings.menu.app_language.one_device_language_warning.title", + value: "No languages installed", + comment: "" + ), + message: NSLocalizedString( + "i18n.app.settings.menu.app_language.one_device_language_warning.message", + value: + "You only have one language installed on your device. Please install more languages in Settings and then you can select different localizations of Scribe.", + comment: "" + ), + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + present(alert, animated: true) + } else { + openSettingsApp() } - } - if data[1].section.isEmpty { - data.remove(at: 1) - } - } else { - // Languages where we can disable accent keys. - let accentKeyLanguages: [String] = [ - languagesStringDict["German"]!, - languagesStringDict["Spanish"]!, - languagesStringDict["Swedish"]! - ] - - let accentKeyOptionIndex = SettingsTableData.languageSettingsData[1].section.firstIndex(where: { s in - s.sectionTitle.elementsEqual(NSLocalizedString("i18n.app.settings.keyboard.layout.disable_accent_characters", value: "Disable accent characters", comment: "")) - }) ?? -1 - - // If there are no accent keys we can remove the `Disable accent characters` option. - if accentKeyLanguages.firstIndex(of: section.sectionTitle) == nil && accentKeyOptionIndex != -1 { - data[1].section.remove(at: accentKeyOptionIndex) - } else if accentKeyLanguages.firstIndex(of: section.sectionTitle) != nil && accentKeyOptionIndex == -1 { - data[1].section.insert(SettingsTableData.languageSettingsData[2].section[2], at: 1) - } - } - viewController.configureTable(for: data, parentSection: section) + case .specificLang: + if let viewController = storyboard?.instantiateViewController( + identifier: "TableViewTemplateViewController" + ) as? TableViewTemplateViewController { + // Copy base settings. + var data = SettingsTableData.languageSettingsData + // Check if the device is an iPad to remove the Layout Section. + if DeviceType.isPad { + for menuOption in data[1].section { + if menuOption.sectionState == .none(.toggleAccentCharacters) + || menuOption.sectionState == .none(.toggleCommaAndPeriod) { + data[1].section.remove(at: 0) + } + } + if data[1].section.isEmpty { + data.remove(at: 1) + } + } else { + // Languages where we can disable accent keys. + let accentKeyLanguages: [String] = [ + languagesStringDict["German"]!, + languagesStringDict["Spanish"]!, + languagesStringDict["Swedish"]! + ] + + let accentKeyOptionIndex = + SettingsTableData.languageSettingsData[1].section.firstIndex(where: { s in + s.sectionTitle.elementsEqual( + NSLocalizedString( + "i18n.app.settings.keyboard.layout.disable_accent_characters", + value: "Disable accent characters", comment: "" + ) + ) + }) ?? -1 + + // If there are no accent keys we can remove the `Disable accent characters` option. + if accentKeyLanguages.firstIndex(of: section.sectionTitle) == nil, + accentKeyOptionIndex != -1 { + data[1].section.remove(at: accentKeyOptionIndex) + } else if accentKeyLanguages.firstIndex(of: section.sectionTitle) != nil, + accentKeyOptionIndex == -1 { + data[1].section.insert( + SettingsTableData.languageSettingsData[2].section[2], at: 1 + ) + } + } + + viewController.configureTable(for: data, parentSection: section) + + navigationController?.pushViewController(viewController, animated: true) + } - navigationController?.pushViewController(viewController, animated: true) - } + default: break + } - default: break + if let selectedIndexPath = tableView.indexPathForSelectedRow { + tableView.deselectRow(at: selectedIndexPath, animated: false) + } } - if let selectedIndexPath = tableView.indexPathForSelectedRow { - tableView.deselectRow(at: selectedIndexPath, animated: false) + func tableView(_: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + let section = tableData[indexPath.section] + let setting = section.section[indexPath.row] + + let hasDescription = setting.shortDescription != nil + return hasDescription ? 80.0 : 48.0 } - } - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let section = tableData[indexPath.section] - let setting = section.section[indexPath.row] + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let headerView: UIView - let hasDescription = setting.shortDescription != nil - return hasDescription ? 80.0 : 48.0 - } + if let reusableHeaderView = tableView.headerView(forSection: section) { + headerView = reusableHeaderView + } else { + headerView = UIView( + frame: CGRect(x: 0, y: 0, width: parentTable.bounds.width, height: 32) + ) + } - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let headerView: UIView + let label = UILabel( + frame: CGRect( + x: preferredLanguage.prefix(2) == "ar" ? -1 * headerView.bounds.width / 10 : 0, + y: 0, + width: headerView.bounds.width, + height: 32 + ) + ) - if let reusableHeaderView = tableView.headerView(forSection: section) { - headerView = reusableHeaderView - } else { - headerView = UIView(frame: CGRect(x: 0, y: 0, width: parentTable.bounds.width, height: 32)) + label.text = tableData[section].headingTitle + label.font = UIFont.boldSystemFont(ofSize: fontSize * 1.1) + label.textColor = keyCharColor + headerView.addSubview(label) + + return headerView } - let label = UILabel( - frame: CGRect( - x: preferredLanguage.prefix(2) == "ar" ? -1 * headerView.bounds.width / 10: 0, - y: 0, - width: headerView.bounds.width, - height: 32 - ) - ) - - label.text = tableData[section].headingTitle - label.font = UIFont.boldSystemFont(ofSize: fontSize * 1.1) - label.textColor = keyCharColor - headerView.addSubview(label) - - return headerView - } - - /// Function to open the settings app that is targeted by settingsBtn. - @objc func openSettingsApp() { - guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { - fatalError("Failed to create settings URL.") + /// Function to open the settings app that is targeted by settingsBtn. + @objc func openSettingsApp() { + guard let settingsURL = URL(string: UIApplication.openSettingsURLString) + else { + fatalError("Failed to create settings URL.") + } + UIApplication.shared.open(settingsURL) } - UIApplication.shared.open(settingsURL) - } - - /// Function to change the key coloration given a touch down. - /// - /// - Parameters - /// - sender: the button that has been pressed. - @objc func keyTouchDown(_ sender: UIButton) { - sender.backgroundColor = .black - sender.alpha = 0.2 - - // Bring sender's opacity back up to fully opaque and replace the background color. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { - sender.backgroundColor = .clear - sender.alpha = 1.0 + + /// Function to change the key coloration given a touch down. + /// + /// - Parameters + /// - sender: the button that has been pressed. + @objc func keyTouchDown(_ sender: UIButton) { + sender.backgroundColor = .black + sender.alpha = 0.2 + + // Bring sender's opacity back up to fully opaque and replace the background color. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + sender.backgroundColor = .clear + sender.alpha = 1.0 + } } - } } // MARK: TipCardView + extension SettingsViewController { - private func showTipCardView() { - let overlayView = SettingsTipCardView( - settingsTipCardState: settingsTipCardState - ) - - let hostingController = UIHostingController(rootView: overlayView) - hostingController.view.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: -20) - hostingController.view.backgroundColor = .clear - hostingController.view.translatesAutoresizingMaskIntoConstraints = false - - let navigationView = navigationController?.navigationBar - guard let navigationView else { return } - navigationView.addSubview(hostingController.view) - navigationView.bringSubviewToFront(hostingController.view) - - NSLayoutConstraint.activate([ - hostingController.view.topAnchor.constraint(equalTo: navigationView.topAnchor, constant: 30), - hostingController.view.leadingAnchor.constraint(equalTo: navigationView.leadingAnchor), - hostingController.view.trailingAnchor.constraint(equalTo: navigationView.trailingAnchor) - ]) - hostingController.didMove(toParent: self) - startGlowingEffect(on: hostingController.view) - } - - func startGlowingEffect(on view: UIView, duration: TimeInterval = 1.0) { - view.layer.shadowColor = UIColor.scribeCTA.cgColor - view.layer.shadowRadius = 8 - view.layer.shadowOpacity = 0.0 - view.layer.shadowOffset = CGSize(width: 0, height: 0) - - UIView.animate( - withDuration: duration, - delay: 0, - options: [.curveEaseOut, .autoreverse], - animations: { - view.layer.shadowOpacity = 0.6 - }, completion: nil) - } + private func showTipCardView() { + let overlayView = SettingsTipCardView( + settingsTipCardState: settingsTipCardState + ) + + let hostingController = UIHostingController(rootView: overlayView) + hostingController.view.frame = CGRect( + x: 0, y: 0, width: view.bounds.width, height: -20 + ) + hostingController.view.backgroundColor = .clear + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + + let navigationView = navigationController?.navigationBar + guard let navigationView else { return } + navigationView.addSubview(hostingController.view) + navigationView.bringSubviewToFront(hostingController.view) + + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint( + equalTo: navigationView.topAnchor, constant: 30 + ), + hostingController.view.leadingAnchor.constraint(equalTo: navigationView.leadingAnchor), + hostingController.view.trailingAnchor.constraint( + equalTo: navigationView.trailingAnchor + ) + ]) + hostingController.didMove(toParent: self) + startGlowingEffect(on: hostingController.view) + } + + func startGlowingEffect(on view: UIView, duration: TimeInterval = 1.0) { + view.layer.shadowColor = UIColor.scribeCTA.cgColor + view.layer.shadowRadius = 8 + view.layer.shadowOpacity = 0.0 + view.layer.shadowOffset = CGSize(width: 0, height: 0) + + UIView.animate( + withDuration: duration, + delay: 0, + options: [.curveEaseOut, .autoreverse], + animations: { + view.layer.shadowOpacity = 0.6 + }, completion: nil + ) + } } diff --git a/Scribe/TipCard/TipCardView.swift b/Scribe/TipCard/TipCardView.swift index d4d8529d..416e2bc6 100644 --- a/Scribe/TipCard/TipCardView.swift +++ b/Scribe/TipCard/TipCardView.swift @@ -1,93 +1,114 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * The app tip views for the application tabs. */ import SwiftUI struct TipCardView: View { - @AppStorage("increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")) - var increaseTextSize: Bool = false - var textSizeMultiplier: CGFloat { increaseTextSize ? 1.25 : 1.0 } + @AppStorage( + "increaseTextSize", store: UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") + ) + var increaseTextSize: Bool = false + var textSizeMultiplier: CGFloat { + increaseTextSize ? 1.25 : 1.0 + } - private let buttonHeight = 70.0 - private let multiplicityPadding = 0.5 - private let leadingPadding = 40.0 - private let cardCornerRadius: CGFloat = 10 - var infoText: String - @Binding var tipCardState: Bool - var onDismiss: (() -> Void)? - let ok = "OK" - var body: some View { - ZStack { - RoundedRectangle(cornerRadius: cardCornerRadius) - .fill(Color.lightWhiteDarkBlack) - HStack { - Image(systemName: "lightbulb.max") - .resizable() - .frame(width: leadingPadding, height: leadingPadding) - .foregroundColor(Color.scribeCTA) - .padding(.horizontal) - Text(infoText) - .font(Font.system(size: DeviceType.isPad ? 22 * textSizeMultiplier : 17 * textSizeMultiplier, weight: .medium)) - Spacer() - Button { - tipCardState = false - self.onDismiss?() - } label: { - Text(ok) - .foregroundColor(.white) + private let buttonHeight = 70.0 + private let multiplicityPadding = 0.5 + private let leadingPadding = 40.0 + private let cardCornerRadius: CGFloat = 10 + var infoText: String + @Binding var tipCardState: Bool + var onDismiss: (() -> Void)? + let ok = "OK" + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: cardCornerRadius) + .fill(Color.lightWhiteDarkBlack) + HStack { + Image(systemName: "lightbulb.max") + .resizable() + .frame(width: leadingPadding, height: leadingPadding) + .foregroundColor(Color.scribeCTA) + .padding(.horizontal) + Text(infoText) + .font( + Font.system( + size: DeviceType.isPad + ? 22 * textSizeMultiplier : 17 * textSizeMultiplier, + weight: .medium + ) + ) + Spacer() + Button { + tipCardState = false + self.onDismiss?() + } label: { + Text(ok) + .foregroundColor(.white) + } + .frame(width: leadingPadding, height: leadingPadding) + .background(Color.scribeBlue) + .cornerRadius(cardCornerRadius) + .padding(.horizontal) + } } - .frame(width: leadingPadding, height: leadingPadding) - .background(Color.scribeBlue) - .cornerRadius(cardCornerRadius) - .padding(.horizontal) - } + .frame( + width: UIScreen.main.bounds.size.width - leadingPadding * multiplicityPadding, + height: buttonHeight + ) + .shadow(color: Color.keyShadow, radius: cardCornerRadius / 2) + .opacity(tipCardState ? 1.0 : 0) + .edgesIgnoringSafeArea(.all) } - .frame( - width: UIScreen.main.bounds.size.width - leadingPadding * multiplicityPadding, - height: buttonHeight - ) - .shadow(color: Color.keyShadow, radius: cardCornerRadius / 2) - .opacity(tipCardState ? 1.0 : 0) - .edgesIgnoringSafeArea(.all) - } } struct InstallationTipCardView: View { - @AppStorage("installationTipCardState", store: .standard) var installationTipCardState: Bool = true + @AppStorage("installationTipCardState", store: .standard) var installationTipCardState: Bool = + true - var body: some View { - TipCardView( - infoText: NSLocalizedString("i18n.app.installation.app_hint_tooltip", - value: "Follow the directions below to install Scribe keyboards on your device.", - comment: ""), - tipCardState: $installationTipCardState - ) - } + var body: some View { + TipCardView( + infoText: NSLocalizedString( + "i18n.app.installation.app_hint_tooltip", + value: "Follow the directions below to install Scribe keyboards on your device.", + comment: "" + ), + tipCardState: $installationTipCardState + ) + } } struct SettingsTipCardView: View { - @AppStorage("settingsTipCardState", store: .standard) var settingsTipCardState: Bool = true - var onDismiss: (() -> Void)? + @AppStorage("settingsTipCardState", store: .standard) var settingsTipCardState: Bool = true + var onDismiss: (() -> Void)? - var body: some View { - TipCardView( - infoText: NSLocalizedString("i18n.app.settings.app_hint_tooltip", value: "Settings for the app and installed language keyboards are found here.", comment: ""), - tipCardState: $settingsTipCardState, - onDismiss: onDismiss - ) - } + var body: some View { + TipCardView( + infoText: NSLocalizedString( + "i18n.app.settings.app_hint_tooltip", + value: "Settings for the app and installed language keyboards are found here.", + comment: "" + ), + tipCardState: $settingsTipCardState, + onDismiss: onDismiss + ) + } } struct AboutTipCardView: View { - @AppStorage("aboutTipCardState", store: .standard) var aboutTipCardState: Bool = true + @AppStorage("aboutTipCardState", store: .standard) var aboutTipCardState: Bool = true - var body: some View { - TipCardView( - infoText: NSLocalizedString("i18n.app.about.app_hint_tooltip", value: "Here's where you can learn more about Scribe and its community.", comment: ""), - tipCardState: $aboutTipCardState - ) - } + var body: some View { + TipCardView( + infoText: NSLocalizedString( + "i18n.app.about.app_hint_tooltip", + value: "Here's where you can learn more about Scribe and its community.", + comment: "" + ), + tipCardState: $aboutTipCardState + ) + } } diff --git a/Scribe/Toast/ToastView.swift b/Scribe/Toast/ToastView.swift index e660dfb1..3a16cdb1 100644 --- a/Scribe/Toast/ToastView.swift +++ b/Scribe/Toast/ToastView.swift @@ -8,7 +8,6 @@ struct ToastView: View { var body: some View { HStack(spacing: 12) { - Text(message) .font(.subheadline) @@ -24,6 +23,7 @@ struct ToastView: View { } // MARK: Toast Modifier + struct ToastModifier: ViewModifier { @ObservedObject var manager: DownloadStateManager diff --git a/Scribe/Views/BaseTableViewController.swift b/Scribe/Views/BaseTableViewController.swift index 37fd4ad6..530f1955 100644 --- a/Scribe/Views/BaseTableViewController.swift +++ b/Scribe/Views/BaseTableViewController.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Base for table view in the Scribe app. */ @@ -8,130 +8,139 @@ import SwipeableTabBarController import UIKit class BaseTableViewController: UITableViewController { - // MARK: Constants + // MARK: Constants - private var sectionHeaderHeight: CGFloat = 0 - private let separatorInset = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0) + private var sectionHeaderHeight: CGFloat = 0 + private let separatorInset = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0) - func setHeaderHeight() { - if DeviceType.isPad { - sectionHeaderHeight = 48 - } else { - sectionHeaderHeight = 32 + func setHeaderHeight() { + if DeviceType.isPad { + sectionHeaderHeight = 48 + } else { + sectionHeaderHeight = 32 + } } - } - // MARK: Properties + // MARK: Properties - var dataSet: [ParentTableCellModel] { - [] - } - - // MARK: Functions + var dataSet: [ParentTableCellModel] { + [] + } - override func viewDidLoad() { - super.viewDidLoad() - setHeaderHeight() + // MARK: Functions - tableView.sectionHeaderHeight = sectionHeaderHeight - tableView.register( - UINib(nibName: "InfoChildTableViewCell", bundle: nil), - forCellReuseIdentifier: "InfoChildTableViewCell" - ) - tableView.separatorInset = separatorInset - if let tabBarController = tabBarController as? SwipeableTabBarController { - tabBarController.isCyclingEnabled = true - } + override func viewDidLoad() { + super.viewDidLoad() + setHeaderHeight() - NotificationCenter.default.addObserver( - self, - selector: #selector(handleFontSizeUpdate), - name: .fontSizeUpdatedNotification, - object: nil + tableView.sectionHeaderHeight = sectionHeaderHeight + tableView.register( + UINib(nibName: "InfoChildTableViewCell", bundle: nil), + forCellReuseIdentifier: "InfoChildTableViewCell" ) + tableView.separatorInset = separatorInset + if let tabBarController = tabBarController as? SwipeableTabBarController { + tabBarController.isCyclingEnabled = true + } + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleFontSizeUpdate), + name: .fontSizeUpdatedNotification, + object: nil + ) + } - } - - @objc func handleFontSizeUpdate() { - DispatchQueue.main.async { - self.tableView.reloadData() - } + @objc func handleFontSizeUpdate() { + DispatchQueue.main.async { + self.tableView.reloadData() + } } - deinit { - NotificationCenter.default.removeObserver(self) + deinit { + NotificationCenter.default.removeObserver(self) } } // MARK: UITableViewDataSource extension BaseTableViewController { - override func numberOfSections(in _: UITableView) -> Int { - dataSet.count - } + override func numberOfSections(in _: UITableView) -> Int { + dataSet.count + } - override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { - dataSet[section].section.count - } + override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { + dataSet[section].section.count + } } // MARK: UITableViewDelegate extension BaseTableViewController { - override func tableView(_: UITableView, viewForHeaderInSection section: Int) -> UIView? { - return headerView(for: section) - } - - private func headerView(for section: Int) -> UIView? { - let headerView: UIView - - if let reusableHeaderView = tableView.headerView(forSection: section) { - headerView = reusableHeaderView - } else { - headerView = UIView( - frame: CGRect(x: 0, y: 0, width: tableView.bounds.width, height: sectionHeaderHeight) - ) + override func tableView(_: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return headerView(for: section) } - let label = UILabel() - label.text = dataSet[section].headingTitle - label.font = UIFont.boldSystemFont(ofSize: fontSize * 1.1) - label.textColor = keyCharColor - label.numberOfLines = 0 - label.lineBreakMode = .byWordWrapping - label.adjustsFontSizeToFitWidth = true - label.minimumScaleFactor = 0.8 - label.textAlignment = .natural - - label.translatesAutoresizingMaskIntoConstraints = false - headerView.addSubview(label) - - let horizontalPadding: CGFloat = 8 - let verticalPadding: CGFloat = 4 - - NSLayoutConstraint.activate([ - label.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: horizontalPadding), - label.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -horizontalPadding), - label.topAnchor.constraint(equalTo: headerView.topAnchor, constant: verticalPadding), - label.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: -verticalPadding) - ]) - - return headerView - } - - override func tableView(_: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - let label = UILabel() - label.text = dataSet[section].headingTitle - label.font = UIFont.boldSystemFont(ofSize: fontSize * 1.1) - label.numberOfLines = 0 - label.lineBreakMode = .byWordWrapping - - let horizontalPadding: CGFloat = 32 - let verticalPadding: CGFloat = 24 - - let constrainedWidth = tableView.bounds.width - horizontalPadding - let size = label.sizeThatFits(CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude)) - - return size.height + verticalPadding - } + private func headerView(for section: Int) -> UIView? { + let headerView: UIView + + if let reusableHeaderView = tableView.headerView(forSection: section) { + headerView = reusableHeaderView + } else { + headerView = UIView( + frame: CGRect( + x: 0, y: 0, width: tableView.bounds.width, height: sectionHeaderHeight + ) + ) + } + + let label = UILabel() + label.text = dataSet[section].headingTitle + label.font = UIFont.boldSystemFont(ofSize: fontSize * 1.1) + label.textColor = keyCharColor + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + label.adjustsFontSizeToFitWidth = true + label.minimumScaleFactor = 0.8 + label.textAlignment = .natural + + label.translatesAutoresizingMaskIntoConstraints = false + headerView.addSubview(label) + + let horizontalPadding: CGFloat = 8 + let verticalPadding: CGFloat = 4 + + NSLayoutConstraint.activate([ + label.leadingAnchor.constraint( + equalTo: headerView.leadingAnchor, constant: horizontalPadding + ), + label.trailingAnchor.constraint( + equalTo: headerView.trailingAnchor, constant: -horizontalPadding + ), + label.topAnchor.constraint(equalTo: headerView.topAnchor, constant: verticalPadding), + label.bottomAnchor.constraint( + equalTo: headerView.bottomAnchor, constant: -verticalPadding + ) + ]) + + return headerView + } + + override func tableView(_: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + let label = UILabel() + label.text = dataSet[section].headingTitle + label.font = UIFont.boldSystemFont(ofSize: fontSize * 1.1) + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + + let horizontalPadding: CGFloat = 32 + let verticalPadding: CGFloat = 24 + + let constrainedWidth = tableView.bounds.width - horizontalPadding + let size = label.sizeThatFits( + CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude) + ) + + return size.height + verticalPadding + } } diff --git a/Scribe/Views/Cells/InfoChildTableViewCell/InfoChildTableViewCell.swift b/Scribe/Views/Cells/InfoChildTableViewCell/InfoChildTableViewCell.swift index 541dba9b..5af6de00 100644 --- a/Scribe/Views/Cells/InfoChildTableViewCell/InfoChildTableViewCell.swift +++ b/Scribe/Views/Cells/InfoChildTableViewCell/InfoChildTableViewCell.swift @@ -1,219 +1,224 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Class for a setting component with a heading, description and switch. */ import UIKit final class InfoChildTableViewCell: UITableViewCell { - - // MARK: Constants - - static let reuseIdentifier = String(describing: InfoChildTableViewCell.self) - - // MARK: Properties - - @IBOutlet var titleLabelPhone: UILabel! - @IBOutlet var titleLabelPad: UILabel! - var titleLabel: UILabel! - - @IBOutlet var subLabelPhone: UILabel! - @IBOutlet var subLabelPad: UILabel! - var subLabel: UILabel! - - @IBOutlet var iconImageViewPhone: UIImageView! - @IBOutlet var iconImageViewPad: UIImageView! - var iconImageView: UIImageView! - - @IBOutlet var toggleSwitchPhone: UISwitch! - @IBOutlet var toggleSwitchPad: UISwitch! - var toggleSwitch: UISwitch! - - @IBOutlet var descriptionLabelPhone: UILabel! - @IBOutlet var descriptionLabelPad: UILabel! - var descriptionLabel: UILabel! - - var section: Section? - var parentSection: Section? - - func setTableView() { - if DeviceType.isPad { - titleLabel = titleLabelPad - subLabel = subLabelPad - iconImageView = iconImageViewPad - toggleSwitch = toggleSwitchPad - descriptionLabel = descriptionLabelPad - - titleLabelPhone.removeFromSuperview() - subLabelPhone.removeFromSuperview() - iconImageViewPhone.removeFromSuperview() - toggleSwitchPhone.removeFromSuperview() - descriptionLabelPhone.removeFromSuperview() - } else { - titleLabel = titleLabelPhone - subLabel = subLabelPhone - iconImageView = iconImageViewPhone - toggleSwitch = toggleSwitchPhone - descriptionLabel = descriptionLabelPhone - - titleLabelPad.removeFromSuperview() - subLabelPad.removeFromSuperview() - iconImageViewPad.removeFromSuperview() - toggleSwitchPad.removeFromSuperview() - descriptionLabelPad.removeFromSuperview() + // MARK: Constants + + static let reuseIdentifier = String(describing: InfoChildTableViewCell.self) + + // MARK: Properties + + @IBOutlet var titleLabelPhone: UILabel! + @IBOutlet var titleLabelPad: UILabel! + var titleLabel: UILabel! + + @IBOutlet var subLabelPhone: UILabel! + @IBOutlet var subLabelPad: UILabel! + var subLabel: UILabel! + + @IBOutlet var iconImageViewPhone: UIImageView! + @IBOutlet var iconImageViewPad: UIImageView! + var iconImageView: UIImageView! + + @IBOutlet var toggleSwitchPhone: UISwitch! + @IBOutlet var toggleSwitchPad: UISwitch! + var toggleSwitch: UISwitch! + + @IBOutlet var descriptionLabelPhone: UILabel! + @IBOutlet var descriptionLabelPad: UILabel! + var descriptionLabel: UILabel! + + var section: Section? + var parentSection: Section? + + func setTableView() { + if DeviceType.isPad { + titleLabel = titleLabelPad + subLabel = subLabelPad + iconImageView = iconImageViewPad + toggleSwitch = toggleSwitchPad + descriptionLabel = descriptionLabelPad + + titleLabelPhone.removeFromSuperview() + subLabelPhone.removeFromSuperview() + iconImageViewPhone.removeFromSuperview() + toggleSwitchPhone.removeFromSuperview() + descriptionLabelPhone.removeFromSuperview() + } else { + titleLabel = titleLabelPhone + subLabel = subLabelPhone + iconImageView = iconImageViewPhone + toggleSwitch = toggleSwitchPhone + descriptionLabel = descriptionLabelPhone + + titleLabelPad.removeFromSuperview() + subLabelPad.removeFromSuperview() + iconImageViewPad.removeFromSuperview() + toggleSwitchPad.removeFromSuperview() + descriptionLabelPad.removeFromSuperview() + } } - } - - let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! - - var languageCode: String { - guard let parentSection = parentSection, - case let .specificLang(lang) = parentSection.sectionState else { return "" } - - return lang - } - - var togglePurpose: UserInteractiveState { - guard let section = section, - case let .none(action) = section.sectionState else { return .none } - return action - } + let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! - // MARK: Functions + var languageCode: String { + guard let parentSection = parentSection, + case let .specificLang(lang) = parentSection.sectionState + else { return "" } - func configureCell(for section: Section) { - self.section = section - selectionStyle = .none - - setTableView() - titleLabel.font = UIFont.systemFont(ofSize: fontSize, weight: .medium) - descriptionLabel.font = UIFont.systemFont(ofSize: fontSize * 0.9) - titleLabel.text = section.sectionTitle - - if let shortDescription = section.shortDescription { - descriptionLabel.text = shortDescription - descriptionLabel.isHidden = false - } else { - descriptionLabel.text = nil - descriptionLabel.isHidden = true + return lang } - if section.hasToggle { - accessoryType = .none - toggleSwitch.isHidden = false + var togglePurpose: UserInteractiveState { + guard let section = section, + case let .none(action) = section.sectionState + else { return .none } - fetchSwitchStateForCell() - - toggleSwitch.onTintColor = .init(ScribeColor.scribeCTA).withAlphaComponent(0.4) - toggleSwitch.thumbTintColor = toggleSwitch.isOn ? .init(.scribeCTA) : .lightGray - } else { - iconImageView.image = UIImage( - systemName: preferredLanguage.prefix(2) == "ar" ? "chevron.left": "chevron.right" - ) - iconImageView.tintColor = menuOptionColor - toggleSwitch.isHidden = true + return action } - if section.sectionState == .translateLang { - var langTranslateLanguage = "English" - if let selectedLang = userDefaults.string(forKey: languageCode + "TranslateLanguage") { - langTranslateLanguage = getKeyInDict(givenValue: selectedLang, dict: languagesAbbrDict) - } else { - userDefaults.set("en", forKey: languageCode + "TranslateLanguage") - } - let currentLang = "i18n.app._global." + langTranslateLanguage.lowercased() - subLabel.text = NSLocalizedString(currentLang, value: langTranslateLanguage, comment: "") - subLabel.textColor = menuOptionColor - } else { - subLabel.removeFromSuperview() + // MARK: Functions + + func configureCell(for section: Section) { + self.section = section + selectionStyle = .none + + setTableView() + titleLabel.font = UIFont.systemFont(ofSize: fontSize, weight: .medium) + descriptionLabel.font = UIFont.systemFont(ofSize: fontSize * 0.9) + titleLabel.text = section.sectionTitle + + if let shortDescription = section.shortDescription { + descriptionLabel.text = shortDescription + descriptionLabel.isHidden = false + } else { + descriptionLabel.text = nil + descriptionLabel.isHidden = true + } + + if section.hasToggle { + accessoryType = .none + toggleSwitch.isHidden = false + + fetchSwitchStateForCell() + + toggleSwitch.onTintColor = .init(ScribeColor.scribeCTA).withAlphaComponent(0.4) + toggleSwitch.thumbTintColor = toggleSwitch.isOn ? .init(.scribeCTA) : .lightGray + } else { + iconImageView.image = UIImage( + systemName: preferredLanguage.prefix(2) == "ar" ? "chevron.left" : "chevron.right" + ) + iconImageView.tintColor = menuOptionColor + toggleSwitch.isHidden = true + } + + if section.sectionState == .translateLang { + var langTranslateLanguage = "English" + if let selectedLang = userDefaults.string(forKey: languageCode + "TranslateLanguage") { + langTranslateLanguage = getKeyInDict( + givenValue: selectedLang, dict: languagesAbbrDict + ) + } else { + userDefaults.set("en", forKey: languageCode + "TranslateLanguage") + } + let currentLang = "i18n.app._global." + langTranslateLanguage.lowercased() + subLabel.text = NSLocalizedString( + currentLang, value: langTranslateLanguage, comment: "" + ) + subLabel.textColor = menuOptionColor + } else { + subLabel.removeFromSuperview() + } } - } - @IBAction func switchDidChange(_: UISwitch) { - switch togglePurpose { - case .toggleCommaAndPeriod: - let dictionaryKey = languageCode + "CommaAndPeriod" - userDefaults.setValue(toggleSwitch.isOn, forKey: dictionaryKey) + @IBAction func switchDidChange(_: UISwitch) { + switch togglePurpose { + case .toggleCommaAndPeriod: + let dictionaryKey = languageCode + "CommaAndPeriod" + userDefaults.setValue(toggleSwitch.isOn, forKey: dictionaryKey) + + case .toggleAccentCharacters: + let dictionaryKey = languageCode + "AccentCharacters" + userDefaults.setValue(toggleSwitch.isOn, forKey: dictionaryKey) - case .toggleAccentCharacters: - let dictionaryKey = languageCode + "AccentCharacters" - userDefaults.setValue(toggleSwitch.isOn, forKey: dictionaryKey) + case .doubleSpacePeriods: + let dictionaryKey = languageCode + "DoubleSpacePeriods" + userDefaults.setValue(toggleSwitch.isOn, forKey: dictionaryKey) - case .doubleSpacePeriods: - let dictionaryKey = languageCode + "DoubleSpacePeriods" - userDefaults.setValue(toggleSwitch.isOn, forKey: dictionaryKey) + case .autosuggestEmojis: + let dictionaryKey = languageCode + "EmojiAutosuggest" + userDefaults.setValue(toggleSwitch.isOn, forKey: dictionaryKey) - case .autosuggestEmojis: - let dictionaryKey = languageCode + "EmojiAutosuggest" - userDefaults.setValue(toggleSwitch.isOn, forKey: dictionaryKey) + case .toggleWordForWordDeletion: + let dictionaryKey = languageCode + "WordForWordDeletion" + userDefaults.setValue(toggleSwitch.isOn, forKey: dictionaryKey) - case .toggleWordForWordDeletion: - let dictionaryKey = languageCode + "WordForWordDeletion" - userDefaults.setValue(toggleSwitch.isOn, forKey: dictionaryKey) + case .increaseTextSize: + userDefaults.setValue(toggleSwitch.isOn, forKey: "increaseTextSize") + initializeFontSize() + NotificationCenter.default.post(name: .fontSizeUpdatedNotification, object: nil) - case .increaseTextSize: - userDefaults.setValue(toggleSwitch.isOn, forKey: "increaseTextSize") - initializeFontSize() - NotificationCenter.default.post(name: .fontSizeUpdatedNotification, object: nil) + case .none: break + } - case .none: break + toggleSwitch.thumbTintColor = toggleSwitch.isOn ? .init(.scribeCTA) : .lightGray } - toggleSwitch.thumbTintColor = toggleSwitch.isOn ? .init(.scribeCTA) : .lightGray - } - - func fetchSwitchStateForCell() { - switch togglePurpose { - case .toggleCommaAndPeriod: - let dictionaryKey = languageCode + "CommaAndPeriod" - if let toggleValue = userDefaults.object(forKey: dictionaryKey) as? Bool { - toggleSwitch.isOn = toggleValue - } else { - toggleSwitch.isOn = false // Default off - } - - case .toggleAccentCharacters: - let dictionaryKey = languageCode + "AccentCharacters" - if let toggleValue = userDefaults.object(forKey: dictionaryKey) as? Bool { - toggleSwitch.isOn = toggleValue - } else { - toggleSwitch.isOn = false // Default off - } - - case .doubleSpacePeriods: - let dictionaryKey = languageCode + "DoubleSpacePeriods" - if let toggleValue = userDefaults.object(forKey: dictionaryKey) as? Bool { - toggleSwitch.isOn = toggleValue - } else { - toggleSwitch.isOn = true // Default on - } - - case .autosuggestEmojis: - let dictionaryKey = languageCode + "EmojiAutosuggest" - if let toggleValue = userDefaults.object(forKey: dictionaryKey) as? Bool { - toggleSwitch.isOn = toggleValue - } else { - toggleSwitch.isOn = true // Default on - } - - case .toggleWordForWordDeletion: - let dictionaryKey = languageCode + "WordForWordDeletion" - if let toggleValue = userDefaults.object(forKey: dictionaryKey) as? Bool { - toggleSwitch.isOn = toggleValue - } else { - toggleSwitch.isOn = false // default off - } - - case .increaseTextSize: - if let toggleValue = userDefaults.object(forKey: "increaseTextSize") as? Bool { - toggleSwitch.isOn = toggleValue - } else { - toggleSwitch.isOn = false // default off - } - - case .none: break + func fetchSwitchStateForCell() { + switch togglePurpose { + case .toggleCommaAndPeriod: + let dictionaryKey = languageCode + "CommaAndPeriod" + if let toggleValue = userDefaults.object(forKey: dictionaryKey) as? Bool { + toggleSwitch.isOn = toggleValue + } else { + toggleSwitch.isOn = false // Default off + } + + case .toggleAccentCharacters: + let dictionaryKey = languageCode + "AccentCharacters" + if let toggleValue = userDefaults.object(forKey: dictionaryKey) as? Bool { + toggleSwitch.isOn = toggleValue + } else { + toggleSwitch.isOn = false // Default off + } + + case .doubleSpacePeriods: + let dictionaryKey = languageCode + "DoubleSpacePeriods" + if let toggleValue = userDefaults.object(forKey: dictionaryKey) as? Bool { + toggleSwitch.isOn = toggleValue + } else { + toggleSwitch.isOn = true // Default on + } + + case .autosuggestEmojis: + let dictionaryKey = languageCode + "EmojiAutosuggest" + if let toggleValue = userDefaults.object(forKey: dictionaryKey) as? Bool { + toggleSwitch.isOn = toggleValue + } else { + toggleSwitch.isOn = true // Default on + } + + case .toggleWordForWordDeletion: + let dictionaryKey = languageCode + "WordForWordDeletion" + if let toggleValue = userDefaults.object(forKey: dictionaryKey) as? Bool { + toggleSwitch.isOn = toggleValue + } else { + toggleSwitch.isOn = false // default off + } + + case .increaseTextSize: + if let toggleValue = userDefaults.object(forKey: "increaseTextSize") as? Bool { + toggleSwitch.isOn = toggleValue + } else { + toggleSwitch.isOn = false // default off + } + + case .none: break + } } - } } diff --git a/Scribe/Views/Cells/InfoChildTableViewCell/WrapperCell.swift b/Scribe/Views/Cells/InfoChildTableViewCell/WrapperCell.swift index 4cb59354..0ce46c68 100644 --- a/Scribe/Views/Cells/InfoChildTableViewCell/WrapperCell.swift +++ b/Scribe/Views/Cells/InfoChildTableViewCell/WrapperCell.swift @@ -3,117 +3,128 @@ import UIKit protocol RoundableCell { - func applyCornerRadius(corners: CACornerMask, radius: CGFloat) - func removeCornerRadius() + func applyCornerRadius(corners: CACornerMask, radius: CGFloat) + func removeCornerRadius() } + /// Generic wrapper cell that can wrap any UITableViewCell with padding and corner radius support. class WrapperCell: UITableViewCell { - static let reuseIdentifier = "WrapperCell" - - private let containerView = UIView() - private(set) var wrappedCell: UITableViewCell? - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - setupViews() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupViews() { - backgroundColor = .clear - contentView.backgroundColor = .clear - selectionStyle = .none - - containerView.translatesAutoresizingMaskIntoConstraints = false - containerView.backgroundColor = lightWhiteDarkBlackColor - contentView.addSubview(containerView) - - NSLayoutConstraint.activate([ - containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), - containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), - containerView.topAnchor.constraint(equalTo: contentView.topAnchor), - containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) - ]) - } - - /// Configure with any cell loaded from XIB. - func configure(withCellNamed nibName: String, section: Section, parentSection: Section? = nil) { - wrappedCell?.removeFromSuperview() - - guard let cell = Bundle.main.loadNibNamed( - nibName, - owner: nil, - options: nil - )?.first as? UITableViewCell else { - fatalError("Failed to load \(nibName) from XIB") + static let reuseIdentifier = "WrapperCell" + + private let containerView = UIView() + private(set) var wrappedCell: UITableViewCell? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupViews() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupViews() { + backgroundColor = .clear + contentView.backgroundColor = .clear + selectionStyle = .none + + containerView.translatesAutoresizingMaskIntoConstraints = false + containerView.backgroundColor = lightWhiteDarkBlackColor + contentView.addSubview(containerView) + + NSLayoutConstraint.activate([ + containerView.leadingAnchor.constraint( + equalTo: contentView.leadingAnchor, constant: 16 + ), + containerView.trailingAnchor.constraint( + equalTo: contentView.trailingAnchor, constant: -16 + ), + containerView.topAnchor.constraint(equalTo: contentView.topAnchor), + containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) + } + + /// Configure with any cell loaded from XIB. + func configure(withCellNamed nibName: String, section: Section, parentSection: Section? = nil) { + wrappedCell?.removeFromSuperview() + + guard + let cell = Bundle.main.loadNibNamed( + nibName, + owner: nil, + options: nil + )?.first as? UITableViewCell + else { + fatalError("Failed to load \(nibName) from XIB") + } + + if let infoCell = cell as? InfoChildTableViewCell { + infoCell.parentSection = parentSection + infoCell.configureCell(for: section) + } else if let aboutCell = cell as? AboutTableViewCell { + aboutCell.configureCell(for: section) + } + + cell.backgroundColor = .clear + cell.translatesAutoresizingMaskIntoConstraints = false + + containerView.addSubview(cell) + + NSLayoutConstraint.activate([ + cell.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + cell.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + cell.topAnchor.constraint(equalTo: containerView.topAnchor), + cell.bottomAnchor.constraint(equalTo: containerView.bottomAnchor) + ]) + + wrappedCell = cell + } + + func applyCornerRadius(corners: CACornerMask, radius: CGFloat) { + containerView.layer.maskedCorners = corners + containerView.layer.cornerRadius = radius + containerView.layer.masksToBounds = true } - if let infoCell = cell as? InfoChildTableViewCell { - infoCell.parentSection = parentSection - infoCell.configureCell(for: section) - } else if let aboutCell = cell as? AboutTableViewCell { - aboutCell.configureCell(for: section) + func removeCornerRadius() { + containerView.layer.cornerRadius = 0 + containerView.layer.masksToBounds = false } - cell.backgroundColor = .clear - cell.translatesAutoresizingMaskIntoConstraints = false - - containerView.addSubview(cell) - - NSLayoutConstraint.activate([ - cell.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - cell.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), - cell.topAnchor.constraint(equalTo: containerView.topAnchor), - cell.bottomAnchor.constraint(equalTo: containerView.bottomAnchor) - ]) - - wrappedCell = cell - } - - func applyCornerRadius(corners: CACornerMask, radius: CGFloat) { - containerView.layer.maskedCorners = corners - containerView.layer.cornerRadius = radius - containerView.layer.masksToBounds = true - } - - func removeCornerRadius() { - containerView.layer.cornerRadius = 0 - containerView.layer.masksToBounds = false - } - - // MARK: Static Helper - - /// Apply corner radius to a cell based on its position in the section. - static func applyCornerRadius( - to cell: UITableViewCell, - isFirst: Bool, - isLast: Bool, - radius: CGFloat = 12 - ) { - guard let roundableCell = cell as? RoundableCell else { return } - - if isFirst && isLast { - roundableCell.applyCornerRadius( - corners: [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner], - radius: radius - ) - } else if isFirst { - roundableCell.applyCornerRadius( - corners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], - radius: radius - ) - } else if isLast { - roundableCell.applyCornerRadius( - corners: [.layerMinXMaxYCorner, .layerMaxXMaxYCorner], - radius: radius - ) - } else { - roundableCell.removeCornerRadius() + // MARK: Static Helper + + /// Apply corner radius to a cell based on its position in the section. + static func applyCornerRadius( + to cell: UITableViewCell, + isFirst: Bool, + isLast: Bool, + radius: CGFloat = 12 + ) { + guard let roundableCell = cell as? RoundableCell else { return } + + if isFirst, isLast { + roundableCell.applyCornerRadius( + corners: [ + .layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, + .layerMaxXMaxYCorner + ], + radius: radius + ) + } else if isFirst { + roundableCell.applyCornerRadius( + corners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], + radius: radius + ) + } else if isLast { + roundableCell.applyCornerRadius( + corners: [.layerMinXMaxYCorner, .layerMaxXMaxYCorner], + radius: radius + ) + } else { + roundableCell.removeCornerRadius() + } } - } } extension WrapperCell: RoundableCell {} diff --git a/Scribe/Views/Cells/RadioTableViewCell/RadioTableViewCell.swift b/Scribe/Views/Cells/RadioTableViewCell/RadioTableViewCell.swift index cc335389..9b9a98c5 100644 --- a/Scribe/Views/Cells/RadioTableViewCell/RadioTableViewCell.swift +++ b/Scribe/Views/Cells/RadioTableViewCell/RadioTableViewCell.swift @@ -1,69 +1,70 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * DESCRIPTION_OF_THE_PURPOSE_OF_THE_FILE */ import UIKit final class RadioTableViewCell: UITableViewCell { + // MARK: Constants - // MARK: Constants + static let reuseIdentifier = String(describing: InfoChildTableViewCell.self) - static let reuseIdentifier = String(describing: InfoChildTableViewCell.self) + // MARK: Properties - // MARK: Properties + @IBOutlet var titleLabelPhone: UILabel! + @IBOutlet var titleLabelPad: UILabel! + var titleLabel: UILabel! - @IBOutlet var titleLabelPhone: UILabel! - @IBOutlet var titleLabelPad: UILabel! - var titleLabel: UILabel! + @IBOutlet var iconImageViewPhone: UIImageView! + @IBOutlet var iconImageViewPad: UIImageView! + var iconImageView: UIImageView! - @IBOutlet var iconImageViewPhone: UIImageView! - @IBOutlet var iconImageViewPad: UIImageView! - var iconImageView: UIImageView! + var section: Section? + var parentSection: Section? + var inUse: Bool = false - var section: Section? - var parentSection: Section? - var inUse: Bool = false + func setTableView() { + if DeviceType.isPad { + titleLabel = titleLabelPad + iconImageView = iconImageViewPad - func setTableView() { - if DeviceType.isPad { - titleLabel = titleLabelPad - iconImageView = iconImageViewPad + titleLabelPhone.removeFromSuperview() + iconImageViewPhone.removeFromSuperview() + } else { + titleLabel = titleLabelPhone + iconImageView = iconImageViewPhone - titleLabelPhone.removeFromSuperview() - iconImageViewPhone.removeFromSuperview() - } else { - titleLabel = titleLabelPhone - iconImageView = iconImageViewPhone - - titleLabelPad.removeFromSuperview() - iconImageViewPad.removeFromSuperview() + titleLabelPad.removeFromSuperview() + iconImageViewPad.removeFromSuperview() + } } - } - var selectedLang: String { - guard let section = section, - case let .specificLang(lang) = section.sectionState else { return "n/a" } + var selectedLang: String { + guard let section = section, + case let .specificLang(lang) = section.sectionState + else { return "n/a" } - return lang - } + return lang + } - var togglePurpose: UserInteractiveState { - guard let section = section, - case let .none(action) = section.sectionState else { return .none } + var togglePurpose: UserInteractiveState { + guard let section = section, + case let .none(action) = section.sectionState + else { return .none } - return action - } + return action + } - // MARK: Functions + // MARK: Functions - func configureCell(for section: Section) { - self.section = section - selectionStyle = .none + func configureCell(for section: Section) { + self.section = section + selectionStyle = .none - setTableView() - titleLabel.text = section.sectionTitle - iconImageView.image = UIImage(named: "radioButton") - } + setTableView() + titleLabel.text = section.sectionTitle + iconImageView.image = UIImage(named: "radioButton") + } } diff --git a/Scribe/Views/Cells/UITableViewCells/AboutTableViewCell.swift b/Scribe/Views/Cells/UITableViewCells/AboutTableViewCell.swift index 0451e06d..5a29866d 100644 --- a/Scribe/Views/Cells/UITableViewCells/AboutTableViewCell.swift +++ b/Scribe/Views/Cells/UITableViewCells/AboutTableViewCell.swift @@ -1,95 +1,94 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Class for a button component with a label, icon and link icon. */ import UIKit final class AboutTableViewCell: UITableViewCell { + // MARK: Constants - // MARK: Constants + static let reuseIdentifier = String(describing: InfoChildTableViewCell.self) - static let reuseIdentifier = String(describing: InfoChildTableViewCell.self) + // MARK: Properties - // MARK: Properties + @IBOutlet var titleLabelPhone: UILabel! + @IBOutlet var titleLabelPad: UILabel! + var titleLabel: UILabel! - @IBOutlet var titleLabelPhone: UILabel! - @IBOutlet var titleLabelPad: UILabel! - var titleLabel: UILabel! + @IBOutlet var iconImageViewPhone: UIImageView! + @IBOutlet var iconImageViewPad: UIImageView! + var iconImageView: UIImageView! - @IBOutlet var iconImageViewPhone: UIImageView! - @IBOutlet var iconImageViewPad: UIImageView! - var iconImageView: UIImageView! + @IBOutlet var linkImageViewPhone: UIImageView! + @IBOutlet var linkImageViewPad: UIImageView! + var linkImageView: UIImageView! - @IBOutlet var linkImageViewPhone: UIImageView! - @IBOutlet var linkImageViewPad: UIImageView! - var linkImageView: UIImageView! + private var section: Section? + private var parentSection: Section? - private var section: Section? - private var parentSection: Section? + func setTableView() { + if DeviceType.isPad { + titleLabel = titleLabelPad + iconImageView = iconImageViewPad + linkImageView = linkImageViewPad - func setTableView() { - if DeviceType.isPad { - titleLabel = titleLabelPad - iconImageView = iconImageViewPad - linkImageView = linkImageViewPad + titleLabelPhone.removeFromSuperview() + iconImageViewPhone.removeFromSuperview() + linkImageViewPhone.removeFromSuperview() + } else { + titleLabel = titleLabelPhone + iconImageView = iconImageViewPhone + linkImageView = linkImageViewPhone - titleLabelPhone.removeFromSuperview() - iconImageViewPhone.removeFromSuperview() - linkImageViewPhone.removeFromSuperview() - } else { - titleLabel = titleLabelPhone - iconImageView = iconImageViewPhone - linkImageView = linkImageViewPhone - - titleLabelPad.removeFromSuperview() - iconImageViewPad.removeFromSuperview() - linkImageViewPad.removeFromSuperview() - } - } - - // MARK: Functions - - func configureCell(for section: Section) { - selectionStyle = .none - - setTableView() - titleLabel.font = UIFont.systemFont(ofSize: fontSize, weight: .medium) - titleLabel.text = section.sectionTitle - - if let icon = section.imageString { - iconImageView.image = UIImage.availableIconImage(with: icon) - iconImageView.contentMode = .scaleAspectFit - } else { - iconImageView.image = nil - } - if let link = section.externalLink { - if link { - linkImageView.image = UIImage.availableIconImage( - with: preferredLanguage.prefix(2) == "ar" ? "externalLinkRTL" : "externalLink" - ) - } else { - linkImageView.image = nil - } - } else { - linkImageView.image = nil + titleLabelPad.removeFromSuperview() + iconImageViewPad.removeFromSuperview() + linkImageViewPad.removeFromSuperview() + } } - if section.hasNestedNavigation { - let resetIcon = UIImage(systemName: "arrow.circlepath") - let disclosureIcon = UIImage( - systemName: preferredLanguage.prefix(2) == "ar" ? "chevron.left": "chevron.right" - ) - let rightIcon = section.sectionState == .appHints ? resetIcon : disclosureIcon - let accessory = UIImageView( - frame: CGRect( - x: 0, y: 0, width: (rightIcon?.size.width)!, height: (rightIcon?.size.height)! - ) - ) - accessory.image = rightIcon - accessory.tintColor = menuOptionColor - accessoryView = accessory + // MARK: Functions + + func configureCell(for section: Section) { + selectionStyle = .none + + setTableView() + titleLabel.font = UIFont.systemFont(ofSize: fontSize, weight: .medium) + titleLabel.text = section.sectionTitle + + if let icon = section.imageString { + iconImageView.image = UIImage.availableIconImage(with: icon) + iconImageView.contentMode = .scaleAspectFit + } else { + iconImageView.image = nil + } + if let link = section.externalLink { + if link { + linkImageView.image = UIImage.availableIconImage( + with: preferredLanguage.prefix(2) == "ar" ? "externalLinkRTL" : "externalLink" + ) + } else { + linkImageView.image = nil + } + } else { + linkImageView.image = nil + } + + if section.hasNestedNavigation { + let resetIcon = UIImage(systemName: "arrow.circlepath") + let disclosureIcon = UIImage( + systemName: preferredLanguage.prefix(2) == "ar" ? "chevron.left" : "chevron.right" + ) + let rightIcon = section.sectionState == .appHints ? resetIcon : disclosureIcon + let accessory = UIImageView( + frame: CGRect( + x: 0, y: 0, width: (rightIcon?.size.width)!, height: (rightIcon?.size.height)! + ) + ) + accessory.image = rightIcon + accessory.tintColor = menuOptionColor + accessoryView = accessory + } } - } } diff --git a/Scribe/Views/SelectionViewTemplateViewController.swift b/Scribe/Views/SelectionViewTemplateViewController.swift index 8c7ac594..0113d282 100644 --- a/Scribe/Views/SelectionViewTemplateViewController.swift +++ b/Scribe/Views/SelectionViewTemplateViewController.swift @@ -1,178 +1,211 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * This file describes */ -import UIKit import SwiftUI +import UIKit final class SelectionViewTemplateViewController: BaseTableViewController { - // MARK: Properties - - override var dataSet: [ParentTableCellModel] { - tableData - } - - private var tableData: [ParentTableCellModel] = [] - private var parentSection: Section? - private var selectedPath: IndexPath? - - let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! - - private var langCode: String = "de" - @ObservedObject private var stateManager = DownloadStateManager.shared - - // MARK: Functions - - override func viewDidLoad() { - super.viewDidLoad() - edgesForExtendedLayout = .all - extendedLayoutIncludesOpaqueBars = true - tableView.register( - UINib(nibName: "RadioTableViewCell", bundle: nil), - forCellReuseIdentifier: RadioTableViewCell.reuseIdentifier - ) - } - - func configureTable(for tableData: [ParentTableCellModel], parentSection: Section, langCode: String) { - self.tableData = tableData - self.parentSection = parentSection - self.langCode = langCode - - title = NSLocalizedString( - "i18n.app.settings.keyboard.translation.select_source.title", - value: "Translation language", - comment: "" - ) - } -} + // MARK: Properties -// MARK: UITableViewDataSource + override var dataSet: [ParentTableCellModel] { + tableData + } -extension SelectionViewTemplateViewController { - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell( - withIdentifier: RadioTableViewCell.reuseIdentifier, - for: indexPath - ) as? RadioTableViewCell else { - fatalError("Failed to dequeue RadioTableViewCell.") + private var tableData: [ParentTableCellModel] = [] + private var parentSection: Section? + private var selectedPath: IndexPath? + + let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! + + private var langCode: String = "de" + @ObservedObject private var stateManager = DownloadStateManager.shared + + // MARK: Functions + + override func viewDidLoad() { + super.viewDidLoad() + edgesForExtendedLayout = .all + extendedLayoutIncludesOpaqueBars = true + tableView.register( + UINib(nibName: "RadioTableViewCell", bundle: nil), + forCellReuseIdentifier: RadioTableViewCell.reuseIdentifier + ) } - cell.parentSection = parentSection - cell.configureCell(for: tableData[indexPath.section].section[indexPath.row]) - cell.backgroundColor = lightWhiteDarkBlackColor - if cell.selectedLang == userDefaults.string(forKey: langCode + "TranslateLanguage") { - selectedPath = indexPath - cell.iconImageView.image = UIImage(named: "radioButtonSelected") + + func configureTable( + for tableData: [ParentTableCellModel], parentSection: Section, langCode: String + ) { + self.tableData = tableData + self.parentSection = parentSection + self.langCode = langCode + + title = NSLocalizedString( + "i18n.app.settings.keyboard.translation.select_source.title", + value: "Translation language", + comment: "" + ) } +} - return cell - } +// MARK: UITableViewDataSource - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let cell = tableView.cellForRow(at: indexPath) as! RadioTableViewCell - let oldLang = userDefaults.string(forKey: langCode + "TranslateLanguage") ?? "en" - let newLang = cell.selectedLang ?? "en" +extension SelectionViewTemplateViewController { + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) + -> UITableViewCell { + guard + let cell = tableView.dequeueReusableCell( + withIdentifier: RadioTableViewCell.reuseIdentifier, + for: indexPath + ) as? RadioTableViewCell + else { + fatalError("Failed to dequeue RadioTableViewCell.") + } + cell.parentSection = parentSection + cell.configureCell(for: tableData[indexPath.section].section[indexPath.row]) + cell.backgroundColor = lightWhiteDarkBlackColor + if cell.selectedLang == userDefaults.string(forKey: langCode + "TranslateLanguage") { + selectedPath = indexPath + cell.iconImageView.image = UIImage(named: "radioButtonSelected") + } - if let selectedIndexPath = tableView.indexPathForSelectedRow { - tableView.deselectRow(at: selectedIndexPath, animated: true) + return cell } - // Only show popup if selecting a different language. - if newLang != oldLang { - let oldIndexPath = selectedPath - updateRadioButton(to: indexPath, in: tableView) - showPopup(oldLang: oldLang, newLang: newLang, oldIndexPath: oldIndexPath, newIndexPath: indexPath, tableView: tableView) - } - } + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let cell = tableView.cellForRow(at: indexPath) as! RadioTableViewCell + let oldLang = userDefaults.string(forKey: langCode + "TranslateLanguage") ?? "en" + let newLang = cell.selectedLang ?? "en" - private func updateRadioButton(to indexPath: IndexPath, in tableView: UITableView) { - if selectedPath != nil { - let previousCell = tableView.cellForRow(at: selectedPath!) as! RadioTableViewCell - previousCell.iconImageView.image = UIImage(named: "radioButton") + if let selectedIndexPath = tableView.indexPathForSelectedRow { + tableView.deselectRow(at: selectedIndexPath, animated: true) + } + + // Only show popup if selecting a different language. + if newLang != oldLang { + let oldIndexPath = selectedPath + updateRadioButton(to: indexPath, in: tableView) + showPopup( + oldLang: oldLang, newLang: newLang, oldIndexPath: oldIndexPath, + newIndexPath: indexPath, + tableView: tableView + ) + } } - let cell = tableView.cellForRow(at: indexPath) as! RadioTableViewCell - cell.iconImageView.image = UIImage(named: "radioButtonSelected") - selectedPath = indexPath - } + private func updateRadioButton(to indexPath: IndexPath, in tableView: UITableView) { + if selectedPath != nil { + let previousCell = tableView.cellForRow(at: selectedPath!) as! RadioTableViewCell + previousCell.iconImageView.image = UIImage(named: "radioButton") + } - private func showPopup(oldLang: String, newLang: String, oldIndexPath: IndexPath?, newIndexPath: IndexPath, tableView: UITableView) { - let oldSourceLanguage = getKeyInDict(givenValue: oldLang, dict: languagesAbbrDict) - let newSourceLanguage = getKeyInDict(givenValue: newLang, dict: languagesAbbrDict) + let cell = tableView.cellForRow(at: indexPath) as! RadioTableViewCell + cell.iconImageView.image = UIImage(named: "radioButtonSelected") + selectedPath = indexPath + } - let infoText = NSLocalizedString("i18n.app.settings.keyboard.translation.change_source_tooltip.download_warning", value: "You've changed your source translation language. Would you like to download new data so that you can translate from {source_language}?", comment: "") + private func showPopup( + oldLang: String, newLang: String, oldIndexPath: IndexPath?, newIndexPath _: IndexPath, + tableView: UITableView + ) { + let oldSourceLanguage = getKeyInDict(givenValue: oldLang, dict: languagesAbbrDict) + let newSourceLanguage = getKeyInDict(givenValue: newLang, dict: languagesAbbrDict) + + let infoText = NSLocalizedString( + "i18n.app.settings.keyboard.translation.change_source_tooltip.download_warning", + value: + "You've changed your source translation language. Would you like to download new data so that you can translate from {source_language}?", + comment: "" + ) - let changeButtonText = NSLocalizedString("i18n.app.settings.keyboard.translation.change_source_tooltip.keep_source_language", value: "Keep {source_language}", comment: "") + let changeButtonText = NSLocalizedString( + "i18n.app.settings.keyboard.translation.change_source_tooltip.keep_source_language", + value: "Keep {source_language}", comment: "" + ) - let confirmButtonText = NSLocalizedString("i18n.app._global.download_data", value: "Download data", comment: "") + let confirmButtonText = NSLocalizedString( + "i18n.app._global.download_data", value: "Download data", comment: "" + ) - let localizedOldSourceLanguage = NSLocalizedString( - "i18n.app._global." + oldSourceLanguage.lowercased(), - value: oldSourceLanguage, - comment: "" + let localizedOldSourceLanguage = NSLocalizedString( + "i18n.app._global." + oldSourceLanguage.lowercased(), + value: oldSourceLanguage, + comment: "" ) - let localizedNewSourceLanguage = NSLocalizedString( - "i18n.app._global." + newSourceLanguage.lowercased(), - value: newSourceLanguage, - comment: "" + let localizedNewSourceLanguage = NSLocalizedString( + "i18n.app._global." + newSourceLanguage.lowercased(), + value: newSourceLanguage, + comment: "" ) - func onKeep() { - // Keep old language - revert and dismiss. - self.dismiss(animated: true) { - if let oldPath = oldIndexPath { - self.updateRadioButton(to: oldPath, in: tableView) + func onKeep() { + // Keep old language - revert and dismiss. + dismiss(animated: true) { + if let oldPath = oldIndexPath { + self.updateRadioButton(to: oldPath, in: tableView) + } } } - } - - func confirmDownload() { - self.dismiss(animated: true) { - let dictionaryKey = self.langCode + "TranslateLanguage" - self.userDefaults.setValue(newLang, forKey: dictionaryKey) - - // Trigger download state change. - DownloadStateManager.shared.handleDownloadAction(key: self.langCode, forceDownload: true) - guard let tabBarController = self.tabBarController else { return } + func confirmDownload() { + dismiss(animated: true) { + let dictionaryKey = self.langCode + "TranslateLanguage" + self.userDefaults.setValue(newLang, forKey: dictionaryKey) + + // Trigger download state change. + DownloadStateManager.shared.handleDownloadAction( + key: self.langCode, forceDownload: true + ) + + guard let tabBarController = self.tabBarController else { return } + + guard + let installationNavController = tabBarController.viewControllers?[0] + as? UINavigationController + else { return } + + // Check if download screen exists in nav stack. + if let downloadScreen = installationNavController.viewControllers.first(where: { + $0 is UIHostingController + }) { + // Download screen found - popping to it. + tabBarController.selectedIndex = 0 + installationNavController.popToViewController(downloadScreen, animated: true) + } else { + // Download screen not found - creating new one. + tabBarController.selectedIndex = 0 + installationNavController.popToRootViewController(animated: false) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + NotificationCenter.default.post( + name: NSNotification.Name("NavigateToDownloadScreen"), + object: nil + ) + } + } + } + } - guard let installationNavController = tabBarController.viewControllers?[0] as? UINavigationController else { return } + let popupView = ConfirmTranslationSource( + infoText: infoText.replacingOccurrences( + of: "{source_language}", with: localizedNewSourceLanguage + ).replacingOccurrences(of: "{source_language}", with: localizedNewSourceLanguage), + changeButtonText: changeButtonText.replacingOccurrences( + of: "{source_language}", with: localizedOldSourceLanguage + ), + confirmButtonText: confirmButtonText, + onDismiss: { onKeep() }, + onChange: { onKeep() }, + onConfirm: { confirmDownload() } + ) - // Check if download screen exists in nav stack. - if let downloadScreen = installationNavController.viewControllers.first(where: { $0 is UIHostingController }) { - // Download screen found - popping to it. - tabBarController.selectedIndex = 0 - installationNavController.popToViewController(downloadScreen, animated: true) - } else { - // Download screen not found - creating new one. - tabBarController.selectedIndex = 0 - installationNavController.popToRootViewController(animated: false) + let hostingController = UIHostingController(rootView: popupView) + hostingController.modalPresentationStyle = .overFullScreen + hostingController.modalTransitionStyle = .crossDissolve + hostingController.view.backgroundColor = .clear - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - NotificationCenter.default.post( - name: NSNotification.Name("NavigateToDownloadScreen"), - object: nil - ) - } - } - } + present(hostingController, animated: true) } - - let popupView = ConfirmTranslationSource( - infoText: infoText.replacingOccurrences(of: "{source_language}", with: localizedNewSourceLanguage).replacingOccurrences(of: "{source_language}", with: localizedNewSourceLanguage), - changeButtonText: changeButtonText.replacingOccurrences(of: "{source_language}", with: localizedOldSourceLanguage), - confirmButtonText: confirmButtonText, - onDismiss: { onKeep() }, - onChange: { onKeep()}, - onConfirm: { confirmDownload() } - ) - - let hostingController = UIHostingController(rootView: popupView) - hostingController.modalPresentationStyle = .overFullScreen - hostingController.modalTransitionStyle = .crossDissolve - hostingController.view.backgroundColor = .clear - - present(hostingController, animated: true) - } } diff --git a/Scribe/Views/TableViewTemplateViewController.swift b/Scribe/Views/TableViewTemplateViewController.swift index 1ed52602..e2c50b24 100644 --- a/Scribe/Views/TableViewTemplateViewController.swift +++ b/Scribe/Views/TableViewTemplateViewController.swift @@ -1,167 +1,185 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Controls the table views within the app. */ import UIKit final class TableViewTemplateViewController: BaseTableViewController { - // MARK: Properties + // MARK: Properties - override var dataSet: [ParentTableCellModel] { - tableData - } - - private var tableData: [ParentTableCellModel] = [] - private var parentSection: Section? - private let cornerRadius: CGFloat = 12 - - let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! - - private var langCode: String { - guard let parentSection else { - return "" + override var dataSet: [ParentTableCellModel] { + tableData } - guard case let .specificLang(lang) = parentSection.sectionState else { - return "de" - } + private var tableData: [ParentTableCellModel] = [] + private var parentSection: Section? + private let cornerRadius: CGFloat = 12 - return lang - } + let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! - // MARK: Functions + private var langCode: String { + guard let parentSection + else { + return "" + } - override func viewDidLoad() { - super.viewDidLoad() + guard case let .specificLang(lang) = parentSection.sectionState + else { + return "de" + } - tableView.register( - WrapperCell.self, - forCellReuseIdentifier: WrapperCell.reuseIdentifier - ) + return lang + } - tableView.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = 250 - tableView.separatorStyle = .none - } + // MARK: Functions - func configureTable(for tableData: [ParentTableCellModel], parentSection: Section) { - self.tableData = tableData - self.parentSection = parentSection + override func viewDidLoad() { + super.viewDidLoad() - title = parentSection.sectionTitle - } + tableView.register( + WrapperCell.self, + forCellReuseIdentifier: WrapperCell.reuseIdentifier + ) - // Refreshes to check for changes when a translation language is selected. - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) + tableView.rowHeight = UITableView.automaticDimension + tableView.estimatedRowHeight = 250 + tableView.separatorStyle = .none + } - for visibleCell in tableView.visibleCells { - // Cast to wrapper cell first. - guard let wrapperCell = visibleCell as? WrapperCell, - let innerCell = wrapperCell.wrappedCell as? InfoChildTableViewCell else { - continue - } + func configureTable(for tableData: [ParentTableCellModel], parentSection: Section) { + self.tableData = tableData + self.parentSection = parentSection - // Now check if it's a translate lang section. - guard innerCell.section?.sectionState == .translateLang else { - continue - } + title = parentSection.sectionTitle + } - let langTranslateLanguage = getKeyInDict(givenValue: (userDefaults.string(forKey: langCode + "TranslateLanguage") ?? "en"), dict: languagesAbbrDict) - let currentLang = "i18n.app._global." + langTranslateLanguage.lowercased() - innerCell.subLabel.text = NSLocalizedString(currentLang, value: langTranslateLanguage, comment: "") + /// Refreshes to check for changes when a translation language is selected. + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + for visibleCell in tableView.visibleCells { + // Cast to wrapper cell first. + guard let wrapperCell = visibleCell as? WrapperCell, + let innerCell = wrapperCell.wrappedCell as? InfoChildTableViewCell + else { + continue + } + + // Now check if it's a translate lang section. + guard innerCell.section?.sectionState == .translateLang + else { + continue + } + + let langTranslateLanguage = getKeyInDict( + givenValue: userDefaults.string(forKey: langCode + "TranslateLanguage") ?? "en", + dict: languagesAbbrDict + ) + let currentLang = "i18n.app._global." + langTranslateLanguage.lowercased() + innerCell.subLabel.text = NSLocalizedString( + currentLang, value: langTranslateLanguage, comment: "" + ) + } } - } } // MARK: UITableViewDataSource extension TableViewTemplateViewController { - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell( - withIdentifier: WrapperCell.reuseIdentifier, - for: indexPath - ) as? WrapperCell else { - fatalError("Failed to dequeue WrapperCell") + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) + -> UITableViewCell { + guard + let cell = tableView.dequeueReusableCell( + withIdentifier: WrapperCell.reuseIdentifier, + for: indexPath + ) as? WrapperCell + else { + fatalError("Failed to dequeue WrapperCell") + } + + let section = dataSet[indexPath.section] + let setting = section.section[indexPath.row] + + cell.configure( + withCellNamed: "InfoChildTableViewCell", section: setting, + parentSection: parentSection + ) + + let isFirstRow = indexPath.row == 0 + let isLastRow = indexPath.row == section.section.count - 1 + WrapperCell.applyCornerRadius(to: cell, isFirst: isFirstRow, isLast: isLastRow) + + return cell } - let section = dataSet[indexPath.section] - let setting = section.section[indexPath.row] + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) + -> CGFloat { + let section = dataSet[indexPath.section] + let setting = section.section[indexPath.row] - cell.configure(withCellNamed: "InfoChildTableViewCell", section: setting, parentSection: self.parentSection) + // If there's no description, return a fixed small height. + guard let description = setting.shortDescription + else { + return 54.0 + } - let isFirstRow = indexPath.row == 0 - let isLastRow = indexPath.row == section.section.count - 1 - WrapperCell.applyCornerRadius(to: cell, isFirst: isFirstRow, isLast: isLastRow) + // Calculate available width for text. + let availableWidth = tableView.bounds.width - 32 - return cell - } + let titleFont: UIFont + let descFont: UIFont - override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let section = dataSet[indexPath.section] - let setting = section.section[indexPath.row] + if DeviceType.isPad { + titleFont = UIFont.systemFont(ofSize: fontSize * 1.5, weight: .medium) + descFont = UIFont.systemFont(ofSize: fontSize * 1.1) + } else { + titleFont = UIFont.systemFont(ofSize: fontSize, weight: .medium) + descFont = UIFont.systemFont(ofSize: fontSize * 0.9) + } - // If there's no description, return a fixed small height. - guard let description = setting.shortDescription else { - return 54.0 + // Calculate actual height needed for title text. + let titleHeight = setting.sectionTitle.boundingRect( + with: CGSize(width: availableWidth, height: .greatestFiniteMagnitude), + options: .usesLineFragmentOrigin, + attributes: [.font: titleFont], + context: nil + ).height + + // Calculate actual height needed for description text. + let descHeight = description.boundingRect( + with: CGSize(width: availableWidth, height: .greatestFiniteMagnitude), + options: .usesLineFragmentOrigin, + attributes: [.font: descFont], + context: nil + ).height + + // Return total height: title + description + padding buffer. + return ceil(titleHeight + descHeight + 52) } - // Calculate available width for text. - let availableWidth = tableView.bounds.width - 32 + override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { + let tableSection = tableData[indexPath.section] + let section = tableSection.section[indexPath.row] - let titleFont: UIFont - let descFont: UIFont + if section.sectionState == .translateLang { + if let viewController = storyboard?.instantiateViewController( + identifier: "SelectionViewTemplateViewController" + ) as? SelectionViewTemplateViewController { + var data = SettingsTableData.translateLangSettingsData - if DeviceType.isPad { - titleFont = UIFont.systemFont(ofSize: fontSize * 1.5, weight: .medium) - descFont = UIFont.systemFont(ofSize: fontSize * 1.1) - } else { - titleFont = UIFont.systemFont(ofSize: fontSize, weight: .medium) - descFont = UIFont.systemFont(ofSize: fontSize * 0.9) - } + // Removes keyboard language from possible translation languages. + let langCodeIndex = + SettingsTableData.translateLangSettingsData[0].section.firstIndex(where: { s in + s.sectionState == .specificLang(langCode) + }) ?? -1 + data[0].section.remove(at: langCodeIndex) - // Calculate actual height needed for title text. - let titleHeight = setting.sectionTitle.boundingRect( - with: CGSize(width: availableWidth, height: .greatestFiniteMagnitude), - options: .usesLineFragmentOrigin, - attributes: [.font: titleFont], - context: nil - ).height - - // Calculate actual height needed for description text. - let descHeight = description.boundingRect( - with: CGSize(width: availableWidth, height: .greatestFiniteMagnitude), - options: .usesLineFragmentOrigin, - attributes: [.font: descFont], - context: nil - ).height - - // Return total height: title + description + padding buffer. - return ceil(titleHeight + descHeight + 52) - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let tableSection = tableData[indexPath.section] - let section = tableSection.section[indexPath.row] - - if section.sectionState == .translateLang { - if let viewController = storyboard?.instantiateViewController( - identifier: "SelectionViewTemplateViewController" - ) as? SelectionViewTemplateViewController { - var data = SettingsTableData.translateLangSettingsData - - // Removes keyboard language from possible translation languages. - let langCodeIndex = SettingsTableData.translateLangSettingsData[0].section.firstIndex(where: { s in - s.sectionState == .specificLang(langCode) - }) ?? -1 - data[0].section.remove(at: langCodeIndex) - - viewController.configureTable(for: data, parentSection: section, langCode: langCode) - - navigationController?.pushViewController(viewController, animated: true) - } + viewController.configureTable(for: data, parentSection: section, langCode: langCode) + + navigationController?.pushViewController(viewController, animated: true) + } + } } - } } diff --git a/Services/LanguageDataService.swift b/Services/LanguageDataService.swift index 6a3bb351..b4658d92 100644 --- a/Services/LanguageDataService.swift +++ b/Services/LanguageDataService.swift @@ -4,111 +4,126 @@ import Foundation import GRDB final class LanguageDataService { - static let shared = LanguageDataService() + static let shared = LanguageDataService() - private let apiClient = LanguageDataAPIClient.shared - private let userDefaults = UserDefaults.standard - private let lastUpdateKey = "last_update_" + private let apiClient = LanguageDataAPIClient.shared + private let userDefaults = UserDefaults.standard + private let lastUpdateKey = "last_update_" - private init() {} + private init() {} - // MARK: Download Data - func downloadData(language: String, forceDownload: Bool = false) async throws { - let localLastUpdate = getLastUpdate(for: language) ?? "1970-01-01" + // MARK: Download Data - // Fetch data from API. - let response = try await apiClient.fetchLanguageData(language: language) - let serverLastUpdate = response.contract.updatedAt + func downloadData(language: String, forceDownload: Bool = false) async throws { + let localLastUpdate = getLastUpdate(for: language) ?? "1970-01-01" - // Check if update is needed. - if forceDownload || isUpdateAvailable(local: localLastUpdate, server: serverLastUpdate) { - try syncDatabaseForLanguage(language: language, response: response) - saveLastUpdate(serverLastUpdate, for: language) - } - } - - // MARK: Check for Updates - func checkForUpdates(language: String) async throws -> Bool { - let localLastUpdate = getLastUpdate(for: language) ?? "1970-01-01" - let versionResponse = try await apiClient.fetchDataVersion(language: language) + // Fetch data from API. + let response = try await apiClient.fetchLanguageData(language: language) + let serverLastUpdate = response.contract.updatedAt - return versionResponse.versions.values.contains { serverDate in - isUpdateAvailable(local: localLastUpdate, server: serverDate) - } - } - - // MARK: Has Data - func hasData(for language: String) -> Bool { - return getLastUpdate(for: language) != nil - } - - // MARK: Sync Database - private func syncDatabaseForLanguage( - language: String, - response: DataResponse - ) throws { - guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.be.scri.userDefaultsContainer") else { - throw NSError(domain: "AppGroup", code: -1, userInfo: [NSLocalizedDescriptionKey: "App group container not found"]) + // Check if update is needed. + if forceDownload || isUpdateAvailable(local: localLastUpdate, server: serverLastUpdate) { + try syncDatabaseForLanguage(language: language, response: response) + saveLastUpdate(serverLastUpdate, for: language) + } } - let dbURL = containerURL.appendingPathComponent("\(language.uppercased())LanguageData.sqlite") - let dbQueue = try DatabaseQueue(path: dbURL.path) + // MARK: Check for Updates - try dbQueue.write { db in + func checkForUpdates(language: String) async throws -> Bool { + let localLastUpdate = getLastUpdate(for: language) ?? "1970-01-01" + let versionResponse = try await apiClient.fetchDataVersion(language: language) - // Create tables. - for (tableName, columns) in response.contract.fields { + return versionResponse.versions.values.contains { serverDate in + isUpdateAvailable(local: localLastUpdate, server: serverDate) + } + } - let columnDefs = columns.map { columnName, _ in - columnName == "lexemeID" - ? "lexemeID TEXT PRIMARY KEY" - : "\"\(columnName)\" TEXT" - }.joined(separator: ", ") + // MARK: Has Data - try db.execute(sql: """ - CREATE TABLE IF NOT EXISTS "\(tableName)" ( - \(columnDefs) - ) - """) + func hasData(for language: String) -> Bool { + return getLastUpdate(for: language) != nil + } - try db.execute(sql: #"DELETE FROM "\#(tableName)""#) + // MARK: Sync Database + + private func syncDatabaseForLanguage( + language: String, + response: DataResponse + ) throws { + guard + let containerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: "group.be.scri.userDefaultsContainer" + ) + else { + throw NSError( + domain: "AppGroup", code: -1, + userInfo: [NSLocalizedDescriptionKey: "App group container not found"] + ) } - // Insert data. - for (tableName, rows) in response.data { - for row in rows { + let dbURL = containerURL.appendingPathComponent( + "\(language.uppercased())LanguageData.sqlite" + ) + let dbQueue = try DatabaseQueue(path: dbURL.path) - let nonNilRow = row.compactMapValues { $0 } - guard !nonNilRow.isEmpty else { continue } - - let columns = nonNilRow.keys.map { "\"\($0)\"" }.joined(separator: ", ") - let placeholders = Array(repeating: "?", count: nonNilRow.count).joined(separator: ", ") - let values = nonNilRow.values.map { $0 } + try dbQueue.write { db in + // Create tables. + for (tableName, columns) in response.contract.fields { + let columnDefs = columns.map { columnName, _ in + columnName == "lexemeID" + ? "lexemeID TEXT PRIMARY KEY" + : "\"\(columnName)\" TEXT" + }.joined(separator: ", ") try db.execute( sql: """ - INSERT OR REPLACE INTO "\(tableName)" (\(columns)) - VALUES (\(placeholders)) - """, - arguments: StatementArguments(values) + CREATE TABLE IF NOT EXISTS "\(tableName)" ( + \(columnDefs) + ) + """ ) + + try db.execute(sql: #"DELETE FROM "\#(tableName)""#) + } + + // Insert data. + for (tableName, rows) in response.data { + for row in rows { + let nonNilRow = row.compactMapValues { $0 } + guard !nonNilRow.isEmpty else { continue } + + let columns = nonNilRow.keys.map { "\"\($0)\"" }.joined(separator: ", ") + let placeholders = Array(repeating: "?", count: nonNilRow.count).joined( + separator: ", " + ) + let values = nonNilRow.values.map { $0 } + + try db.execute( + sql: """ + INSERT OR REPLACE INTO "\(tableName)" (\(columns)) + VALUES (\(placeholders)) + """, + arguments: StatementArguments(values) + ) + } } } } - } - - // MARK: Private Helpers - private func isUpdateAvailable(local: String, server: String) -> Bool { - let localDate = String(local.prefix(10)) - let serverDate = String(server.prefix(10)) - return serverDate > localDate - } - - private func getLastUpdate(for language: String) -> String? { - return userDefaults.string(forKey: "\(lastUpdateKey)\(language)") - } - - private func saveLastUpdate(_ date: String, for language: String) { - userDefaults.set(date, forKey: "\(lastUpdateKey)\(language)") - } + + // MARK: Private Helpers + + private func isUpdateAvailable(local: String, server: String) -> Bool { + let localDate = String(local.prefix(10)) + let serverDate = String(server.prefix(10)) + return serverDate > localDate + } + + private func getLastUpdate(for language: String) -> String? { + return userDefaults.string(forKey: "\(lastUpdateKey)\(language)") + } + + private func saveLastUpdate(_ date: String, for language: String) { + userDefaults.set(date, forKey: "\(lastUpdateKey)\(language)") + } } diff --git a/Tests/Keyboards/KeyboardsBase/TestExtensions.swift b/Tests/Keyboards/KeyboardsBase/TestExtensions.swift index 076c92b5..e3fe2c2f 100644 --- a/Tests/Keyboards/KeyboardsBase/TestExtensions.swift +++ b/Tests/Keyboards/KeyboardsBase/TestExtensions.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Tests for class extensions used in Scribe keyboards. */ @@ -11,221 +11,226 @@ import XCTest // MARK: secondToLast class ExtensionTest: XCTestCase { - func testSecondToLastNotNil() { - let array = [1, 2, 3, 4, 5] + func testSecondToLastNotNil() throws { + let array = [1, 2, 3, 4, 5] - let result = array.secondToLast()! + let result = try XCTUnwrap(array.secondToLast()) - XCTAssertEqual(result, 4) - } + XCTAssertEqual(result, 4) + } - func testSecondToLastNil() { - let array = [String]() + func testSecondToLastNil() { + let array = [String]() - let result = array.secondToLast() + let result = array.secondToLast() - XCTAssertEqual(result, nil) - } + XCTAssertEqual(result, nil) + } } // MARK: unique extension ExtensionTest { - func testUniqueElements() { - let array = [1, 2, 3, 4, 5] - let expectedResult = [1, 2, 3, 4, 5] + func testUniqueElements() { + let array = [1, 2, 3, 4, 5] + let expectedResult = [1, 2, 3, 4, 5] - let result = array.unique() + let result = array.unique() - XCTAssertEqual(result, expectedResult) - } + XCTAssertEqual(result, expectedResult) + } - func testUniqueElementsWithDuplicates() { - let array = [1, 2, 2, 4, 5] - let expectedResult = [1, 2, 4, 5] + func testUniqueElementsWithDuplicates() { + let array = [1, 2, 2, 4, 5] + let expectedResult = [1, 2, 4, 5] - let result = array.unique() + let result = array.unique() - XCTAssertEqual(result, expectedResult) - } + XCTAssertEqual(result, expectedResult) + } } // MARK: index extension ExtensionTest { - func testIndexValidIndex() { - let string = "Hello, World!" - let index = 5 + func testIndexValidIndex() { + let string = "Hello, World!" + let index = 5 - let result = string.index(fromIdx: index) + let result = string.index(fromIdx: index) - XCTAssertEqual(result, string.index(string.startIndex, offsetBy: index)) - } + XCTAssertEqual(result, string.index(string.startIndex, offsetBy: index)) + } } // MARK: substring extension ExtensionTest { - func testSubstringFromIndexCorrectStringValidIndex() { - let string = "Hello, World!" - let index = 7 + func testSubstringFromIndexCorrectStringValidIndex() { + let string = "Hello, World!" + let index = 7 - let result = string.substring(fromIdx: index) + let result = string.substring(fromIdx: index) - XCTAssertEqual(result, "World!") - } + XCTAssertEqual(result, "World!") + } - func testSubstringToIndexCorrectStringValidIndex() { - let string = "Hello, World!" - let index = 5 + func testSubstringToIndexCorrectStringValidIndex() { + let string = "Hello, World!" + let index = 5 - let result = string.substring(toIdx: index) + let result = string.substring(toIdx: index) - XCTAssertEqual(result, "Hello") - } + XCTAssertEqual(result, "Hello") + } - func testSubstringRangeCorrectStringValidRange() { - let string = "Hello, World!" - let range = Range(1 ... 4) + func testSubstringRangeCorrectStringValidRange() { + let string = "Hello, World!" + let range = Range(1 ... 4) - let result = string.substring(with: range) + let result = string.substring(with: range) - XCTAssertEqual(result, "ello") - } + XCTAssertEqual(result, "ello") + } } // MARK: insertPriorToCursor extension ExtensionTest { - func testInsertPriorToCursor() { - let string = "Hello │" - let char = "Scribe" - let expectedResult = "Hello Scribe│" + func testInsertPriorToCursor() { + let string = "Hello │" + let char = "Scribe" + let expectedResult = "Hello Scribe│" - let result = string.insertPriorToCursor(char: char) + let result = string.insertPriorToCursor(char: char) - XCTAssertEqual(result, expectedResult) - } + XCTAssertEqual(result, expectedResult) + } } // MARK: deletePriorToCursor extension ExtensionTest { - func testDeletePriorToCursor() { - let string = "Hello│" - let expectedResult = "Hell│" + func testDeletePriorToCursor() { + let string = "Hello│" + let expectedResult = "Hell│" - let result = string.deletePriorToCursor() + let result = string.deletePriorToCursor() - XCTAssertEqual(result, expectedResult) - } + XCTAssertEqual(result, expectedResult) + } } // MARK: isLowercase extension ExtensionTest { - func testIsLowercase() { - XCTAssertEqual("hello".isLowercase, true) - XCTAssertEqual("HELLO".isLowercase, false) - XCTAssertEqual("Hello".isLowercase, false) - XCTAssertEqual("👋hello".isLowercase, true) - } + func testIsLowercase() { + XCTAssertEqual("hello".isLowercase, true) + XCTAssertEqual("HELLO".isLowercase, false) + XCTAssertEqual("Hello".isLowercase, false) + XCTAssertEqual("👋hello".isLowercase, true) + } } // MARK: isUppercase extension ExtensionTest { - func testIsUppercase() { - XCTAssertEqual("HELLO".isUppercase, true) - XCTAssertEqual("Hello".isUppercase, false) - XCTAssertEqual("hello".isUppercase, false) - XCTAssertEqual("👋HELLO".isUppercase, true) - } + func testIsUppercase() { + XCTAssertEqual("HELLO".isUppercase, true) + XCTAssertEqual("Hello".isUppercase, false) + XCTAssertEqual("hello".isUppercase, false) + XCTAssertEqual("👋HELLO".isUppercase, true) + } } // MARK: isCapitalized extension ExtensionTest { - func testIsCapitalized() { - XCTAssertEqual("Hello".isCapitalized, true) - XCTAssertEqual("hello".isCapitalized, false) - XCTAssertEqual("HELLO".isCapitalized, false) - XCTAssertEqual("👋HELLO".isCapitalized, false) - } + func testIsCapitalized() { + XCTAssertEqual("Hello".isCapitalized, true) + XCTAssertEqual("hello".isCapitalized, false) + XCTAssertEqual("HELLO".isCapitalized, false) + XCTAssertEqual("👋HELLO".isCapitalized, false) + } } // MARK: count extension ExtensionTest { - func testCount() { - XCTAssertEqual("Hello, World!".count(of: "!"), 1) - XCTAssertEqual("Hello, World!".count(of: "@"), 0) - XCTAssertEqual("Hello, World!".count(of: "l"), 3) - XCTAssertEqual("".count(of: "!"), 0) - XCTAssertEqual("👋".count(of: "👋"), 1) - } + func testCount() { + XCTAssertEqual("Hello, World!".count(of: "!"), 1) + XCTAssertEqual("Hello, World!".count(of: "@"), 0) + XCTAssertEqual("Hello, World!".count(of: "l"), 3) + XCTAssertEqual("".count(of: "!"), 0) + XCTAssertEqual("👋".count(of: "👋"), 1) + } } // MARK: capitalize extension ExtensionTest { - func testCapitalize() { - XCTAssertEqual("hello".capitalize(), "Hello") - XCTAssertEqual("HELLO".capitalize(), "Hello") - XCTAssertEqual("hELLO".capitalize(), "Hello") - XCTAssertEqual("".capitalize(), "") - XCTAssertEqual("👋hello".capitalize(), "👋hello") - } + func testCapitalize() { + XCTAssertEqual("hello".capitalize(), "Hello") + XCTAssertEqual("HELLO".capitalize(), "Hello") + XCTAssertEqual("hELLO".capitalize(), "Hello") + XCTAssertEqual("".capitalize(), "") + XCTAssertEqual("👋hello".capitalize(), "👋hello") + } } // MARK: isNumeric extension ExtensionTest { - func testIsNumberic() { - XCTAssertEqual("123".isNumeric, true) - XCTAssertEqual("0123".isNumeric, true) - XCTAssertEqual("hello".isNumeric, false) - XCTAssertEqual("👋".isNumeric, false) - } + func testIsNumberic() { + XCTAssertEqual("123".isNumeric, true) + XCTAssertEqual("0123".isNumeric, true) + XCTAssertEqual("hello".isNumeric, false) + XCTAssertEqual("👋".isNumeric, false) + } } // MARK: trailingSpacesTrimmed extension ExtensionTest { - func testTrailingSpacesTrimmed() { - XCTAssertEqual("".trailingSpacesTrimmed, "") - XCTAssertEqual("Hello ".trailingSpacesTrimmed, "Hello") - XCTAssertEqual("Hello".trailingSpacesTrimmed, "Hello") - } + func testTrailingSpacesTrimmed() { + XCTAssertEqual("".trailingSpacesTrimmed, "") + XCTAssertEqual("Hello ".trailingSpacesTrimmed, "Hello") + XCTAssertEqual("Hello".trailingSpacesTrimmed, "Hello") + } } // MARK: setColorForText extension ExtensionTest { - func testSetColorForExistingText() { - let string = "Hello, World!" - let attributedString = NSMutableAttributedString(string: string) - let textForAttribute = "World" - let color = UIColor.scribeBlue - - attributedString.setColorForText(textForAttribute: textForAttribute, withColor: color) - - let range = (attributedString.string as NSString).range(of: textForAttribute, options: .caseInsensitive) - - attributedString.enumerateAttribute(.foregroundColor, in: range, options: []) { value, _, _ in - XCTAssertEqual(value as! UIColor, color) + func testSetColorForExistingText() { + let string = "Hello, World!" + let attributedString = NSMutableAttributedString(string: string) + let textForAttribute = "World" + let color = UIColor.scribeBlue + + attributedString.setColorForText(textForAttribute: textForAttribute, withColor: color) + + let range = (attributedString.string as NSString).range( + of: textForAttribute, options: .caseInsensitive + ) + + attributedString.enumerateAttribute(.foregroundColor, in: range, options: []) { + value, _, _ in + XCTAssertEqual(value as! UIColor, color) + } } - } - func testSetColorForNonExistingText() { - let string = "Hello, World!" - let attributedString = NSMutableAttributedString(string: string) - let textForAttribute = "Universe" - let color = UIColor.red + func testSetColorForNonExistingText() { + let string = "Hello, World!" + let attributedString = NSMutableAttributedString(string: string) + let textForAttribute = "Universe" + let color = UIColor.red - attributedString.setColorForText(textForAttribute: textForAttribute, withColor: color) + attributedString.setColorForText(textForAttribute: textForAttribute, withColor: color) - let range = (attributedString.string as NSString).range(of: textForAttribute, options: .caseInsensitive) - XCTAssertEqual(range.location, NSNotFound) - } + let range = (attributedString.string as NSString).range( + of: textForAttribute, options: .caseInsensitive + ) + XCTAssertEqual(range.location, NSNotFound) + } } diff --git a/Tests/Keyboards/KeyboardsBase/TestKeyboardStyling.swift b/Tests/Keyboards/KeyboardsBase/TestKeyboardStyling.swift index 303d6e73..d61f42fc 100644 --- a/Tests/Keyboards/KeyboardsBase/TestKeyboardStyling.swift +++ b/Tests/Keyboards/KeyboardsBase/TestKeyboardStyling.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * Tests for keyboard styling used in Scribe keyboards. */ @@ -11,282 +11,292 @@ import XCTest // MARK: styleBtn class KeyboardStylingTest: XCTestCase { - func testStyleBtnNormalButton() { - let button = UIButton(type: .system) - let title = "return" - let radius = 4.0 - - styleBtn(btn: button, title: title, radius: radius) - - XCTAssertEqual(button.configuration, nil) - XCTAssertEqual(button.clipsToBounds, false) - XCTAssertEqual(button.layer.masksToBounds, false) - XCTAssertEqual(button.layer.cornerRadius, radius) - XCTAssertEqual(button.titleLabel?.text, title) - XCTAssertEqual(button.contentHorizontalAlignment, .center) - XCTAssertEqual(button.titleColor(for: .normal), keyCharColor) - XCTAssertEqual(button.layer.shadowColor, keyShadowColor) - XCTAssertEqual(button.layer.shadowOffset, CGSize(width: 0.0, height: 1.0)) - XCTAssertEqual(button.layer.shadowOpacity, 1.0) - XCTAssertEqual(button.layer.shadowRadius, 0.0) - } - - func testStyleBtnWithWikidataInvalidCommandMsg() { - let button = UIButton(type: .system) - let title = "Not in Wikidata" - let radius = 4.0 - - invalidCommandMsgWikidata = "Not in Wikidata" - styleBtn(btn: button, title: title, radius: radius) - - XCTAssertEqual(button.configuration?.baseForegroundColor, UITraitCollection.current.userInterfaceStyle == .light ? specialKeyColor : keyColor) - XCTAssertEqual(button.configuration?.image, UIImage(systemName: "info.circle.fill")) - XCTAssertEqual(button.configuration?.imagePlacement, .trailing) - XCTAssertEqual(button.configuration?.imagePadding, 3) - } - - func testStyleBtnWithWiktionaryInvalidCommandMsg() { - let button = UIButton(type: .system) - let title = "Not in Wiktionary" - let radius = 4.0 - - invalidCommandMsgWiktionary = "Not in Wiktionary" - styleBtn(btn: button, title: title, radius: radius) - - XCTAssertEqual(button.configuration?.baseForegroundColor, UITraitCollection.current.userInterfaceStyle == .light ? specialKeyColor : keyColor) - XCTAssertEqual(button.configuration?.image, UIImage(systemName: "info.circle.fill")) - XCTAssertEqual(button.configuration?.imagePlacement, .trailing) - XCTAssertEqual(button.configuration?.imagePadding, 3) - } - - func testStyleBtnWitheScribeTitle() { - let button = UIButton(type: .system) - let title = "Scribe" - let radius = 4.0 - - styleBtn(btn: button, title: title, radius: radius) - - XCTAssertNotEqual(button.layer.shadowColor, keyShadowColor) - XCTAssertNotEqual(button.layer.shadowOffset, CGSize(width: 0.0, height: 1.0)) - XCTAssertNotEqual(button.layer.shadowOpacity, 1.0) - XCTAssertNotEqual(button.layer.shadowRadius, 0.0) - } - - func testStyleBtnWithEmojisToShow() { - let button = UIButton(type: .system) - let title = "return" - let radius = 4.0 - - emojisToShow = .one - styleBtn(btn: button, title: title, radius: radius) - - XCTAssertEqual(button.layer.shadowOpacity, 0) - } + func testStyleBtnNormalButton() { + let button = UIButton(type: .system) + let title = "return" + let radius = 4.0 + + styleBtn(btn: button, title: title, radius: radius) + + XCTAssertEqual(button.configuration, nil) + XCTAssertEqual(button.clipsToBounds, false) + XCTAssertEqual(button.layer.masksToBounds, false) + XCTAssertEqual(button.layer.cornerRadius, radius) + XCTAssertEqual(button.titleLabel?.text, title) + XCTAssertEqual(button.contentHorizontalAlignment, .center) + XCTAssertEqual(button.titleColor(for: .normal), keyCharColor) + XCTAssertEqual(button.layer.shadowColor, keyShadowColor) + XCTAssertEqual(button.layer.shadowOffset, CGSize(width: 0.0, height: 1.0)) + XCTAssertEqual(button.layer.shadowOpacity, 1.0) + XCTAssertEqual(button.layer.shadowRadius, 0.0) + } + + func testStyleBtnWithWikidataInvalidCommandMsg() { + let button = UIButton(type: .system) + let title = "Not in Wikidata" + let radius = 4.0 + + invalidCommandMsgWikidata = "Not in Wikidata" + styleBtn(btn: button, title: title, radius: radius) + + XCTAssertEqual( + button.configuration?.baseForegroundColor, + UITraitCollection.current.userInterfaceStyle == .light ? specialKeyColor : keyColor + ) + XCTAssertEqual(button.configuration?.image, UIImage(systemName: "info.circle.fill")) + XCTAssertEqual(button.configuration?.imagePlacement, .trailing) + XCTAssertEqual(button.configuration?.imagePadding, 3) + } + + func testStyleBtnWithWiktionaryInvalidCommandMsg() { + let button = UIButton(type: .system) + let title = "Not in Wiktionary" + let radius = 4.0 + + invalidCommandMsgWiktionary = "Not in Wiktionary" + styleBtn(btn: button, title: title, radius: radius) + + XCTAssertEqual( + button.configuration?.baseForegroundColor, + UITraitCollection.current.userInterfaceStyle == .light ? specialKeyColor : keyColor + ) + XCTAssertEqual(button.configuration?.image, UIImage(systemName: "info.circle.fill")) + XCTAssertEqual(button.configuration?.imagePlacement, .trailing) + XCTAssertEqual(button.configuration?.imagePadding, 3) + } + + func testStyleBtnWitheScribeTitle() { + let button = UIButton(type: .system) + let title = "Scribe" + let radius = 4.0 + + styleBtn(btn: button, title: title, radius: radius) + + XCTAssertNotEqual(button.layer.shadowColor, keyShadowColor) + XCTAssertNotEqual(button.layer.shadowOffset, CGSize(width: 0.0, height: 1.0)) + XCTAssertNotEqual(button.layer.shadowOpacity, 1.0) + XCTAssertNotEqual(button.layer.shadowRadius, 0.0) + } + + func testStyleBtnWithEmojisToShow() { + let button = UIButton(type: .system) + let title = "return" + let radius = 4.0 + + emojisToShow = .one + styleBtn(btn: button, title: title, radius: radius) + + XCTAssertEqual(button.layer.shadowOpacity, 0) + } } // MARK: getPhoneIconConfig extension KeyboardStylingTest { - func testGetPhoneIconConfigWithInvalidIconNameInPortrait() { - letterKeyWidth = 100 - isLandscapeView = false - let iconName = "abc" - let expected = UIImage.SymbolConfiguration( - pointSize: letterKeyWidth / 1.75, - weight: .light, - scale: .medium - ) - - let result = getPhoneIconConfig(iconName: iconName) - - XCTAssertEqual(expected, result) - } - - func testGetPhoneIconConfigWithValidIconNameInPortrait() { - letterKeyWidth = 100 - isLandscapeView = false - let iconName = "delete.left" - let expected = UIImage.SymbolConfiguration( - pointSize: letterKeyWidth / 1.55, - weight: .light, - scale: .medium - ) - - let result = getPhoneIconConfig(iconName: iconName) - - XCTAssertEqual(expected, result) - } - - func testGetPhoneIconConfigWithValidIconNameInLandscape() { - letterKeyWidth = 100 - isLandscapeView = true - let iconName = "delete.left" - let expected = UIImage.SymbolConfiguration( - pointSize: letterKeyWidth / 3.2, - weight: .light, - scale: .medium - ) - - let result = getPhoneIconConfig(iconName: iconName) - - XCTAssertEqual(expected, result) - } - - func testGetPhoneIconConfigWithInvalidIconNameInLandscape() { - letterKeyWidth = 100 - isLandscapeView = true - let iconName = "abc" - let expected = UIImage.SymbolConfiguration( - pointSize: letterKeyWidth / 3.5, - weight: .light, - scale: .medium - ) - - let result = getPhoneIconConfig(iconName: iconName) - - XCTAssertEqual(expected, result) - } + func testGetPhoneIconConfigWithInvalidIconNameInPortrait() { + letterKeyWidth = 100 + isLandscapeView = false + let iconName = "abc" + let expected = UIImage.SymbolConfiguration( + pointSize: letterKeyWidth / 1.75, + weight: .light, + scale: .medium + ) + + let result = getPhoneIconConfig(iconName: iconName) + + XCTAssertEqual(expected, result) + } + + func testGetPhoneIconConfigWithValidIconNameInPortrait() { + letterKeyWidth = 100 + isLandscapeView = false + let iconName = "delete.left" + let expected = UIImage.SymbolConfiguration( + pointSize: letterKeyWidth / 1.55, + weight: .light, + scale: .medium + ) + + let result = getPhoneIconConfig(iconName: iconName) + + XCTAssertEqual(expected, result) + } + + func testGetPhoneIconConfigWithValidIconNameInLandscape() { + letterKeyWidth = 100 + isLandscapeView = true + let iconName = "delete.left" + let expected = UIImage.SymbolConfiguration( + pointSize: letterKeyWidth / 3.2, + weight: .light, + scale: .medium + ) + + let result = getPhoneIconConfig(iconName: iconName) + + XCTAssertEqual(expected, result) + } + + func testGetPhoneIconConfigWithInvalidIconNameInLandscape() { + letterKeyWidth = 100 + isLandscapeView = true + let iconName = "abc" + let expected = UIImage.SymbolConfiguration( + pointSize: letterKeyWidth / 3.5, + weight: .light, + scale: .medium + ) + + let result = getPhoneIconConfig(iconName: iconName) + + XCTAssertEqual(expected, result) + } } // MARK: getPadIconConfig extension KeyboardStylingTest { - func testGetPadIconConfigWithInvalidIconNameInPortrait() { - letterKeyWidth = 100 - isLandscapeView = false - let expected = UIImage.SymbolConfiguration( - pointSize: letterKeyWidth / 3, - weight: .light, - scale: .medium - ) - - let result = getPadIconConfig(iconName: "abc") - - XCTAssertEqual(expected, result) - } - - func testGetPadIconConfigWithValidIconNameInPortrait() { - letterKeyWidth = 100 - isLandscapeView = false - let iconName = "delete.left" - let expected = UIImage.SymbolConfiguration( - pointSize: letterKeyWidth / 2.75, - weight: .light, - scale: .medium - ) - - let result = getPadIconConfig(iconName: iconName) - - XCTAssertEqual(expected, result) - } - - func testGetPadIconConfigWithInvalidIconNameInLandscape() { - letterKeyWidth = 100 - isLandscapeView = true - let iconName = "abc" - let expected = UIImage.SymbolConfiguration( - pointSize: letterKeyWidth / 3.75, - weight: .light, - scale: .medium - ) - - let result = getPadIconConfig(iconName: iconName) - - XCTAssertEqual(expected, result) - } - - func testGetPadIconConfigWithValidIconNameInLandscape() { - letterKeyWidth = 100 - isLandscapeView = true - let iconName = "delete.left" - let expected = UIImage.SymbolConfiguration( - pointSize: letterKeyWidth / 3.4, - weight: .light, - scale: .medium - ) - - let result = getPadIconConfig(iconName: iconName) - - XCTAssertEqual(expected, result) - } - - func testGetPadIconConfigWithValidIconNameGlobe() { - letterKeyWidth = 100 - isLandscapeView = false - let iconName = "globe" - let expected = UIImage.SymbolConfiguration( - pointSize: letterKeyWidth / 2.75, - weight: .light, - scale: .medium - ) - - let result = getPadIconConfig(iconName: iconName) - - XCTAssertEqual(expected, result) - } + func testGetPadIconConfigWithInvalidIconNameInPortrait() { + letterKeyWidth = 100 + isLandscapeView = false + let expected = UIImage.SymbolConfiguration( + pointSize: letterKeyWidth / 3, + weight: .light, + scale: .medium + ) + + let result = getPadIconConfig(iconName: "abc") + + XCTAssertEqual(expected, result) + } + + func testGetPadIconConfigWithValidIconNameInPortrait() { + letterKeyWidth = 100 + isLandscapeView = false + let iconName = "delete.left" + let expected = UIImage.SymbolConfiguration( + pointSize: letterKeyWidth / 2.75, + weight: .light, + scale: .medium + ) + + let result = getPadIconConfig(iconName: iconName) + + XCTAssertEqual(expected, result) + } + + func testGetPadIconConfigWithInvalidIconNameInLandscape() { + letterKeyWidth = 100 + isLandscapeView = true + let iconName = "abc" + let expected = UIImage.SymbolConfiguration( + pointSize: letterKeyWidth / 3.75, + weight: .light, + scale: .medium + ) + + let result = getPadIconConfig(iconName: iconName) + + XCTAssertEqual(expected, result) + } + + func testGetPadIconConfigWithValidIconNameInLandscape() { + letterKeyWidth = 100 + isLandscapeView = true + let iconName = "delete.left" + let expected = UIImage.SymbolConfiguration( + pointSize: letterKeyWidth / 3.4, + weight: .light, + scale: .medium + ) + + let result = getPadIconConfig(iconName: iconName) + + XCTAssertEqual(expected, result) + } + + func testGetPadIconConfigWithValidIconNameGlobe() { + letterKeyWidth = 100 + isLandscapeView = false + let iconName = "globe" + let expected = UIImage.SymbolConfiguration( + pointSize: letterKeyWidth / 2.75, + weight: .light, + scale: .medium + ) + + let result = getPadIconConfig(iconName: iconName) + + XCTAssertEqual(expected, result) + } } // MARK: styleIconBtn extension KeyboardStylingTest { - func testStyleIconBtn() { - let color = UIColor.black - let iconName = "delete.left" - let button = UIButton(type: .system) + func testStyleIconBtn() { + let color = UIColor.black + let iconName = "delete.left" + let button = UIButton(type: .system) - let iconConfig = getPhoneIconConfig(iconName: iconName) - let image = UIImage(systemName: iconName, withConfiguration: iconConfig) + let iconConfig = getPhoneIconConfig(iconName: iconName) + let image = UIImage(systemName: iconName, withConfiguration: iconConfig) - styleIconBtn(btn: button, color: color, iconName: iconName) + styleIconBtn(btn: button, color: color, iconName: iconName) - XCTAssertEqual(image, button.imageView?.image) - XCTAssertEqual(button.tintColor, color) - } + XCTAssertEqual(image, button.imageView?.image) + XCTAssertEqual(button.tintColor, color) + } } // MARK: styleDeleteButton extension KeyboardStylingTest { - func testStyleDeleteButton() { - let pressedButton = UIButton() - let unpressedButton = UIButton() - let pressedIconName = "delete.left.fill" - let unpressedIconName = "delete.left" - let pressedButtonIconConfig = getPhoneIconConfig(iconName: pressedIconName) - let unpressedButtonIconConfig = getPhoneIconConfig(iconName: unpressedIconName) - let pressedButtonImage = UIImage(systemName: pressedIconName, withConfiguration: pressedButtonIconConfig) - let unpressedButtonImage = UIImage(systemName: unpressedIconName, withConfiguration: unpressedButtonIconConfig) - - styleDeleteButton(pressedButton, isPressed: true) - styleDeleteButton(unpressedButton, isPressed: false) - - XCTAssertEqual(pressedButton.imageView?.image, pressedButtonImage) - XCTAssertEqual(unpressedButton.imageView?.image, unpressedButtonImage) - } + func testStyleDeleteButton() { + let pressedButton = UIButton() + let unpressedButton = UIButton() + let pressedIconName = "delete.left.fill" + let unpressedIconName = "delete.left" + let pressedButtonIconConfig = getPhoneIconConfig(iconName: pressedIconName) + let unpressedButtonIconConfig = getPhoneIconConfig(iconName: unpressedIconName) + let pressedButtonImage = UIImage( + systemName: pressedIconName, withConfiguration: pressedButtonIconConfig + ) + let unpressedButtonImage = UIImage( + systemName: unpressedIconName, withConfiguration: unpressedButtonIconConfig + ) + + styleDeleteButton(pressedButton, isPressed: true) + styleDeleteButton(unpressedButton, isPressed: false) + + XCTAssertEqual(pressedButton.imageView?.image, pressedButtonImage) + XCTAssertEqual(unpressedButton.imageView?.image, unpressedButtonImage) + } } // MARK: addPadding extension KeyboardStylingTest { - func testAddPadding() { - let stackView = UIStackView() - let width = CGFloat(10) - let key = "@" - - paddingViews = [] - XCTAssertEqual(paddingViews.count, 0) - XCTAssertEqual(stackView.subviews.count, 0) - - addPadding(to: stackView, width: width, key: key) - - XCTAssertEqual(paddingViews.count, 1) - XCTAssertEqual(stackView.subviews.count, 1) - - let padding = stackView.subviews.first as! UIButton - let widthConstraint = padding.constraints.first { $0.firstAttribute == .width } - XCTAssertEqual(padding.titleColor(for: .normal), .clear) - XCTAssertEqual(padding.alpha, 0.0) - XCTAssertEqual(padding.isUserInteractionEnabled, false) - XCTAssertEqual(widthConstraint?.constant, CGFloat(10)) - } + func testAddPadding() throws { + let stackView = UIStackView() + let width = CGFloat(10) + let key = "@" + + paddingViews = [] + XCTAssertEqual(paddingViews.count, 0) + XCTAssertEqual(stackView.subviews.count, 0) + + addPadding(to: stackView, width: width, key: key) + + XCTAssertEqual(paddingViews.count, 1) + XCTAssertEqual(stackView.subviews.count, 1) + + let padding = try XCTUnwrap(stackView.subviews.first as? UIButton) + let widthConstraint = padding.constraints.first { $0.firstAttribute == .width } + XCTAssertEqual(padding.titleColor(for: .normal), .clear) + XCTAssertEqual(padding.alpha, 0.0) + XCTAssertEqual(padding.isUserInteractionEnabled, false) + XCTAssertEqual(widthConstraint?.constant, CGFloat(10)) + } } diff --git a/Tests/Scribe/Views/BaseTableViewControllerTest.swift b/Tests/Scribe/Views/BaseTableViewControllerTest.swift index 8e876d21..e3fc42f7 100644 --- a/Tests/Scribe/Views/BaseTableViewControllerTest.swift +++ b/Tests/Scribe/Views/BaseTableViewControllerTest.swift @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -/** +/* * A base table view controller for testing Scribe. */ @@ -9,21 +9,21 @@ import Foundation import XCTest final class BaseTableViewControllerTest: XCTestCase { - private var sut: BaseTableViewController! + private var sut: BaseTableViewController! - override func setUp() { - super.setUp() - sut = BaseTableViewController() - } + override func setUp() { + super.setUp() + sut = BaseTableViewController() + } - override func tearDown() { - sut = nil - super.tearDown() - } + override func tearDown() { + sut = nil + super.tearDown() + } - func testNumberOfSectionsZero() { - let result = sut.numberOfSections(in: sut.tableView) + func testNumberOfSectionsZero() { + let result = sut.numberOfSections(in: sut.tableView) - XCTAssertEqual(result, 0) - } + XCTAssertEqual(result, 0) + } } From af3a380d97c5874d96b4c475ffbbd819eb41adc5 Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Tue, 24 Mar 2026 22:08:30 +0100 Subject: [PATCH 23/31] Order imports --- Tests/Keyboards/KeyboardsBase/TestExtensions.swift | 5 +++-- Tests/Keyboards/KeyboardsBase/TestKeyboardStyling.swift | 3 ++- Tests/Scribe/Views/BaseTableViewControllerTest.swift | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Tests/Keyboards/KeyboardsBase/TestExtensions.swift b/Tests/Keyboards/KeyboardsBase/TestExtensions.swift index e3fe2c2f..6a7dffd8 100644 --- a/Tests/Keyboards/KeyboardsBase/TestExtensions.swift +++ b/Tests/Keyboards/KeyboardsBase/TestExtensions.swift @@ -5,9 +5,10 @@ */ import Foundation -@testable import Scribe import XCTest +@testable import Scribe + // MARK: secondToLast class ExtensionTest: XCTestCase { @@ -86,7 +87,7 @@ extension ExtensionTest { func testSubstringRangeCorrectStringValidRange() { let string = "Hello, World!" - let range = Range(1 ... 4) + let range = Range(1...4) let result = string.substring(with: range) diff --git a/Tests/Keyboards/KeyboardsBase/TestKeyboardStyling.swift b/Tests/Keyboards/KeyboardsBase/TestKeyboardStyling.swift index d61f42fc..7407f95b 100644 --- a/Tests/Keyboards/KeyboardsBase/TestKeyboardStyling.swift +++ b/Tests/Keyboards/KeyboardsBase/TestKeyboardStyling.swift @@ -5,9 +5,10 @@ */ import Foundation -@testable import Scribe import XCTest +@testable import Scribe + // MARK: styleBtn class KeyboardStylingTest: XCTestCase { diff --git a/Tests/Scribe/Views/BaseTableViewControllerTest.swift b/Tests/Scribe/Views/BaseTableViewControllerTest.swift index e3fc42f7..2f43912f 100644 --- a/Tests/Scribe/Views/BaseTableViewControllerTest.swift +++ b/Tests/Scribe/Views/BaseTableViewControllerTest.swift @@ -5,9 +5,10 @@ */ import Foundation -@testable import Scribe import XCTest +@testable import Scribe + final class BaseTableViewControllerTest: XCTestCase { private var sut: BaseTableViewController! From 11924e1b2af5f63c346a285b3a0b7688f0dd7cc4 Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Wed, 25 Mar 2026 00:54:11 +0100 Subject: [PATCH 24/31] Update repo workflows --- ...ader_check.yaml => ci_license_header_check.yaml} | 10 ++++++++-- .../{pr_swiftlint.yaml => ci_swiftlint.yaml} | 10 ++++++++-- .github/workflows/{pr_ci.yaml => ci_test.yaml} | 10 ++++++++-- .github/workflows/pr_maintainer_checklist.yaml | 13 ++++++++++--- 4 files changed, 34 insertions(+), 9 deletions(-) rename .github/workflows/{pr_ci_license_header_check.yaml => ci_license_header_check.yaml} (67%) rename .github/workflows/{pr_swiftlint.yaml => ci_swiftlint.yaml} (73%) rename .github/workflows/{pr_ci.yaml => ci_test.yaml} (92%) diff --git a/.github/workflows/pr_ci_license_header_check.yaml b/.github/workflows/ci_license_header_check.yaml similarity index 67% rename from .github/workflows/pr_ci_license_header_check.yaml rename to .github/workflows/ci_license_header_check.yaml index f9fa3c7f..c3e28335 100644 --- a/.github/workflows/pr_ci_license_header_check.yaml +++ b/.github/workflows/ci_license_header_check.yaml @@ -1,9 +1,15 @@ -name: pr_ci_license_header_check +name: ci_license_header_check on: pull_request: branches: - main - types: [opened, reopened, synchronize] + types: + - opened + - reopened + - synchronize + push: + branches: + - main jobs: license_header_check: diff --git a/.github/workflows/pr_swiftlint.yaml b/.github/workflows/ci_swiftlint.yaml similarity index 73% rename from .github/workflows/pr_swiftlint.yaml rename to .github/workflows/ci_swiftlint.yaml index 5d26735f..5ad4e310 100644 --- a/.github/workflows/pr_swiftlint.yaml +++ b/.github/workflows/ci_swiftlint.yaml @@ -1,9 +1,15 @@ -name: pr_swiftlint +name: ci_swiftlint on: pull_request: branches: - main - types: [opened, reopened, synchronize] + types: + - opened + - reopened + - synchronize + push: + branches: + - main jobs: swiftlint: diff --git a/.github/workflows/pr_ci.yaml b/.github/workflows/ci_test.yaml similarity index 92% rename from .github/workflows/pr_ci.yaml rename to .github/workflows/ci_test.yaml index bfd60a72..bc0a59af 100644 --- a/.github/workflows/pr_ci.yaml +++ b/.github/workflows/ci_test.yaml @@ -1,9 +1,15 @@ -name: pr_ci +name: ci_test on: pull_request: branches: - main - types: [opened, reopened, synchronize] + types: + - opened + - reopened + - synchronize + push: + branches: + - main jobs: test: diff --git a/.github/workflows/pr_maintainer_checklist.yaml b/.github/workflows/pr_maintainer_checklist.yaml index c3a2527f..7c0c7691 100644 --- a/.github/workflows/pr_maintainer_checklist.yaml +++ b/.github/workflows/pr_maintainer_checklist.yaml @@ -17,9 +17,16 @@ jobs: uses: thollander/actions-comment-pull-request@v2 with: message: | - ## Thank you for the pull request! 💙 - - The Scribe-iOS team will do our best to address your contribution as soon as we can. If you're not already a member of our [public Matrix community](https://matrix.to/#/#scribe_community:matrix.org), please consider joining! We'd suggest that you use the [Element](https://element.io/) client as well as [Element X](https://element.io/app) for a mobile app, and definitely join the `General` and `iOS` rooms once you're in. Also consider attending our [bi-weekly Saturday dev syncs](https://etherpad.wikimedia.org/p/scribe-dev-sync). It'd be great to meet you 😊 + ## Thank you for the pull request! 💙🩵 + + The Scribe-iOS team will do our best to address your contribution as soon as we can. The following are some important points: + - Those interested in developing their skills and expanding their role in the community should read the [mentorship and growth section of the contribution guide](https://github.com/scribe-org/Scribe-iOS/blob/main/CONTRIBUTING.md#mentorship-and-growth) + - If you're not already a member of our [public Matrix community](https://matrix.to/#/#scribe_community:matrix.org), please consider joining! + - We'd suggest that you use the [Element](https://element.io/) client as well as [Element X](https://element.io/app) for a mobile app + - Join the `General` and `iOS` rooms once you're in + - Also consider attending our bi-weekly Saturday developer syncs! + - Details are shared in the `General` room on Matrix each Wednesday before the sync + - It would be great to meet you 😊 > [!NOTE] > Scribe uses [Conventional Comments](https://conventionalcomments.org/) in reviews to make sure that communication is as clear as possible. From 55591cb916add3964b608db5a38838e7391be02f Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Wed, 25 Mar 2026 01:00:15 +0100 Subject: [PATCH 25/31] Minor workflow update --- .github/workflows/pr_maintainer_checklist.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr_maintainer_checklist.yaml b/.github/workflows/pr_maintainer_checklist.yaml index 7c0c7691..b5ac5807 100644 --- a/.github/workflows/pr_maintainer_checklist.yaml +++ b/.github/workflows/pr_maintainer_checklist.yaml @@ -52,5 +52,5 @@ jobs: ## First PR Commit Check - [ ] The commit messages for the remote branch should be checked to make sure the contributor's email is set up correctly so that they receive credit for their contribution - - The contributor's name and icon in remote commits should be the same as what appears in the PR - - If there's a mismatch, the contributor needs to make sure that the [email they use for GitHub](https://github.com/settings/emails) matches what they have for `git config user.email` in their local Scribe-iOS repo (can be set with `git config --global user.email "GITHUB_EMAIL"`) + - The contributor's name and icon in remote commits should be the same as what appears in the PR + - If there's a mismatch, the contributor needs to make sure that the [email they use for GitHub](https://github.com/settings/emails) matches what they have for `git config user.email` in their local Scribe-iOS repo (can be set with `git config --global user.email "GITHUB_EMAIL"`) From ddb14de1977de617efec1f84aa07e91a1d178d9d Mon Sep 17 00:00:00 2001 From: Purnama S Rahayu Date: Fri, 27 Feb 2026 13:20:32 +0700 Subject: [PATCH 26/31] refactor: remove old form elements --- Keyboards/KeyboardsBase/Keyboard.xib | 81 ++++++++++++++++++---------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/Keyboards/KeyboardsBase/Keyboard.xib b/Keyboards/KeyboardsBase/Keyboard.xib index 05d32246..d63874cb 100644 --- a/Keyboards/KeyboardsBase/Keyboard.xib +++ b/Keyboards/KeyboardsBase/Keyboard.xib @@ -1,9 +1,11 @@ + + @@ -41,37 +43,49 @@ + + - + - + - + - + - + - - + - + - + - + - +