From a1480533234e77d293d8bee68823ca7128801d1e Mon Sep 17 00:00:00 2001 From: zonble Date: Thu, 3 Feb 2022 02:05:56 +0800 Subject: [PATCH 1/8] Updates CI settings. --- ...tinuous-integration-workflow-xcode-12.yml} | 5 +- ...uous-integration-workflow-xcode-latest.yml | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) rename .github/workflows/{continuous-integration-workflow.yml => continuous-integration-workflow-xcode-12.yml} (92%) create mode 100644 .github/workflows/continuous-integration-workflow-xcode-latest.yml diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow-xcode-12.yml similarity index 92% rename from .github/workflows/continuous-integration-workflow.yml rename to .github/workflows/continuous-integration-workflow-xcode-12.yml index 0d126720..11696803 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow-xcode-12.yml @@ -3,12 +3,15 @@ on: [push] jobs: build: - name: Build and Test + name: Build and Test with Xcode 12.4 runs-on: macOS-latest env: DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer steps: - uses: actions/checkout@v1 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '12.4' - name: Build McBopomofoLMLibTest run: cmake -S . -B build working-directory: Source/Engine diff --git a/.github/workflows/continuous-integration-workflow-xcode-latest.yml b/.github/workflows/continuous-integration-workflow-xcode-latest.yml new file mode 100644 index 00000000..7c155bd1 --- /dev/null +++ b/.github/workflows/continuous-integration-workflow-xcode-latest.yml @@ -0,0 +1,48 @@ +name: Build +on: [push] + +jobs: + build: + name: Build and Test with Latest Xcode + runs-on: macOS-latest + env: + DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer + steps: + - uses: actions/checkout@v1 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + - name: Build McBopomofoLMLibTest + run: cmake -S . -B build + working-directory: Source/Engine + - name: Run McBopomofoLMLibTest + run: make runTest + working-directory: Source/Engine/build + - name: Build MandarinTest + run: cmake -S . -B build + working-directory: Source/Engine/Mandarin + - name: Run MandarinTest + run: make runTest + working-directory: Source/Engine/Mandarin/build + - name: Test McBopomofo App Bundle + run: xcodebuild -scheme McBopomofo -configuration Debug test + - name: Test CandidateUI + run: swift test + working-directory: Packages/CandidateUI + - name: Test OpenCCBridge + run: swift test + working-directory: Packages/OpenCCBridge + - name: Test VXHanConvert + run: swift test + working-directory: Packages/VXHanConvert + - name: Test NSStringUtils + run: swift test + working-directory: Packages/NSStringUtils + - name: Clean McBopomofo + run: xcodebuild -scheme McBopomofo -configuration Release clean + - name: Clean McBopomofoInstaller + run: xcodebuild -scheme McBopomofoInstaller -configuration Release clean + - name: Build McBopomofo + run: xcodebuild -scheme McBopomofo -configuration Release build + - name: Build McBopomofoInstaller + run: xcodebuild -scheme McBopomofoInstaller -configuration Release build From 9ade7d16a912497810b573cb5f73bfef2717a97f Mon Sep 17 00:00:00 2001 From: zonble Date: Fri, 4 Feb 2022 00:03:16 +0800 Subject: [PATCH 2/8] Adds an setting to move the cursor after selecting a candidate. The setting only works when "SelectPhraseAfterCursorAsCandidate" is also on. When a user use the mode, it is very possible that he or she has already something in the input buffer and go back to choose a candidate. When he or she completes selection, the user may want to go back to the end and continue inputting. The setting is a time saver. --- Source/Base.lproj/preferences.xib | 57 +++++++++++++++++++--------- Source/KeyHandler.mm | 14 +++++++ Source/Preferences.swift | 26 ++++++++----- Source/README | 44 +++++---------------- Source/zh-Hant.lproj/preferences.xib | 52 +++++++++++++++---------- 5 files changed, 110 insertions(+), 83 deletions(-) diff --git a/Source/Base.lproj/preferences.xib b/Source/Base.lproj/preferences.xib index 25ee9a38..988a4209 100644 --- a/Source/Base.lproj/preferences.xib +++ b/Source/Base.lproj/preferences.xib @@ -19,14 +19,14 @@ - + - + - + @@ -35,7 +35,7 @@ - + @@ -47,7 +47,7 @@ - + @@ -56,7 +56,7 @@ - + @@ -84,7 +84,7 @@ - + @@ -93,7 +93,7 @@ - + @@ -114,7 +114,7 @@ - + @@ -134,7 +134,7 @@ - + @@ -143,7 +143,7 @@ - + @@ -161,7 +161,7 @@ - + @@ -248,7 +248,7 @@ - + @@ -274,7 +274,7 @@ - + @@ -297,7 +297,7 @@ - + + diff --git a/Source/KeyHandler.mm b/Source/KeyHandler.mm index 48049838..f1d6c41e 100644 --- a/Source/KeyHandler.mm +++ b/Source/KeyHandler.mm @@ -202,6 +202,20 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot _userOverrideModel->observe(_walkedNodes, cursorIndex, stringValue, [[NSDate date] timeIntervalSince1970]); } [self _walk]; + + if (Preferences.selectPhraseAfterCursorAsCandidate && + Preferences.moveCursorAfterSelectingCandidate) { + size_t nextPosition = 0; + for (auto node: _walkedNodes) { + if (nextPosition >= cursorIndex) { + break; + } + nextPosition += node.spanningLength; + } + if (nextPosition < _builder->length()) { + _builder->setCursorIndex(nextPosition); + } + } } - (void)clear diff --git a/Source/Preferences.swift b/Source/Preferences.swift index 8d0b6f64..53775ff2 100644 --- a/Source/Preferences.swift +++ b/Source/Preferences.swift @@ -24,11 +24,15 @@ import Cocoa private let kKeyboardLayoutPreferenceKey = "KeyboardLayout" -private let kBasisKeyboardLayoutPreferenceKey = "BasisKeyboardLayout" // alphanumeric ("ASCII") input basi -private let kFunctionKeyKeyboardLayoutPreferenceKey = "FunctionKeyKeyboardLayout" // alphanumeric ("ASCII") input basi -private let kFunctionKeyKeyboardLayoutOverrideIncludeShiftKey = "FunctionKeyKeyboardLayoutOverrideIncludeShift" // whether include shif +/// alphanumeric ("ASCII") input basic keyboard layout. +private let kBasisKeyboardLayoutPreferenceKey = "BasisKeyboardLayout" +/// alphanumeric ("ASCII") input basic keyboard layout. +private let kFunctionKeyKeyboardLayoutPreferenceKey = "FunctionKeyKeyboardLayout" +/// whether include shift. +private let kFunctionKeyKeyboardLayoutOverrideIncludeShiftKey = "FunctionKeyKeyboardLayoutOverrideIncludeShift" private let kCandidateListTextSizeKey = "CandidateListTextSize" -private let kSelectPhraseAfterCursorAsCandidatePreferenceKey = "SelectPhraseAfterCursorAsCandidate" +private let kSelectPhraseAfterCursorAsCandidateKey = "SelectPhraseAfterCursorAsCandidate" +private let kMoveCursorAfterSelectingCandidateKey = "MoveCursorAfterSelectingCandidate" private let kUseHorizontalCandidateListPreferenceKey = "UseHorizontalCandidateList" private let kComposingBufferSizePreferenceKey = "ComposingBufferSize" private let kChooseCandidateUsingSpaceKey = "ChooseCandidateUsingSpaceKey" @@ -41,9 +45,8 @@ private let kCandidateKeyLabelFontName = "CandidateKeyLabelFontName" private let kCandidateKeys = "CandidateKeys" private let kPhraseReplacementEnabledKey = "PhraseReplacementEnabled" private let kChineseConversionEngineKey = "ChineseConversionEngine" -private let kChineseConversionStyle = "ChineseConversionStyle" +private let kChineseConversionStyleKey = "ChineseConversionStyle" private let kAssociatedPhrasesEnabledKey = "AssociatedPhrasesEnabled" -//private let kAssociatedPhrasesKeys = "AssociatedPhrasesKeys" private let kDefaultCandidateListTextSize: CGFloat = 16 private let kMinCandidateListTextSize: CGFloat = 12 @@ -204,7 +207,7 @@ class Preferences: NSObject { kFunctionKeyKeyboardLayoutPreferenceKey, kFunctionKeyKeyboardLayoutOverrideIncludeShiftKey, kCandidateListTextSizeKey, - kSelectPhraseAfterCursorAsCandidatePreferenceKey, + kSelectPhraseAfterCursorAsCandidateKey, kUseHorizontalCandidateListPreferenceKey, kComposingBufferSizePreferenceKey, kChooseCandidateUsingSpaceKey, @@ -216,7 +219,7 @@ class Preferences: NSObject { kCandidateKeys, kPhraseReplacementEnabledKey, kChineseConversionEngineKey, - kChineseConversionStyle, + kChineseConversionStyleKey, kAssociatedPhrasesEnabledKey] } @@ -240,9 +243,12 @@ class Preferences: NSObject { @CandidateListTextSize(key: kCandidateListTextSizeKey) @objc static var candidateListTextSize: CGFloat - @UserDefault(key: kSelectPhraseAfterCursorAsCandidatePreferenceKey, defaultValue: false) + @UserDefault(key: kSelectPhraseAfterCursorAsCandidateKey, defaultValue: false) @objc static var selectPhraseAfterCursorAsCandidate: Bool + @UserDefault(key: kMoveCursorAfterSelectingCandidateKey, defaultValue: false) + @objc static var moveCursorAfterSelectingCandidate: Bool + @UserDefault(key: kUseHorizontalCandidateListPreferenceKey, defaultValue: false) @objc static var useHorizontalCandidateList: Bool @@ -362,7 +368,7 @@ class Preferences: NSObject { /// /// - 0: convert the output /// - 1: convert the phrase models. - @UserDefault(key: kChineseConversionStyle, defaultValue: 0) + @UserDefault(key: kChineseConversionStyleKey, defaultValue: 0) @objc static var chineseConversionStyle: Int @objc static var chineseConversionStyleName: String? { diff --git a/Source/README b/Source/README index 994fe349..1092f5c7 100644 --- a/Source/README +++ b/Source/README @@ -2,6 +2,8 @@ ├── AppDelegate.swift ├── Base.lproj │   ├── Credits.rtf +│   ├── InfoPlist.strings +│   ├── Localizable.strings │   ├── MainMenu.xib │   ├── preferences.xib │   ├── template-data.txt @@ -16,6 +18,7 @@ │   ├── PhraseFreq.txt │   ├── README │   ├── Symbols.txt +│   ├── associated-phrases.cin │   ├── bin │   │   ├── C_Version │   │   │   ├── Makefile @@ -59,6 +62,8 @@ │   │   └── falsecount.txt │   └── phrase.occ ├── Engine +│   ├── AssociatedPhrases.cpp +│   ├── AssociatedPhrases.h │   ├── CMakeLists.txt │   ├── Gramambular │   │   ├── Bigram.h @@ -76,43 +81,12 @@ │   ├── KeyValueBlobReader.h │   ├── KeyValueBlobReaderTest.cpp │   ├── Mandarin +│   │   ├── CMakeLists.txt │   │   ├── Mandarin.cpp -│   │   └── Mandarin.h +│   │   ├── Mandarin.h +│   │   └── MandarinTest.cpp │   ├── McBopomofoLM.cpp │   ├── McBopomofoLM.h -│   ├── OpenVanilla -│   │   ├── OVAroundFilter.h -│   │   ├── OVBase.h -│   │   ├── OVBenchmark.h -│   │   ├── OVCINDataTable.h -│   │   ├── OVCINDatabaseService.h -│   │   ├── OVCINToSQLiteConvertor.h -│   │   ├── OVCandidateService.h -│   │   ├── OVDatabaseService.h -│   │   ├── OVDateTimeHelper.h -│   │   ├── OVEncodingService.h -│   │   ├── OVEventHandlingContext.h -│   │   ├── OVException.h -│   │   ├── OVFileHelper.h -│   │   ├── OVFrameworkInfo.h -│   │   ├── OVInputMethod.h -│   │   ├── OVKey.h -│   │   ├── OVKeyPreprocessor.h -│   │   ├── OVKeyValueMap.h -│   │   ├── OVLoaderBase.h -│   │   ├── OVLoaderService.h -│   │   ├── OVLocalization.h -│   │   ├── OVModule.h -│   │   ├── OVModulePackage.h -│   │   ├── OVOutputFilter.h -│   │   ├── OVPathInfo.h -│   │   ├── OVSQLiteDatabaseService.h -│   │   ├── OVSQLiteWrapper.h -│   │   ├── OVStringHelper.h -│   │   ├── OVTextBuffer.h -│   │   ├── OVUTF8Helper.h -│   │   ├── OVWildcard.h -│   │   └── OpenVanilla.h │   ├── ParselessLM.cpp │   ├── ParselessLM.h │   ├── ParselessLMBenchmark.cpp @@ -205,4 +179,4 @@ ├── template-exclude-phrases.txt └── template-phrases-replacement.txt -23 directories, 182 files +22 directories, 157 files diff --git a/Source/zh-Hant.lproj/preferences.xib b/Source/zh-Hant.lproj/preferences.xib index 3aad70ae..91ecee15 100644 --- a/Source/zh-Hant.lproj/preferences.xib +++ b/Source/zh-Hant.lproj/preferences.xib @@ -19,14 +19,14 @@ - + - + - + @@ -49,7 +49,7 @@ - + @@ -58,7 +58,7 @@ - + @@ -70,16 +70,16 @@ - + - + - + @@ -88,7 +88,7 @@ - + @@ -97,7 +97,7 @@ - + @@ -123,7 +123,7 @@ - + @@ -149,7 +149,7 @@ - + @@ -172,7 +172,7 @@ - + @@ -200,7 +200,7 @@ - + @@ -209,7 +209,7 @@ - + @@ -232,7 +232,7 @@ - + @@ -267,7 +267,7 @@ - + @@ -313,9 +313,21 @@ + - + From bfa272ac65e26c7fc68475ab2ec870707833ec9e Mon Sep 17 00:00:00 2001 From: zonble Date: Fri, 4 Feb 2022 00:09:44 +0800 Subject: [PATCH 3/8] Minor fix on the function to move cursor after selection. --- Source/KeyHandler.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/KeyHandler.mm b/Source/KeyHandler.mm index f1d6c41e..7c2e9d4d 100644 --- a/Source/KeyHandler.mm +++ b/Source/KeyHandler.mm @@ -212,7 +212,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot } nextPosition += node.spanningLength; } - if (nextPosition < _builder->length()) { + if (nextPosition <= _builder->length()) { _builder->setCursorIndex(nextPosition); } } From ccfb97e7c5028feec0a5d512481ecd51668e3aef Mon Sep 17 00:00:00 2001 From: zonble Date: Fri, 4 Feb 2022 04:33:49 +0800 Subject: [PATCH 4/8] Fixes the bug that the state was not reset if marking range is zero. --- McBopomofoTests/KeyHandlerBopomofoTests.swift | 95 +++++++++++++++++++ .../KeyHandlerPlainBopomofoTests.swift | 35 +++++++ Source/KeyHandler.mm | 1 - 3 files changed, 130 insertions(+), 1 deletion(-) diff --git a/McBopomofoTests/KeyHandlerBopomofoTests.swift b/McBopomofoTests/KeyHandlerBopomofoTests.swift index a92407a3..bb68dc9c 100644 --- a/McBopomofoTests/KeyHandlerBopomofoTests.swift +++ b/McBopomofoTests/KeyHandlerBopomofoTests.swift @@ -601,6 +601,74 @@ class KeyHandlerBopomofoTests: XCTestCase { XCTAssertTrue(state is InputState.EmptyIgnoringPreviousState, "\(state)") } + func testBackspaceToDeleteReading() { + var state: InputState = InputState.Empty() + let keys = Array("su").map { + String($0) + } + for key in keys { + let input = KeyHandlerInput(inputText: key, keyCode: 0, charCode: charCode(key), flags: [], isVerticalMode: false) + handler.handle(input: input, state: state) { newState in + state = newState + } errorCallback: { + } + } + + let backspace = KeyHandlerInput(inputText: " ", keyCode: 0, charCode: 8, flags: [], isVerticalMode: false) + + handler.handle(input: backspace, state: state) { newState in + state = newState + } errorCallback: { + } + XCTAssertTrue(state is InputState.Inputting, "\(state)") + if let state = state as? InputState.Inputting { + XCTAssertEqual(state.composingBuffer, "ㄋ") + XCTAssertEqual(state.cursorIndex, 1) + } + + handler.handle(input: backspace, state: state) { newState in + state = newState + } errorCallback: { + } + XCTAssertTrue(state is InputState.EmptyIgnoringPreviousState, "\(state)") + } + + func testBackspaceToDeleteReadingWithText() { + var state: InputState = InputState.Empty() + let keys = Array("su3cl").map { + String($0) + } + for key in keys { + let input = KeyHandlerInput(inputText: key, keyCode: 0, charCode: charCode(key), flags: [], isVerticalMode: false) + handler.handle(input: input, state: state) { newState in + state = newState + } errorCallback: { + } + } + + let backspace = KeyHandlerInput(inputText: " ", keyCode: 0, charCode: 8, flags: [], isVerticalMode: false) + + handler.handle(input: backspace, state: state) { newState in + state = newState + } errorCallback: { + } + XCTAssertTrue(state is InputState.Inputting, "\(state)") + if let state = state as? InputState.Inputting { + XCTAssertEqual(state.composingBuffer, "你ㄏ") + XCTAssertEqual(state.cursorIndex, 2) + } + + handler.handle(input: backspace, state: state) { newState in + state = newState + } errorCallback: { + } + XCTAssertTrue(state is InputState.Inputting, "\(state)") + if let state = state as? InputState.Inputting { + XCTAssertEqual(state.composingBuffer, "你") + XCTAssertEqual(state.cursorIndex, 1) + } + } + func testBackspace() { var state: InputState = InputState.Empty() let keys = Array("su3cl3").map { @@ -974,6 +1042,20 @@ class KeyHandlerBopomofoTests: XCTestCase { XCTAssertEqual(state.markerIndex, 0) XCTAssertEqual(state.markedRange, NSRange(location: 0, length: 2)) } + + var stateForGoingRight: InputState = state + + let right = KeyHandlerInput(inputText: " ", keyCode: KeyCode.right.rawValue, charCode: 0, flags: .shift, isVerticalMode: false) + handler.handle(input: right, state: stateForGoingRight) { newState in + stateForGoingRight = newState + } errorCallback: { + } + handler.handle(input: right, state: stateForGoingRight) { newState in + stateForGoingRight = newState + } errorCallback: { + } + + XCTAssertTrue(stateForGoingRight is InputState.Inputting, "\(stateForGoingRight)") } func testMarkingRight() { @@ -1030,6 +1112,19 @@ class KeyHandlerBopomofoTests: XCTestCase { XCTAssertEqual(state.markerIndex, 2) XCTAssertEqual(state.markedRange, NSRange(location: 0, length: 2)) } + + var stateForGoingLeft: InputState = state + + handler.handle(input: left, state: stateForGoingLeft) { newState in + stateForGoingLeft = newState + } errorCallback: { + } + handler.handle(input: left, state: stateForGoingLeft) { newState in + stateForGoingLeft = newState + } errorCallback: { + } + + XCTAssertTrue(stateForGoingLeft is InputState.Inputting, "\(stateForGoingLeft)") } func testCancelMarking() { diff --git a/McBopomofoTests/KeyHandlerPlainBopomofoTests.swift b/McBopomofoTests/KeyHandlerPlainBopomofoTests.swift index a69192e4..edd7ba08 100644 --- a/McBopomofoTests/KeyHandlerPlainBopomofoTests.swift +++ b/McBopomofoTests/KeyHandlerPlainBopomofoTests.swift @@ -84,6 +84,41 @@ class KeyHandlerPlainBopomofoTests: XCTestCase { Preferences.halfWidthPunctuationEnabled = enabled } + func testHalfPunctuationPeriod() { + let enabled = Preferences.halfWidthPunctuationEnabled + Preferences.halfWidthPunctuationEnabled = true + let input = KeyHandlerInput(inputText: ">", keyCode: 0, charCode: charCode(">"), flags: .shift, isVerticalMode: false) + var state: InputState = InputState.Empty() + handler.handle(input: input, state: state) { newState in + state = newState + } errorCallback: { + } + + XCTAssertTrue(state is InputState.ChoosingCandidate, "\(state)") + if let state = state as? InputState.ChoosingCandidate { + XCTAssertEqual(state.composingBuffer, ".") + } + Preferences.halfWidthPunctuationEnabled = enabled + } + + func testControlPunctuationPeriod() { + let input = KeyHandlerInput(inputText: ".", keyCode: 0, charCode: charCode("."), flags: [.shift, .control], isVerticalMode: false) + var state: InputState = InputState.Empty() + var count = 0 + handler.handle(input: input, state: state) { newState in + if count == 0 { + state = newState + } + count += 1 + } errorCallback: { + } + + XCTAssertTrue(state is InputState.Inputting, "\(state)") + if let state = state as? InputState.Inputting { + XCTAssertEqual(state.composingBuffer, "。") + } + } + func testInputNe() { let input = KeyHandlerInput(inputText: "s", keyCode: 0, charCode: charCode("s"), flags: .shift, isVerticalMode: false) var state: InputState = InputState.Empty() diff --git a/Source/KeyHandler.mm b/Source/KeyHandler.mm index 7c2e9d4d..123f50b8 100644 --- a/Source/KeyHandler.mm +++ b/Source/KeyHandler.mm @@ -894,7 +894,6 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot } else { stateCallback(marking); } - stateCallback(marking); } else { errorCallback(); stateCallback(state); From 6db08b8420303f2f05f054e3e6b71c991fe2b11c Mon Sep 17 00:00:00 2001 From: zonble Date: Fri, 4 Feb 2022 04:49:05 +0800 Subject: [PATCH 5/8] Adds test cases. --- McBopomofoTests/KeyHandlerBopomofoTests.swift | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/McBopomofoTests/KeyHandlerBopomofoTests.swift b/McBopomofoTests/KeyHandlerBopomofoTests.swift index bb68dc9c..aedd3df8 100644 --- a/McBopomofoTests/KeyHandlerBopomofoTests.swift +++ b/McBopomofoTests/KeyHandlerBopomofoTests.swift @@ -708,6 +708,39 @@ class KeyHandlerBopomofoTests: XCTestCase { XCTAssertTrue(state is InputState.EmptyIgnoringPreviousState, "\(state)") } + func testCursorWithReading() { + var state: InputState = InputState.Empty() + let keys = Array("su3cl").map { + String($0) + } + for key in keys { + let input = KeyHandlerInput(inputText: key, keyCode: 0, charCode: charCode(key), flags: [], isVerticalMode: false) + handler.handle(input: input, state: state) { newState in + state = newState + } errorCallback: { + } + } + let left = KeyHandlerInput(inputText: " ", keyCode: KeyCode.left.rawValue, charCode: 0, flags: [], isVerticalMode: false) + let right = KeyHandlerInput(inputText: " ", keyCode: KeyCode.right.rawValue, charCode: 0, flags: [], isVerticalMode: false) + var leftErrorCalled = false + var rightErrorCalled = false + + handler.handle(input: left, state: state) { newState in + state = newState + } errorCallback: { + leftErrorCalled = true + } + + handler.handle(input: right, state: state) { newState in + state = newState + } errorCallback: { + rightErrorCalled = true + } + + XCTAssertTrue(leftErrorCalled) + XCTAssertTrue(rightErrorCalled) + } + func testCursor() { var state: InputState = InputState.Empty() let keys = Array("su3cl3").map { @@ -997,6 +1030,112 @@ class KeyHandlerBopomofoTests: XCTestCase { } } + func testHomeAndEndWithReading() { + var state: InputState = InputState.Empty() + let keys = Array("su3cl").map { + String($0) + } + for key in keys { + let input = KeyHandlerInput(inputText: key, keyCode: 0, charCode: charCode(key), flags: [], isVerticalMode: false) + handler.handle(input: input, state: state) { newState in + state = newState + } errorCallback: { + } + } + XCTAssertTrue(state is InputState.Inputting, "\(state)") + if let state = state as? InputState.Inputting { + XCTAssertEqual(state.composingBuffer, "你ㄏㄠ") + XCTAssertEqual(state.cursorIndex, 3) + } + + let home = KeyHandlerInput(inputText: " ", keyCode: KeyCode.home.rawValue, charCode: 0, flags: [], isVerticalMode: false) + let end = KeyHandlerInput(inputText: " ", keyCode: KeyCode.end.rawValue, charCode: 0, flags: [], isVerticalMode: false) + var homeErrorCalled = false + var endErrorCalled = false + + handler.handle(input: home, state: state) { newState in + state = newState + } errorCallback: { + homeErrorCalled = true + } + + XCTAssertTrue(homeErrorCalled) + XCTAssertTrue(state is InputState.Inputting, "\(state)") + if let state = state as? InputState.Inputting { + XCTAssertEqual(state.composingBuffer, "你ㄏㄠ") + XCTAssertEqual(state.cursorIndex, 3) + } + + handler.handle(input: end, state: state) { newState in + state = newState + } errorCallback: { + endErrorCalled = true + } + + XCTAssertTrue(endErrorCalled) + XCTAssertTrue(state is InputState.Inputting, "\(state)") + if let state = state as? InputState.Inputting { + XCTAssertEqual(state.composingBuffer, "你ㄏㄠ") + XCTAssertEqual(state.cursorIndex, 3) + } + } + + func testMarkingLeftAtBegin() { + var state: InputState = InputState.Empty() + let keys = Array("su3cl3").map { + String($0) + } + for key in keys { + let input = KeyHandlerInput(inputText: key, keyCode: 0, charCode: charCode(key), flags: [], isVerticalMode: false) + handler.handle(input: input, state: state) { newState in + state = newState + } errorCallback: { + } + } + + let input = KeyHandlerInput(inputText: " ", keyCode: KeyCode.left.rawValue, charCode: 0, flags: .shift, isVerticalMode: false) + var errorCalled = false + + handler.handle(input: input, state: state) { newState in + state = newState + } errorCallback: { + } + handler.handle(input: input, state: state) { newState in + state = newState + } errorCallback: { + } + handler.handle(input: input, state: state) { newState in + state = newState + } errorCallback: { + errorCalled = true + } + XCTAssertTrue(errorCalled) + } + + + func testMarkingRightAtEnd() { + var state: InputState = InputState.Empty() + let keys = Array("su3cl3").map { + String($0) + } + for key in keys { + let input = KeyHandlerInput(inputText: key, keyCode: 0, charCode: charCode(key), flags: [], isVerticalMode: false) + handler.handle(input: input, state: state) { newState in + state = newState + } errorCallback: { + } + } + + let input = KeyHandlerInput(inputText: " ", keyCode: KeyCode.right.rawValue, charCode: 0, flags: .shift, isVerticalMode: false) + var errorCalled = false + handler.handle(input: input, state: state) { newState in + state = newState + } errorCallback: { + errorCalled = true + } + XCTAssertTrue(errorCalled) + } + func testMarkingLeft() { var state: InputState = InputState.Empty() let keys = Array("su3cl3").map { @@ -1086,6 +1225,16 @@ class KeyHandlerBopomofoTests: XCTestCase { } errorCallback: { } + let errorInput = KeyHandlerInput(inputText: " ", keyCode: KeyCode.left.rawValue, charCode: 0, flags: .shift, isVerticalMode: false) + var errorCalled = false + handler.handle(input: errorInput, state: state) { newState in + state = newState + } errorCallback: { + errorCalled = true + } + XCTAssertTrue(errorCalled) + + let input = KeyHandlerInput(inputText: " ", keyCode: KeyCode.right.rawValue, charCode: 0, flags: .shift, isVerticalMode: false) handler.handle(input: input, state: state) { newState in state = newState From ef61acf56356f70e512e929595fd6fb0c6b7f3c9 Mon Sep 17 00:00:00 2001 From: zonble Date: Fri, 4 Feb 2022 05:00:44 +0800 Subject: [PATCH 6/8] Adds tests. --- McBopomofoTests/KeyHandlerBopomofoTests.swift | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/McBopomofoTests/KeyHandlerBopomofoTests.swift b/McBopomofoTests/KeyHandlerBopomofoTests.swift index aedd3df8..07cfcfba 100644 --- a/McBopomofoTests/KeyHandlerBopomofoTests.swift +++ b/McBopomofoTests/KeyHandlerBopomofoTests.swift @@ -633,6 +633,45 @@ class KeyHandlerBopomofoTests: XCTestCase { XCTAssertTrue(state is InputState.EmptyIgnoringPreviousState, "\(state)") } + func testBackspaceAtBegin() { + var state: InputState = InputState.Empty() + let keys = Array("su3cl3").map { + String($0) + } + for key in keys { + let input = KeyHandlerInput(inputText: key, keyCode: 0, charCode: charCode(key), flags: [], isVerticalMode: false) + handler.handle(input: input, state: state) { newState in + state = newState + } errorCallback: { + } + } + + let left = KeyHandlerInput(inputText: " ", keyCode: KeyCode.left.rawValue, charCode: 0, flags: [], isVerticalMode: false) + handler.handle(input: left, state: state) { newState in + state = newState + } errorCallback: { + } + handler.handle(input: left, state: state) { newState in + state = newState + } errorCallback: { + } + + XCTAssertTrue(state is InputState.Inputting, "\(state)") + if let state = state as? InputState.Inputting { + XCTAssertEqual(state.composingBuffer, "你好") + XCTAssertEqual(state.cursorIndex, 0) + } + + let backspace = KeyHandlerInput(inputText: " ", keyCode: 0, charCode: 8, flags: [], isVerticalMode: false) + var errorCall = false + handler.handle(input: backspace, state: state) { newState in + state = newState + } errorCallback: { + errorCall = true + } + XCTAssertTrue(errorCall) + } + func testBackspaceToDeleteReadingWithText() { var state: InputState = InputState.Empty() let keys = Array("su3cl").map { @@ -1019,6 +1058,20 @@ class KeyHandlerBopomofoTests: XCTestCase { XCTAssertEqual(state.cursorIndex, 0) } + var homeErrorCalled = false + handler.handle(input: home, state: state) { newState in + state = newState + } errorCallback: { + homeErrorCalled = true + } + + XCTAssertTrue(state is InputState.Inputting, "\(state)") + if let state = state as? InputState.Inputting { + XCTAssertEqual(state.composingBuffer, "你好") + XCTAssertEqual(state.cursorIndex, 0) + } + XCTAssertTrue(homeErrorCalled) + handler.handle(input: end, state: state) { newState in state = newState } errorCallback: { @@ -1028,6 +1081,19 @@ class KeyHandlerBopomofoTests: XCTestCase { XCTAssertEqual(state.composingBuffer, "你好") XCTAssertEqual(state.cursorIndex, 2) } + + var endErrorCalled = false + handler.handle(input: end, state: state) { newState in + state = newState + } errorCallback: { + endErrorCalled = true + } + + if let state = state as? InputState.Inputting { + XCTAssertEqual(state.composingBuffer, "你好") + XCTAssertEqual(state.cursorIndex, 2) + } + XCTAssertTrue(endErrorCalled) } func testHomeAndEndWithReading() { From 472b149020b5350f1efcc6d7f1cf9db0ed635e20 Mon Sep 17 00:00:00 2001 From: zonble Date: Fri, 4 Feb 2022 05:19:25 +0800 Subject: [PATCH 7/8] Adds tests. --- .../KeyHandlerPlainBopomofoTests.swift | 30 +++++++++++++++++++ Source/KeyHandler.mm | 15 ++++++---- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/McBopomofoTests/KeyHandlerPlainBopomofoTests.swift b/McBopomofoTests/KeyHandlerPlainBopomofoTests.swift index edd7ba08..4e99ebbd 100644 --- a/McBopomofoTests/KeyHandlerPlainBopomofoTests.swift +++ b/McBopomofoTests/KeyHandlerPlainBopomofoTests.swift @@ -119,6 +119,36 @@ class KeyHandlerPlainBopomofoTests: XCTestCase { } } + func testEnterWithReading() { + let input = KeyHandlerInput(inputText: "s", keyCode: 0, charCode: charCode("s"), flags: .shift, isVerticalMode: false) + var state: InputState = InputState.Empty() + handler.handle(input: input, state: state) { newState in + state = newState + } errorCallback: { + } + + XCTAssertTrue(state is InputState.Inputting, "\(state)") + if let state = state as? InputState.Inputting { + XCTAssertEqual(state.composingBuffer, "ㄋ") + } + + let enter = KeyHandlerInput(inputText: " ", keyCode: 0, charCode: 13, flags: [], isVerticalMode: false) + var count = 0 + + handler.handle(input: enter, state: state) { newState in + if count == 0 { + state = newState + } + count += 1 + } errorCallback: { + } + + XCTAssertTrue(state is InputState.Inputting, "\(state)") + if let state = state as? InputState.Inputting { + XCTAssertEqual(state.composingBuffer, "ㄋ") + } + } + func testInputNe() { let input = KeyHandlerInput(inputText: "s", keyCode: 0, charCode: charCode("s"), flags: .shift, isVerticalMode: false) var state: InputState = InputState.Empty() diff --git a/Source/KeyHandler.mm b/Source/KeyHandler.mm index 123f50b8..34efb93e 100644 --- a/Source/KeyHandler.mm +++ b/Source/KeyHandler.mm @@ -779,12 +779,15 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot return NO; } - if (_inputMode == InputModePlainBopomofo) { - if (!_bpmfReadingBuffer->isEmpty()) { - errorCallback(); - } - return YES; - } +// Actually the lines would not be reached. When there is BMPF reading and +// a user input enter, we just send the readings to the client app. + +// if (_inputMode == InputModePlainBopomofo) { +// if (!_bpmfReadingBuffer->isEmpty()) { +// errorCallback(); +// } +// return YES; +// } [self clear]; From dc24de2ccbf07f4680413a3883a17ebc70c4fab7 Mon Sep 17 00:00:00 2001 From: zonble Date: Fri, 4 Feb 2022 05:31:47 +0800 Subject: [PATCH 8/8] Notifies the users not to add duplicated phrases. --- Source/Base.lproj/Localizable.strings | 2 ++ Source/InputState.swift | 25 +++++++++++++++++++++--- Source/LanguageModelManager.h | 2 ++ Source/LanguageModelManager.mm | 13 ++++++++++++ Source/en.lproj/Localizable.strings | 3 +++ Source/zh-Hant.lproj/Localizable.strings | 8 +++++--- 6 files changed, 47 insertions(+), 6 deletions(-) diff --git a/Source/Base.lproj/Localizable.strings b/Source/Base.lproj/Localizable.strings index 3f906806..6a30f895 100644 --- a/Source/Base.lproj/Localizable.strings +++ b/Source/Base.lproj/Localizable.strings @@ -90,3 +90,5 @@ "Cursor is after \"%@\"." = "Cursor is after \"%@\"."; "Cursor is between \"%@\" and \"%@\"." = "Cursor is between \"%@\" and \"%@\"."; + +"You are now selecting \"%@\". The phrase already exists." = "You are now selecting \"%@\". The phrase already exists."; diff --git a/Source/InputState.swift b/Source/InputState.swift index 23f2f39c..61217c47 100644 --- a/Source/InputState.swift +++ b/Source/InputState.swift @@ -185,6 +185,16 @@ class InputState: NSObject { } else if (markedRange.length > kMaxMarkRangeLength) { return String(format: NSLocalizedString("You are now selecting \"%@\". A phrase cannot be longer than %d characters.", comment: ""), text, kMaxMarkRangeLength) } + + let (exactBegin, _) = (composingBuffer as NSString).characterIndex(from: markedRange.location) + let (exactEnd, _) = (composingBuffer as NSString).characterIndex(from: markedRange.location + markedRange.length) + let selectedReadings = readings[exactBegin..= kMinMarkRangeLength && - markedRange.length <= kMaxMarkRangeLength + if markedRange.length < kMinMarkRangeLength { + return false + } + if markedRange.length > kMaxMarkRangeLength { + return false + } + let text = (composingBuffer as NSString).substring(with: markedRange) + let (exactBegin, _) = (composingBuffer as NSString).characterIndex(from: markedRange.location) + let (exactEnd, _) = (composingBuffer as NSString).characterIndex(from: markedRange.location + markedRange.length) + let selectedReadings = readings[exactBegin.. unigrams = gLanguageModelMcBopomofo.unigramsForKey(unigramKey); + string userPhraseString = string(userPhrase.UTF8String); + for (auto unigram: unigrams) { + if (unigram.keyValue.value == userPhraseString) { + return YES; + } + } + return NO; +} + + (BOOL)writeUserPhrase:(NSString *)userPhrase { if (![self checkIfUserLanguageModelFilesExist]) { diff --git a/Source/en.lproj/Localizable.strings b/Source/en.lproj/Localizable.strings index 3f906806..acaee5b1 100644 --- a/Source/en.lproj/Localizable.strings +++ b/Source/en.lproj/Localizable.strings @@ -90,3 +90,6 @@ "Cursor is after \"%@\"." = "Cursor is after \"%@\"."; "Cursor is between \"%@\" and \"%@\"." = "Cursor is between \"%@\" and \"%@\"."; + +"You are now selecting \"%@\". The phrase already exists." = "You are now selecting \"%@\". The phrase already exists."; + diff --git a/Source/zh-Hant.lproj/Localizable.strings b/Source/zh-Hant.lproj/Localizable.strings index 3bcd325e..36c4370c 100644 --- a/Source/zh-Hant.lproj/Localizable.strings +++ b/Source/zh-Hant.lproj/Localizable.strings @@ -47,11 +47,11 @@ "Use Half-Width Punctuations" = "使用半型標點符號"; -"You are now selecting \"%@\". You can add a phrase with two or more characters." = "您目前選擇了 \"%@\"。請選擇兩個字以上,才能加入使用者詞彙。"; +"You are now selecting \"%@\". You can add a phrase with two or more characters." = "您目前選擇了「%@」。請選擇兩個字以上,才能加入使用者詞彙。"; -"You are now selecting \"%@\". Press enter to add a new phrase." = "您目前選擇了 \"%@\"。按下 Enter 就可以加入到使用者詞彙中。"; +"You are now selecting \"%@\". Press enter to add a new phrase." = "您目前選擇了「%@」。按下 Enter 就可以加入到使用者詞彙中。"; -"You are now selecting \"%@\". A phrase cannot be longer than %d characters." = "您目前選擇了 \"%@\"。自訂詞彙不能超過 %d 個字元。"; +"You are now selecting \"%@\". A phrase cannot be longer than %d characters." = "您目前選擇了「%@」。自訂詞彙不能超過 %d 個字元。"; "Chinese conversion on" = "已經切換到簡體中文模式"; @@ -90,3 +90,5 @@ "Cursor is after \"%@\"." = "游標正在「%@」後方"; "Cursor is between \"%@\" and \"%@\"." = "游標正在「%@」與「%@」之間"; + +"You are now selecting \"%@\". The phrase already exists." = "您目前選擇了「%@」,這個詞彙已經存在了";