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())
+ }
+}