From 7648193a50c0bedeb59c5961c13f7ac498c54811 Mon Sep 17 00:00:00 2001 From: Jatin Foujdar Date: Fri, 9 Jan 2026 04:26:30 +0530 Subject: [PATCH 1/9] Add colon-to-emoji toggle feature to settings and keyboard --- .../KeyboardsBase/InterfaceVariables.swift | 1 + .../KeyboardViewController.swift | 96 ++++++++++++++++++- .../KeyboardsBase/LanguageDBManager.swift | 41 ++++++++ .../CommandVariables.swift | 1 + Scribe/ParentTableCellModel.swift | 1 + Scribe/SettingsTab/SettingsTableData.swift | 6 ++ .../InfoChildTableViewCell.swift | 12 +++ 7 files changed, 155 insertions(+), 3 deletions(-) diff --git a/Keyboards/KeyboardsBase/InterfaceVariables.swift b/Keyboards/KeyboardsBase/InterfaceVariables.swift index cf34a93c..3903d907 100644 --- a/Keyboards/KeyboardsBase/InterfaceVariables.swift +++ b/Keyboards/KeyboardsBase/InterfaceVariables.swift @@ -96,6 +96,7 @@ enum CommandState { case alreadyPlural case invalid case displayInformation + case colonToEmoji } /// States of the keyboard corresponding to which auto actions should be presented. diff --git a/Keyboards/KeyboardsBase/KeyboardViewController.swift b/Keyboards/KeyboardsBase/KeyboardViewController.swift index 97bc06b9..7f47067b 100644 --- a/Keyboards/KeyboardsBase/KeyboardViewController.swift +++ b/Keyboards/KeyboardsBase/KeyboardViewController.swift @@ -404,6 +404,51 @@ class KeyboardViewController: UIInputViewController { } } + func getEmojiAutoSuggestionsPatternMatching(for word: String) { + let emojisToDisplay = LanguageDBManager.shared.queryEmojisPatternMatching(of: 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 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]) + + emojisToShow = .one + } + } else { + emojisToShow = .zero + } + } + /// Generates an array of the three autocomplete words. func getAutocompletions() { completionWords = [" ", " ", " "] @@ -633,12 +678,14 @@ 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) @@ -843,6 +890,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. @@ -2330,6 +2387,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.bool(forKey: dictionaryKey) + } else { + return true // return the default value + } + } + // MARK: Button Actions /// Triggers actions based on the press of a key. @@ -2716,6 +2784,15 @@ class KeyboardViewController: UIInputViewController { pastStringInTextProxy = "" } + if commandState == .colonToEmoji { + if !colonSearchString.isEmpty { + colonSearchString.removeLast() + } else { + commandState = .idle + loadKeys() + } + } + handleDeleteButtonPressed() autoCapAtStartOfProxy() @@ -2870,7 +2947,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 { diff --git a/Keyboards/KeyboardsBase/LanguageDBManager.swift b/Keyboards/KeyboardsBase/LanguageDBManager.swift index ac49e68f..7e8c23cb 100644 --- a/Keyboards/KeyboardsBase/LanguageDBManager.swift +++ b/Keyboards/KeyboardsBase/LanguageDBManager.swift @@ -268,6 +268,47 @@ 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 { + outputValues.append(val) + if outputValues.count == 3 { return } + } + } + } + } + } catch {} + + while outputValues.count < 3 { + outputValues.append("") + } + + return Array(outputValues.prefix(3)) + } + /// Query the noun form of word in `nonuns`. func queryNounForm(of word: String) -> [String] { let query = """ diff --git a/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift b/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift index 7ae6d22d..d26c50e2 100644 --- a/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift +++ b/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift @@ -24,6 +24,7 @@ enum EmojisToShow { var emojisToShow: EmojisToShow = .zero var currentEmojiTriggerWord = "" var emojiAutoActionRepeatPossible = false +var colonSearchString = "" var firstCompletionIsHighlighted = false var spaceAutoInsertIsPossible = false 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 44ee9fe6..4544c4ee 100644 --- a/Scribe/SettingsTab/SettingsTableData.swift +++ b/Scribe/SettingsTab/SettingsTableData.swift @@ -78,6 +78,12 @@ enum SettingsTableData { hasToggle: true, sectionState: .none(.toggleWordForWordDeletion), shortDescription: NSLocalizedString("i18n.app.word_for_word.description", value: "Word for word deletion.", 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..97395e07 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 } @@ -199,6 +203,14 @@ final class InfoChildTableViewCell: UITableViewCell { 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 } } From d20a1cfdea24e2d164fc23ac4c04f732dadc258b Mon Sep 17 00:00:00 2001 From: Jatin Foujdar Date: Fri, 9 Jan 2026 05:06:56 +0530 Subject: [PATCH 2/9] Add colon-to-emoji toggle feature --- .../KeyboardsBase/EmojiQueryTests.swift | 30 +++++++++++++++++++ .../KeyboardsBase/KeyboardCommandTests.swift | 17 +++++++++++ 2 files changed, 47 insertions(+) create mode 100644 Tests/Keyboards/KeyboardsBase/EmojiQueryTests.swift create mode 100644 Tests/Keyboards/KeyboardsBase/KeyboardCommandTests.swift diff --git a/Tests/Keyboards/KeyboardsBase/EmojiQueryTests.swift b/Tests/Keyboards/KeyboardsBase/EmojiQueryTests.swift new file mode 100644 index 00000000..1ff6485f --- /dev/null +++ b/Tests/Keyboards/KeyboardsBase/EmojiQueryTests.swift @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +@testable import Scribe +import XCTest + +class EmojiQueryTests: XCTestCase { + + 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, 3, "Should always return 3 elements (including empty strings)") + } + + func testQueryEmojisPatternMatchingWithEmptyKeyword() { + let results = LanguageDBManager.shared.queryEmojisPatternMatching(of: "") + XCTAssertEqual(results.count, 3) + } + + func testQueryEmojisPatternMatchingWithNonExistentKeyword() { + let results = LanguageDBManager.shared.queryEmojisPatternMatching(of: "nonexistentkeyword12345") + XCTAssertEqual(results.count, 3) + XCTAssertEqual(results[0], "") + XCTAssertEqual(results[1], "") + XCTAssertEqual(results[2], "") + } +} diff --git a/Tests/Keyboards/KeyboardsBase/KeyboardCommandTests.swift b/Tests/Keyboards/KeyboardsBase/KeyboardCommandTests.swift new file mode 100644 index 00000000..33a030e1 --- /dev/null +++ b/Tests/Keyboards/KeyboardsBase/KeyboardCommandTests.swift @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +@testable import Scribe +import XCTest + +class KeyboardCommandTests: XCTestCase { + + func testColonToEmojiIsEnabled() { + let keyboard = KeyboardViewController() + // Default should be true as per the implementation + XCTAssertTrue(keyboard.colonToEmojiIsEnabled()) + } + + // Note: Testing UI state transitions often requires a more complex setup + // with UIInputViewController and its proxy. Here we test the logic we can. +} From 655bb6be0e92422a0b1bb379d6f1f5d3b0438572 Mon Sep 17 00:00:00 2001 From: Jatin Foujdar Date: Sun, 11 Jan 2026 23:26:14 +0530 Subject: [PATCH 3/9] style: fix swiftlint trailing whitespace violations in tests --- Tests/Keyboards/KeyboardsBase/EmojiQueryTests.swift | 8 ++++---- Tests/Keyboards/KeyboardsBase/KeyboardCommandTests.swift | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/Keyboards/KeyboardsBase/EmojiQueryTests.swift b/Tests/Keyboards/KeyboardsBase/EmojiQueryTests.swift index 1ff6485f..dd159c9c 100644 --- a/Tests/Keyboards/KeyboardsBase/EmojiQueryTests.swift +++ b/Tests/Keyboards/KeyboardsBase/EmojiQueryTests.swift @@ -5,21 +5,21 @@ import Foundation import XCTest class EmojiQueryTests: XCTestCase { - + 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, 3, "Should always return 3 elements (including empty strings)") } - + func testQueryEmojisPatternMatchingWithEmptyKeyword() { let results = LanguageDBManager.shared.queryEmojisPatternMatching(of: "") XCTAssertEqual(results.count, 3) } - + func testQueryEmojisPatternMatchingWithNonExistentKeyword() { let results = LanguageDBManager.shared.queryEmojisPatternMatching(of: "nonexistentkeyword12345") XCTAssertEqual(results.count, 3) diff --git a/Tests/Keyboards/KeyboardsBase/KeyboardCommandTests.swift b/Tests/Keyboards/KeyboardsBase/KeyboardCommandTests.swift index 33a030e1..e18ff4c1 100644 --- a/Tests/Keyboards/KeyboardsBase/KeyboardCommandTests.swift +++ b/Tests/Keyboards/KeyboardsBase/KeyboardCommandTests.swift @@ -5,13 +5,13 @@ import Foundation import XCTest class KeyboardCommandTests: XCTestCase { - + func testColonToEmojiIsEnabled() { let keyboard = KeyboardViewController() // Default should be true as per the implementation XCTAssertTrue(keyboard.colonToEmojiIsEnabled()) } - + // Note: Testing UI state transitions often requires a more complex setup // with UIInputViewController and its proxy. Here we test the logic we can. } From ad144757e4235ebe7e91f2e71645d9da086bbbcc Mon Sep 17 00:00:00 2001 From: Jatin Foujdar Date: Mon, 12 Jan 2026 00:07:27 +0530 Subject: [PATCH 4/9] Toggle colon to emoji --- Keyboards/KeyboardsBase/KeyboardViewController.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Keyboards/KeyboardsBase/KeyboardViewController.swift b/Keyboards/KeyboardsBase/KeyboardViewController.swift index 7f47067b..1d11f207 100644 --- a/Keyboards/KeyboardsBase/KeyboardViewController.swift +++ b/Keyboards/KeyboardsBase/KeyboardViewController.swift @@ -2358,7 +2358,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 } @@ -2369,7 +2369,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 } @@ -2392,7 +2392,7 @@ class KeyboardViewController: UIInputViewController { if let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer") { let dictionaryKey = langCode + "ColonToEmoji" - return userDefaults.bool(forKey: dictionaryKey) + return userDefaults.object(forKey: dictionaryKey) as? Bool ?? true } else { return true // return the default value } From c5025ad28d01d80447ae581bacd645c08d61c25d Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Sun, 11 Jan 2026 19:56:44 +0100 Subject: [PATCH 5/9] Minor update of comments for code --- .../InfoChildTableViewCell.swift | 12 ++++++------ .../KeyboardsBase/KeyboardCommandTests.swift | 5 +---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Scribe/Views/Cells/InfoChildTableViewCell/InfoChildTableViewCell.swift b/Scribe/Views/Cells/InfoChildTableViewCell/InfoChildTableViewCell.swift index 97395e07..fed8e652 100644 --- a/Scribe/Views/Cells/InfoChildTableViewCell/InfoChildTableViewCell.swift +++ b/Scribe/Views/Cells/InfoChildTableViewCell/InfoChildTableViewCell.swift @@ -168,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: @@ -176,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: @@ -184,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: @@ -192,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: @@ -200,7 +200,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 .colonToEmoji: @@ -208,7 +208,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 .none: break diff --git a/Tests/Keyboards/KeyboardsBase/KeyboardCommandTests.swift b/Tests/Keyboards/KeyboardsBase/KeyboardCommandTests.swift index e18ff4c1..7cc89d1c 100644 --- a/Tests/Keyboards/KeyboardsBase/KeyboardCommandTests.swift +++ b/Tests/Keyboards/KeyboardsBase/KeyboardCommandTests.swift @@ -8,10 +8,7 @@ class KeyboardCommandTests: XCTestCase { func testColonToEmojiIsEnabled() { let keyboard = KeyboardViewController() - // Default should be true as per the implementation + // Default should be true as per the implementation. XCTAssertTrue(keyboard.colonToEmojiIsEnabled()) } - - // Note: Testing UI state transitions often requires a more complex setup - // with UIInputViewController and its proxy. Here we test the logic we can. } From a2bd368afbd7d7647d0cf83165812eb88c0e4043 Mon Sep 17 00:00:00 2001 From: Jatin Foujdar Date: Tue, 13 Jan 2026 20:23:56 +0530 Subject: [PATCH 6/9] fix: clean up emoji suggestion UI to distinguish from word suggestions --- .../KeyboardViewController.swift | 330 +++++++++--------- 1 file changed, 167 insertions(+), 163 deletions(-) diff --git a/Keyboards/KeyboardsBase/KeyboardViewController.swift b/Keyboards/KeyboardsBase/KeyboardViewController.swift index 1d11f207..ef3b0388 100644 --- a/Keyboards/KeyboardsBase/KeyboardViewController.swift +++ b/Keyboards/KeyboardsBase/KeyboardViewController.swift @@ -407,17 +407,30 @@ class KeyboardViewController: UIInputViewController { func getEmojiAutoSuggestionsPatternMatching(for word: String) { let emojisToDisplay = LanguageDBManager.shared.queryEmojisPatternMatching(of: word.lowercased()) + emojisToDisplayArray = [String]() if !emojisToDisplay[0].isEmpty { - emojisToDisplayArray = [String]() currentEmojiTriggerWord = ":" + word.lowercased() - if !emojisToDisplay[2].isEmpty && DeviceType.isPad { - for i in 0 ..< 3 { - emojisToDisplayArray.append(emojisToDisplay[i]) - } + 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 + default: emojisToShow = .zero + } + + if commandState == .colonToEmoji { + autoAction0Visible = false autoAction2Visible = false - emojisToShow = .three + } + if DeviceType.isPad && emojisToShow.rawValue >= 2 { if UITraitCollection.current.userInterfaceStyle == .light { padEmojiDivider0.backgroundColor = specialKeyColor padEmojiDivider1.backgroundColor = specialKeyColor @@ -425,25 +438,14 @@ class KeyboardViewController: UIInputViewController { 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 - + } else if DeviceType.isPhone && emojisToShow.rawValue >= 2 { 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]) - - emojisToShow = .one } + conditionallyHideEmojiDividers() } else { emojisToShow = .zero } @@ -700,144 +702,168 @@ class KeyboardViewController: UIInputViewController { 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] + } else { + emojiButtons = [translateKey, conjugateKey, pluralKey, phoneEmojiKey0, phoneEmojiKey1] } - 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() { + if 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 { + // spaceAutoInsertIsPossible = true + 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 @@ -2698,30 +2724,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 } @@ -2977,7 +2981,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 From 4359e6e383bf0041faf5e6a9eeb74e73f1686035 Mon Sep 17 00:00:00 2001 From: Jatin Foujdar Date: Tue, 13 Jan 2026 23:30:09 +0530 Subject: [PATCH 7/9] Populate leftmost emoji button with first suggestion and expand database --- Keyboards/KeyboardsBase/LanguageDBManager.swift | 10 ++++++---- .../ScribeFunctionality/CommandVariables.swift | 13 ++++++++----- Tests/Keyboards/KeyboardsBase/EmojiQueryTests.swift | 9 ++++++--- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/Keyboards/KeyboardsBase/LanguageDBManager.swift b/Keyboards/KeyboardsBase/LanguageDBManager.swift index 7e8c23cb..cf253b0c 100644 --- a/Keyboards/KeyboardsBase/LanguageDBManager.swift +++ b/Keyboards/KeyboardsBase/LanguageDBManager.swift @@ -294,19 +294,21 @@ extension LanguageDBManager { 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 { - outputValues.append(val) - if outputValues.count == 3 { return } + if !outputValues.contains(val) { + outputValues.append(val) + } + if outputValues.count == 6 { return } } } } } } catch {} - while outputValues.count < 3 { + while outputValues.count < 6 { outputValues.append("") } - return Array(outputValues.prefix(3)) + return Array(outputValues.prefix(6)) } /// Query the noun form of word in `nonuns`. diff --git a/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift b/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift index d26c50e2..bff57fa4 100644 --- a/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift +++ b/Keyboards/KeyboardsBase/ScribeFunctionality/CommandVariables.swift @@ -14,11 +14,14 @@ 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 } var emojisToShow: EmojisToShow = .zero diff --git a/Tests/Keyboards/KeyboardsBase/EmojiQueryTests.swift b/Tests/Keyboards/KeyboardsBase/EmojiQueryTests.swift index dd159c9c..59ccc5fb 100644 --- a/Tests/Keyboards/KeyboardsBase/EmojiQueryTests.swift +++ b/Tests/Keyboards/KeyboardsBase/EmojiQueryTests.swift @@ -12,19 +12,22 @@ class EmojiQueryTests: XCTestCase { let keyword = "happ" let results = LanguageDBManager.shared.queryEmojisPatternMatching(of: keyword) - XCTAssertEqual(results.count, 3, "Should always return 3 elements (including empty strings)") + XCTAssertEqual(results.count, 6, "Should always return 6 elements (including empty strings)") } func testQueryEmojisPatternMatchingWithEmptyKeyword() { let results = LanguageDBManager.shared.queryEmojisPatternMatching(of: "") - XCTAssertEqual(results.count, 3) + XCTAssertEqual(results.count, 6) } func testQueryEmojisPatternMatchingWithNonExistentKeyword() { let results = LanguageDBManager.shared.queryEmojisPatternMatching(of: "nonexistentkeyword12345") - XCTAssertEqual(results.count, 3) + XCTAssertEqual(results.count, 6) XCTAssertEqual(results[0], "") XCTAssertEqual(results[1], "") XCTAssertEqual(results[2], "") + XCTAssertEqual(results[3], "") + XCTAssertEqual(results[4], "") + XCTAssertEqual(results[5], "") } } From 5a2510733a6fa651b497c2c6889394ac187a9f55 Mon Sep 17 00:00:00 2001 From: Jatin Foujdar Date: Wed, 14 Jan 2026 10:23:24 +0530 Subject: [PATCH 8/9] chore: refresh mergeability with main From 0199e6b56adad9c8889107be10f9c6b620ecccdb Mon Sep 17 00:00:00 2001 From: Jatin Foujdar Date: Wed, 14 Jan 2026 11:05:45 +0530 Subject: [PATCH 9/9] Resolve merge conflict in SettingsTableData --- Scribe/SettingsTab/SettingsTableData.swift | 40 +++++++++++++++------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/Scribe/SettingsTab/SettingsTableData.swift b/Scribe/SettingsTab/SettingsTableData.swift index 4544c4ee..ff2b76fc 100644 --- a/Scribe/SettingsTab/SettingsTableData.swift +++ b/Scribe/SettingsTab/SettingsTableData.swift @@ -73,18 +73,34 @@ enum SettingsTableData { 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.word_for_word", value: "Word for word deletion on long press", comment: ""), - hasToggle: true, - sectionState: .none(.toggleWordForWordDeletion), - shortDescription: NSLocalizedString("i18n.app.word_for_word.description", value: "Word for word deletion.", 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: "") - ) + 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: "" + ) +), + 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 )