diff --git a/.gitignore b/.gitignore index 637b6d18..97372299 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ DerivedData/ # Testing code_coverage.json + +# Python virtual environment +venv/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6f75e2c1..26bf5b4b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -# Setting up a Python environment and pre-commit. +# Set up a Python environment and prek for pre-commit hooks. # Unix or MacOS: # >>> python3 -m venv venv @@ -9,9 +9,9 @@ # >>> venv\Scripts\activate.bat # >>> pip install --upgrade pip -# >>> pip install pre-commit -# >>> pre-commit install -# >>> pre-commit run --all-files +# >>> pip install prek +# >>> prek install +# >>> prek run --all-files repos: - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/Keyboards/DataManager/LanguageDBManager.swift b/Keyboards/DataManager/LanguageDBManager.swift index d1e8461c..5af96006 100644 --- a/Keyboards/DataManager/LanguageDBManager.swift +++ b/Keyboards/DataManager/LanguageDBManager.swift @@ -22,23 +22,42 @@ class LanguageDBManager { /// 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 mainBundlePath = Bundle.main.path(forResource: dbName, ofType: "sqlite") + let classBundlePath = Bundle(for: LanguageDBManager.self).path(forResource: dbName, ofType: "sqlite") + + guard let resourcePath = mainBundlePath ?? classBundlePath else { + print("Database \(dbName).sqlite not found in main or class bundle. Using in-memory DB.") + return try! DatabaseQueue() + } + let fileManager = FileManager.default do { - let dbPath = try fileManager - .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) - .appendingPathComponent("\(dbName).sqlite") - .path + let appSupportURL = try fileManager.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + let dbURL = appSupportURL.appendingPathComponent("\(dbName).sqlite") + let dbPath = dbURL.path + + var shouldCopy = true if fileManager.fileExists(atPath: dbPath) { + // Only copy if the resource is newer or if we want to ensure a fresh copy. + // For now, keeping the "fresh copy" behavior but more safely. try fileManager.removeItem(atPath: dbPath) } - try fileManager.copyItem(atPath: dbResourcePath, toPath: dbPath) - let dbQueue = try DatabaseQueue(path: dbPath) - return dbQueue + + if shouldCopy { + try fileManager.copyItem(atPath: resourcePath, toPath: dbPath) + } + + return try DatabaseQueue(path: dbPath) } catch { - print("An error occurred: UILexicon not available") - let dbQueue = try! DatabaseQueue(path: dbResourcePath) - return dbQueue + print("An error occurred during DB setup for \(dbName): \(error). Attempting read-only access.") + var config = Configuration() + config.readonly = true + if let dbQueue = try? DatabaseQueue(path: resourcePath, configuration: config) { + return dbQueue + } + // Last resort: try to return an empty DB instead of crashing the keyword. + print("Failed to open database \(dbName) even in read-only mode. Returning empty DB.") + return try! DatabaseQueue() } } @@ -273,6 +292,49 @@ extension LanguageDBManager { return queryDBRow(query: query, outputCols: outputCols, args: StatementArguments(args)) } + /// Query emojis of word in `emoji_keywords` using pattern matching. + func queryEmojisPatternMatching(of word: String) -> [String] { + var outputValues = [String]() + let query = """ + SELECT + emoji_keyword_0, emoji_keyword_1, emoji_keyword_2 + + FROM + emoji_keywords + + WHERE + word LIKE ? + + ORDER BY + LENGTH(word) ASC + + LIMIT + 3 + """ + let args = StatementArguments(["\(word.lowercased())%"]) + do { + try database?.read { db in + let rows = try Row.fetchAll(db, sql: query, arguments: args) + for row in rows { + for col in ["emoji_keyword_0", "emoji_keyword_1", "emoji_keyword_2"] { + if let val = row[col] as? String, !val.isEmpty { + if !outputValues.contains(val) { + outputValues.append(val) + } + if outputValues.count == 9 { return } + } + } + } + } + } catch {} + + while outputValues.count < 9 { + outputValues.append("") + } + + return Array(outputValues.prefix(9)) + } + /// Query the noun form of word in `nonuns`. func queryNounForm(of word: String) -> [String] { let language = getControllerLanguageAbbr() diff --git a/Keyboards/KeyboardsBase/InterfaceVariables.swift b/Keyboards/KeyboardsBase/InterfaceVariables.swift index a5a509f3..5f8fecb7 100644 --- a/Keyboards/KeyboardsBase/InterfaceVariables.swift +++ b/Keyboards/KeyboardsBase/InterfaceVariables.swift @@ -94,6 +94,7 @@ enum CommandState { case invalid case displayInformation case dynamicConjugation + case colonToEmoji } /// States of the keyboard corresponding to which auto actions should be presented. diff --git a/Keyboards/KeyboardsBase/Keyboard.xib b/Keyboards/KeyboardsBase/Keyboard.xib index 29c5703d..c224c5e6 100644 --- a/Keyboards/KeyboardsBase/Keyboard.xib +++ b/Keyboards/KeyboardsBase/Keyboard.xib @@ -50,12 +50,20 @@ + + + + + + + + @@ -430,6 +438,70 @@ + + + + + + + + @@ -453,7 +525,25 @@ - + + + + + + + + + + + + + + + + + + + @@ -653,7 +743,13 @@ - + + + + + + + diff --git a/Keyboards/KeyboardsBase/KeyboardViewController.swift b/Keyboards/KeyboardsBase/KeyboardViewController.swift index c8af135c..4fd2a672 100644 --- a/Keyboards/KeyboardsBase/KeyboardViewController.swift +++ b/Keyboards/KeyboardsBase/KeyboardViewController.swift @@ -80,7 +80,7 @@ class KeyboardViewController: UIInputViewController { /// Function to load the keyboard interface into which keyboardView is instantiated. func loadInterface() { - let keyboardNib = UINib(nibName: "Keyboard", bundle: nil) + let keyboardNib = UINib(nibName: "Keyboard", bundle: Bundle(for: KeyboardViewController.self)) keyboardView = keyboardNib.instantiate(withOwner: self, options: nil)[0] as? UIView keyboardView.translatesAutoresizingMaskIntoConstraints = true view.addSubview(keyboardView) @@ -102,25 +102,25 @@ class KeyboardViewController: UIInputViewController { /// /// - 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 + 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 + 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 @@ -390,13 +390,25 @@ class KeyboardViewController: UIInputViewController { } autoAction2Visible = false emojisToShow = .three + } + + let dividerColor: UIColor + if UITraitCollection.current.userInterfaceStyle == .light { + dividerColor = specialKeyColor + } else { + dividerColor = UIColor(cgColor: commandBarPlaceholderColorCG) + } - 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) + if !emojisToDisplay[2].isEmpty && DeviceType.isPad { + for i in 0 ..< 3 { + emojisToDisplayArray.append(emojisToDisplay[i]) + } + autoAction2Visible = false + emojisToShow = .three + + let padDividers: [UILabel] = [padEmojiDivider0, padEmojiDivider1, padEmojiDivider2, padEmojiDivider3, padEmojiDivider4] + for i in 0 ..< padDividers.count where emojisToShow.rawValue > i + 1 { + padDividers[i].backgroundColor = dividerColor } conditionallyHideEmojiDividers() } else if !emojisToDisplay[1].isEmpty { @@ -406,10 +418,9 @@ class KeyboardViewController: UIInputViewController { autoAction2Visible = false emojisToShow = .two - if UITraitCollection.current.userInterfaceStyle == .light { - phoneEmojiDivider.backgroundColor = specialKeyColor - } else if UITraitCollection.current.userInterfaceStyle == .dark { - phoneEmojiDivider.backgroundColor = UIColor(cgColor: commandBarPlaceholderColorCG) + let phoneDividers: [UILabel] = [phoneEmojiDivider, phoneEmojiDivider1] + for i in 0 ..< phoneDividers.count where emojisToShow.rawValue > i + 1 { + phoneDividers[i].backgroundColor = dividerColor } conditionallyHideEmojiDividers() } else { @@ -420,6 +431,59 @@ class KeyboardViewController: UIInputViewController { } } + func getEmojiAutoSuggestionsPatternMatching(for word: String) { + let emojisToDisplay = LanguageDBManager.shared.queryEmojisPatternMatching(of: word.lowercased()) + + emojisToDisplayArray = [String]() + if !emojisToDisplay[0].isEmpty { + currentEmojiTriggerWord = ":" + word.lowercased() + + for emoji in emojisToDisplay where !emoji.isEmpty { + emojisToDisplayArray.append(emoji) + } + + switch emojisToDisplayArray.count { + case 1: emojisToShow = .one + case 2: emojisToShow = .two + case 3: emojisToShow = .three + case 4: emojisToShow = .four + case 5: emojisToShow = .five + case 6: emojisToShow = .six + case 7: emojisToShow = .seven + case 8: emojisToShow = .eight + case 9: emojisToShow = .nine + default: emojisToShow = .zero + } + + if commandState == .colonToEmoji { + autoAction0Visible = false + autoAction2Visible = false + } + + let dividerColor: UIColor + if UITraitCollection.current.userInterfaceStyle == .light { + dividerColor = specialKeyColor + } else { + dividerColor = UIColor(cgColor: commandBarPlaceholderColorCG) + } + + if DeviceType.isPad { + let padDividers: [UILabel] = [padEmojiDivider0, padEmojiDivider1, padEmojiDivider2, padEmojiDivider3, padEmojiDivider4] + for i in 0 ..< padDividers.count where emojisToShow.rawValue > i + 1 { + padDividers[i].backgroundColor = dividerColor + } + } else if DeviceType.isPhone { + let phoneDividers: [UILabel] = [phoneEmojiDivider, phoneEmojiDivider1] + for i in 0 ..< phoneDividers.count where emojisToShow.rawValue > i + 1 { + phoneDividers[i].backgroundColor = dividerColor + } + } + conditionallyHideEmojiDividers() + } else { + emojisToShow = .zero + } + } + /// Generates an array of the three autocomplete words. func getAutocompletions() { completionWords = [" ", " ", " "] @@ -649,164 +713,197 @@ class KeyboardViewController: UIInputViewController { autoActionAnnotationSeparators.forEach { $0.removeFromSuperview() } autoActionAnnotationSeparators.removeAll() - if autoActionState == .suggest { + if commandState == .colonToEmoji { + getEmojiAutoSuggestionsPatternMatching(for: colonSearchString) + } else if autoActionState == .suggest { getAutosuggestions() } else { getAutocompletions() } - if commandState == .idle { + if [.idle, .colonToEmoji].contains(commandState) { deactivateBtn(btn: translateKey) deactivateBtn(btn: conjugateKey) deactivateBtn(btn: pluralKey) deactivateBtn(btn: phoneEmojiKey0) deactivateBtn(btn: phoneEmojiKey1) + deactivateBtn(btn: phoneEmojiKey2) + deactivateBtn(btn: phoneEmojiKey3) + deactivateBtn(btn: phoneEmojiKey4) + deactivateBtn(btn: phoneEmojiKey5) deactivateBtn(btn: padEmojiKey0) deactivateBtn(btn: padEmojiKey1) deactivateBtn(btn: padEmojiKey2) + deactivateBtn(btn: padEmojiKey3) + deactivateBtn(btn: padEmojiKey4) + deactivateBtn(btn: padEmojiKey5) + deactivateBtn(btn: padEmojiKey6) + deactivateBtn(btn: padEmojiKey7) + deactivateBtn(btn: padEmojiKey8) 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 - } - 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) + if commandState == .colonToEmoji && emojisToShow != .zero { + let emojiButtons: [UIButton] + if DeviceType.isPad { + emojiButtons = [translateKey, conjugateKey, pluralKey, padEmojiKey0, padEmojiKey1, padEmojiKey2, padEmojiKey3, padEmojiKey4, padEmojiKey5] + } else { + emojiButtons = [translateKey, conjugateKey, pluralKey, phoneEmojiKey0, phoneEmojiKey1, phoneEmojiKey2] } - 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 - } + for (index, emoji) in emojisToDisplayArray.enumerated() where index < emojiButtons.count { + let btn = emojiButtons[index] + setBtn(btn: btn, color: keyboardBgColor, name: "EmojiKey\(index)", canBeCapitalized: false, isSpecial: false) + styleBtn(btn: btn, title: emoji, radius: commandKeyCornerRadius) + if DeviceType.isPhone { + btn.titleLabel?.font = .systemFont(ofSize: scribeKey.frame.height * scalarFontPhone) + } else { + btn.titleLabel?.font = .systemFont(ofSize: scribeKey.frame.height * scalarFontPad) + } + activateBtn(btn: btn) + } + conditionallyHideEmojiDividers() + } else { + 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) + } - 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 - ) + // 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 { + completionWords[1] = currentPrefix + } - if autoAction2Visible && emojisToShow == .zero { setBtn( - btn: pluralKey, - color: keyboardBgColor, - name: "AutoAction2", + btn: conjugateKey, + color: keyboardBgColor, name: "AutoAction1", canBeCapitalized: false, isSpecial: false ) styleBtn( - btn: pluralKey, - title: !autoAction0Visible ? completionWords[1] : completionWords[2], + btn: conjugateKey, + title: !autoAction0Visible ? completionWords[0] : completionWords[1], radius: commandKeyCornerRadius ) - if pluralKey.currentTitle != " " { - activateBtn(btn: pluralKey) + if conjugateKey.currentTitle != " " { + activateBtn(btn: conjugateKey) } autoActionAnnotation( - autoActionWord: !autoAction0Visible ? completionWords[1] : completionWords[2], index: 2, KVC: self + autoActionWord: !autoAction0Visible ? completionWords[0] : completionWords[1], index: 1, 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) - } - 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 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 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) - } + 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) - activateBtn(btn: phoneEmojiKey0) - activateBtn(btn: phoneEmojiKey1) + conditionallyHideEmojiDividers() + } else if !autoAction2Visible && emojisToShow.rawValue >= 2 { + if DeviceType.isPhone || 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) + } - 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) + activateBtn(btn: phoneEmojiKey0) + activateBtn(btn: phoneEmojiKey1) + } else if DeviceType.isPad && emojisToShow.rawValue >= 3 { + 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() + conditionallyHideEmojiDividers() + } } translateKey.layer.shadowColor = UIColor.clear.cgColor @@ -859,6 +956,16 @@ class KeyboardViewController: UIInputViewController { allowUndo = false } + if commandState == .colonToEmoji { + for _ in 0 ... colonSearchString.count { + proxy.deleteBackward() + } + proxy.insertText(keyPressed.titleLabel?.text ?? "") + commandState = .idle + loadKeys() + return + } + clearPrefixFromTextFieldProxy() emojisToDisplayArray = [String]() // Remove the space from the previous auto action or replace the current prefix. @@ -961,13 +1068,27 @@ class KeyboardViewController: UIInputViewController { @IBOutlet var phoneEmojiKey0: UIButton! @IBOutlet var phoneEmojiKey1: UIButton! + @IBOutlet var phoneEmojiKey2: UIButton! + @IBOutlet var phoneEmojiKey3: UIButton! + @IBOutlet var phoneEmojiKey4: UIButton! + @IBOutlet var phoneEmojiKey5: UIButton! @IBOutlet var phoneEmojiDivider: UILabel! + @IBOutlet var phoneEmojiDivider1: UILabel! @IBOutlet var padEmojiKey0: UIButton! @IBOutlet var padEmojiKey1: UIButton! @IBOutlet var padEmojiKey2: UIButton! + @IBOutlet var padEmojiKey3: UIButton! + @IBOutlet var padEmojiKey4: UIButton! + @IBOutlet var padEmojiKey5: UIButton! + @IBOutlet var padEmojiKey6: UIButton! + @IBOutlet var padEmojiKey7: UIButton! + @IBOutlet var padEmojiKey8: UIButton! @IBOutlet var padEmojiDivider0: UILabel! @IBOutlet var padEmojiDivider1: UILabel! + @IBOutlet var padEmojiDivider2: UILabel! + @IBOutlet var padEmojiDivider3: UILabel! + @IBOutlet var padEmojiDivider4: UILabel! /// Sets up all buttons that are associated with Scribe commands. func setCommandBtns() { @@ -984,18 +1105,30 @@ class KeyboardViewController: UIInputViewController { /// Hides all emoji dividers based on conditions determined by the keyboard state. func conditionallyHideEmojiDividers() { + let dividers: [UILabel] + if DeviceType.isPhone { + dividers = [phoneEmojiDivider, phoneEmojiDivider1] + } else { + dividers = [padEmojiDivider0, padEmojiDivider1, padEmojiDivider2, padEmojiDivider3, padEmojiDivider4] + } + if commandState == .idle { if [.zero, .one, .three].contains(emojisToShow) { phoneEmojiDivider.backgroundColor = .clear } + phoneEmojiDivider1.backgroundColor = .clear + if [.zero, .one, .two].contains(emojisToShow) { padEmojiDivider0.backgroundColor = .clear padEmojiDivider1.backgroundColor = .clear } + padEmojiDivider2.backgroundColor = .clear + padEmojiDivider3.backgroundColor = .clear + padEmojiDivider4.backgroundColor = .clear } else { - phoneEmojiDivider.backgroundColor = .clear - padEmojiDivider0.backgroundColor = .clear - padEmojiDivider1.backgroundColor = .clear + for divider in dividers { + divider.backgroundColor = .clear + } } } @@ -1670,9 +1803,19 @@ class KeyboardViewController: UIInputViewController { deactivateBtn(btn: translateKey) deactivateBtn(btn: phoneEmojiKey0) deactivateBtn(btn: phoneEmojiKey1) + deactivateBtn(btn: phoneEmojiKey2) + deactivateBtn(btn: phoneEmojiKey3) + deactivateBtn(btn: phoneEmojiKey4) + deactivateBtn(btn: phoneEmojiKey5) deactivateBtn(btn: padEmojiKey0) deactivateBtn(btn: padEmojiKey1) deactivateBtn(btn: padEmojiKey2) + deactivateBtn(btn: padEmojiKey3) + deactivateBtn(btn: padEmojiKey4) + deactivateBtn(btn: padEmojiKey5) + deactivateBtn(btn: padEmojiKey6) + deactivateBtn(btn: padEmojiKey7) + deactivateBtn(btn: padEmojiKey8) if [.translate, .conjugate, .plural].contains(commandState) { scribeKey.setPartialCornerRadius() @@ -1727,7 +1870,7 @@ class KeyboardViewController: UIInputViewController { if let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") { let dictionaryKey = langCode + "DoubleSpacePeriods" - return userDefaults.bool(forKey: dictionaryKey) + return userDefaults.object(forKey: dictionaryKey) as? Bool ?? true } else { return true // return the default value } @@ -1738,7 +1881,7 @@ class KeyboardViewController: UIInputViewController { if let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") { let dictionaryKey = langCode + "EmojiAutosuggest" - return userDefaults.bool(forKey: dictionaryKey) + return userDefaults.object(forKey: dictionaryKey) as? Bool ?? true } else { return true // return the default value } @@ -1756,6 +1899,17 @@ class KeyboardViewController: UIInputViewController { } + func colonToEmojiIsEnabled() -> Bool { + let langCode = languagesAbbrDict[controllerLanguage] ?? "unknown" + if let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") { + let dictionaryKey = langCode + "ColonToEmoji" + + return userDefaults.object(forKey: dictionaryKey) as? Bool ?? true + } else { + return true // return the default value + } + } + // MARK: Button Actions /// Triggers actions based on the press of a key. @@ -1976,30 +2130,8 @@ class KeyboardViewController: UIInputViewController { 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 "EmojiKey2": - executeAutoAction(keyPressed: padEmojiKey2) + case "EmojiKey0", "EmojiKey1", "EmojiKey2", "EmojiKey3", "EmojiKey4", "EmojiKey5": + executeAutoAction(keyPressed: sender) if shiftButtonState == .normal { shiftButtonState = .shift } @@ -2062,6 +2194,15 @@ class KeyboardViewController: UIInputViewController { pastStringInTextProxy = "" } + if commandState == .colonToEmoji { + if !colonSearchString.isEmpty { + colonSearchString.removeLast() + } else { + commandState = .idle + loadKeys() + } + } + handleDeleteButtonPressed() autoCapAtStartOfProxy() @@ -2216,7 +2357,20 @@ class KeyboardViewController: UIInputViewController { shiftButtonState = .normal loadKeys() } - if [.idle, .selectCommand, .alreadyPlural, .invalid].contains(commandState) { + + if keyToDisplay == ":" && commandState == .idle && colonToEmojiIsEnabled() { + commandState = .colonToEmoji + colonSearchString = "" + } else if commandState == .colonToEmoji { + if keyToDisplay.rangeOfCharacter(from: CharacterSet.alphanumerics) != nil { + colonSearchString += keyToDisplay + } else { + commandState = .idle + loadKeys() + } + } + + if [.idle, .selectCommand, .alreadyPlural, .invalid, .colonToEmoji].contains(commandState) { proxy.insertText(keyToDisplay) } else { if let currentText = commandBar.text { @@ -2233,7 +2387,7 @@ class KeyboardViewController: UIInputViewController { // Reset emoji repeat functionality. if !( - ["EmojiKey0", "EmojiKey1", "EmojiKey2"].contains(originalKey) + ["EmojiKey0", "EmojiKey1", "EmojiKey2", "EmojiKey3", "EmojiKey4", "EmojiKey5"].contains(originalKey) || (originalKey == "AutoAction2" && emojisToShow == .one) ) { emojiAutoActionRepeatPossible = false @@ -2470,7 +2624,6 @@ class KeyboardViewController: UIInputViewController { shiftButtonState = .shift loadKeys() } - // Show auto actions if the keyboard states dictate. conditionallySetAutoActionBtns() } } diff --git a/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift b/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift index 7ae6d22d..d8d31374 100644 --- a/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift +++ b/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift @@ -14,16 +14,23 @@ var autoAction0Visible = true var autoAction2Visible = true /// States of the emoji display corresponding to the number to show. -enum EmojisToShow { - case zero - case one - case two - case three +enum EmojisToShow: Int { + case zero = 0 + case one = 1 + case two = 2 + case three = 3 + case four = 4 + case five = 5 + case six = 6 + case seven = 7 + case eight = 8 + case nine = 9 } var emojisToShow: EmojisToShow = .zero var currentEmojiTriggerWord = "" var emojiAutoActionRepeatPossible = false +var colonSearchString = "" var firstCompletionIsHighlighted = false var spaceAutoInsertIsPossible = false diff --git a/Scribe.xcodeproj/project.pbxproj b/Scribe.xcodeproj/project.pbxproj index 233f3833..2df09c81 100644 --- a/Scribe.xcodeproj/project.pbxproj +++ b/Scribe.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ 14AC56842A24AED3006B1DDF /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14AC56832A24AED3006B1DDF /* AboutViewController.swift */; }; 14AC568A2A261663006B1DDF /* InformationScreenVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14AC56892A261663006B1DDF /* InformationScreenVC.swift */; }; 1900C00E2C88BF980017A874 /* TestKeyboardStyling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1900C00D2C88BF980017A874 /* TestKeyboardStyling.swift */; }; + A1B2C3D4E5F60708090A0B02 /* KeyboardCommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60708090A0B01 /* KeyboardCommandTests.swift */; }; + A1B2C3D4E5F60708090A0B04 /* EmojiQueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60708090A0B03 /* EmojiQueryTests.swift */; }; 198369CC2C7980BA00C1B583 /* KeyboardProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 198369CB2C7980BA00C1B583 /* KeyboardProvider.swift */; }; 198369CD2C7980BA00C1B583 /* KeyboardProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 198369CB2C7980BA00C1B583 /* KeyboardProvider.swift */; }; 198369CE2C7980BA00C1B583 /* KeyboardProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 198369CB2C7980BA00C1B583 /* KeyboardProvider.swift */; }; @@ -1075,6 +1077,8 @@ 14AC56832A24AED3006B1DDF /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; 14AC56892A261663006B1DDF /* InformationScreenVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InformationScreenVC.swift; sourceTree = ""; }; 1900C00D2C88BF980017A874 /* TestKeyboardStyling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestKeyboardStyling.swift; sourceTree = ""; }; + A1B2C3D4E5F60708090A0B01 /* KeyboardCommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardCommandTests.swift; sourceTree = ""; }; + A1B2C3D4E5F60708090A0B03 /* EmojiQueryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiQueryTests.swift; sourceTree = ""; }; 198369CB2C7980BA00C1B583 /* KeyboardProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardProvider.swift; sourceTree = ""; }; 19DC85F92C7772FC006E32FD /* KeyboardBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardBuilder.swift; sourceTree = ""; }; 30453963293B9D18003AE55B /* InformationToolTipData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InformationToolTipData.swift; sourceTree = ""; }; @@ -1970,6 +1974,8 @@ children = ( D13E0DC82C86530E007F00AF /* TestExtensions.swift */, 1900C00D2C88BF980017A874 /* TestKeyboardStyling.swift */, + A1B2C3D4E5F60708090A0B01 /* KeyboardCommandTests.swift */, + A1B2C3D4E5F60708090A0B03 /* EmojiQueryTests.swift */, ); path = KeyboardsBase; sourceTree = ""; @@ -3163,6 +3169,8 @@ files = ( 693150472C881DCE005F99E8 /* BaseTableViewControllerTest.swift in Sources */, 1900C00E2C88BF980017A874 /* TestKeyboardStyling.swift in Sources */, + A1B2C3D4E5F60708090A0B02 /* KeyboardCommandTests.swift in Sources */, + A1B2C3D4E5F60708090A0B04 /* EmojiQueryTests.swift in Sources */, D13E0DC92C86530E007F00AF /* TestExtensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Scribe/ParentTableCellModel.swift b/Scribe/ParentTableCellModel.swift index 58d21d56..94a372da 100644 --- a/Scribe/ParentTableCellModel.swift +++ b/Scribe/ParentTableCellModel.swift @@ -70,6 +70,7 @@ enum UserInteractiveState { case autosuggestEmojis case toggleAccentCharacters case toggleWordForWordDeletion + case colonToEmoji case none } diff --git a/Scribe/SettingsTab/SettingsTableData.swift b/Scribe/SettingsTab/SettingsTableData.swift index d6c0ba5b..1eed1295 100644 --- a/Scribe/SettingsTab/SettingsTableData.swift +++ b/Scribe/SettingsTab/SettingsTableData.swift @@ -74,10 +74,32 @@ enum SettingsTableData { 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: ""), + 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: "") + 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: "" + ) + ), + Section( + sectionTitle: NSLocalizedString( + "i18n.app.settings.keyboard.functionality.colon_to_emoji", + value: "Colon to emoji entry", + comment: "" + ), + hasToggle: true, + sectionState: .none(.colonToEmoji), + shortDescription: NSLocalizedString( + "i18n.app.settings.keyboard.functionality.colon_to_emoji_description", + value: "Type : followed by a keyword to suggest emojis.", + comment: "" + ) ) ], hasDynamicData: nil diff --git a/Scribe/Views/Cells/InfoChildTableViewCell/InfoChildTableViewCell.swift b/Scribe/Views/Cells/InfoChildTableViewCell/InfoChildTableViewCell.swift index a27303fd..fed8e652 100644 --- a/Scribe/Views/Cells/InfoChildTableViewCell/InfoChildTableViewCell.swift +++ b/Scribe/Views/Cells/InfoChildTableViewCell/InfoChildTableViewCell.swift @@ -151,6 +151,10 @@ final class InfoChildTableViewCell: UITableViewCell { let dictionaryKey = languageCode + "WordForWordDeletion" userDefaults.setValue(toggleSwitch.isOn, forKey: dictionaryKey) + case .colonToEmoji: + let dictionaryKey = languageCode + "ColonToEmoji" + userDefaults.setValue(toggleSwitch.isOn, forKey: dictionaryKey) + case .none: break } @@ -164,7 +168,7 @@ final class InfoChildTableViewCell: UITableViewCell { if let toggleValue = userDefaults.object(forKey: dictionaryKey) as? Bool { toggleSwitch.isOn = toggleValue } else { - toggleSwitch.isOn = false // Default value + toggleSwitch.isOn = false // default value } case .toggleAccentCharacters: @@ -172,7 +176,7 @@ final class InfoChildTableViewCell: UITableViewCell { if let toggleValue = userDefaults.object(forKey: dictionaryKey) as? Bool { toggleSwitch.isOn = toggleValue } else { - toggleSwitch.isOn = false // Default value + toggleSwitch.isOn = false // default value } case .doubleSpacePeriods: @@ -180,7 +184,7 @@ final class InfoChildTableViewCell: UITableViewCell { if let toggleValue = userDefaults.object(forKey: dictionaryKey) as? Bool { toggleSwitch.isOn = toggleValue } else { - toggleSwitch.isOn = true // Default value + toggleSwitch.isOn = true // default value } case .autosuggestEmojis: @@ -188,7 +192,7 @@ final class InfoChildTableViewCell: UITableViewCell { if let toggleValue = userDefaults.object(forKey: dictionaryKey) as? Bool { toggleSwitch.isOn = toggleValue } else { - toggleSwitch.isOn = true // Default value + toggleSwitch.isOn = true // default value } case .toggleWordForWordDeletion: @@ -196,7 +200,15 @@ final class InfoChildTableViewCell: UITableViewCell { if let toggleValue = userDefaults.object(forKey: dictionaryKey) as? Bool { toggleSwitch.isOn = toggleValue } else { - toggleSwitch.isOn = false // Default value + toggleSwitch.isOn = false // default value + } + + case .colonToEmoji: + let dictionaryKey = languageCode + "ColonToEmoji" + if let toggleValue = userDefaults.object(forKey: dictionaryKey) as? Bool { + toggleSwitch.isOn = toggleValue + } else { + toggleSwitch.isOn = true // default value } case .none: break diff --git a/Tests/Keyboards/KeyboardsBase/EmojiQueryTests.swift b/Tests/Keyboards/KeyboardsBase/EmojiQueryTests.swift new file mode 100644 index 00000000..79e8c07a --- /dev/null +++ b/Tests/Keyboards/KeyboardsBase/EmojiQueryTests.swift @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import XCTest + +@testable import Scribe + +class EmojiQueryTests: XCTestCase { + + override func setUpWithError() throws { + controllerLanguage = "English" + } + + func testQueryEmojisPatternMatchingWithCommonKeyword() { + // This test assumes the database is populated with some emojis. + // If not, it might return empty, which we also handle. + let keyword = "happ" + let results = LanguageDBManager.shared.queryEmojisPatternMatching(of: keyword) + + XCTAssertEqual( + results.count, 9, "Should always return 9 elements (including empty strings)" + ) + } + + func testQueryEmojisPatternMatchingWithEmptyKeyword() { + let results = LanguageDBManager.shared.queryEmojisPatternMatching(of: "") + XCTAssertEqual(results.count, 9) + } + + func testQueryEmojisPatternMatchingWithNonExistentKeyword() { + let results = LanguageDBManager.shared.queryEmojisPatternMatching( + of: "nonexistentkeyword12345" + ) + XCTAssertEqual(results.count, 9) + XCTAssertEqual(results[0], "") + XCTAssertEqual(results[1], "") + XCTAssertEqual(results[2], "") + XCTAssertEqual(results[3], "") + XCTAssertEqual(results[4], "") + XCTAssertEqual(results[5], "") + XCTAssertEqual(results[6], "") + XCTAssertEqual(results[7], "") + XCTAssertEqual(results[8], "") + } +} diff --git a/Tests/Keyboards/KeyboardsBase/KeyboardCommandTests.swift b/Tests/Keyboards/KeyboardsBase/KeyboardCommandTests.swift new file mode 100644 index 00000000..9d23eac3 --- /dev/null +++ b/Tests/Keyboards/KeyboardsBase/KeyboardCommandTests.swift @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import XCTest + +@testable import Scribe + +class KeyboardCommandTests: XCTestCase { + func testColonToEmojiIsEnabled() { + let keyboard = KeyboardViewController() + // Default should be true as per the implementation. + XCTAssertTrue(keyboard.colonToEmojiIsEnabled()) + } +}