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
diff --git a/McBopomofoTests/KeyHandlerBopomofoTests.swift b/McBopomofoTests/KeyHandlerBopomofoTests.swift
index a92407a3..07cfcfba 100644
--- a/McBopomofoTests/KeyHandlerBopomofoTests.swift
+++ b/McBopomofoTests/KeyHandlerBopomofoTests.swift
@@ -601,6 +601,113 @@ 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 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 {
+ 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 {
@@ -640,6 +747,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 {
@@ -918,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: {
@@ -927,6 +1081,125 @@ 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() {
+ 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() {
@@ -974,6 +1247,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() {
@@ -1004,6 +1291,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
@@ -1030,6 +1327,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..4e99ebbd 100644
--- a/McBopomofoTests/KeyHandlerPlainBopomofoTests.swift
+++ b/McBopomofoTests/KeyHandlerPlainBopomofoTests.swift
@@ -84,6 +84,71 @@ 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 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/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/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/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..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
@@ -765,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];
@@ -880,7 +897,6 @@ static NSString *const kGraphVizOutputfile = @"/tmp/McBopomofo-visualization.dot
} else {
stateCallback(marking);
}
- stateCallback(marking);
} else {
errorCallback();
stateCallback(state);
diff --git a/Source/LanguageModelManager.h b/Source/LanguageModelManager.h
index ad75814d..8fdcfbc5 100644
--- a/Source/LanguageModelManager.h
+++ b/Source/LanguageModelManager.h
@@ -33,6 +33,8 @@ NS_ASSUME_NONNULL_BEGIN
+ (void)loadUserPhraseReplacement;
+ (void)setupDataModelValueConverter;
+ (BOOL)checkIfUserLanguageModelFilesExist;
+
++ (BOOL)checkIfUserPhraseExist:(NSString *)userPhrase key:(NSString *)key NS_SWIFT_NAME(checkIfExist(userPhrase:key:));
+ (BOOL)writeUserPhrase:(NSString *)userPhrase;
@property (class, readonly, nonatomic) NSString *dataFolderPath;
diff --git a/Source/LanguageModelManager.mm b/Source/LanguageModelManager.mm
index df6b4019..59c31b52 100644
--- a/Source/LanguageModelManager.mm
+++ b/Source/LanguageModelManager.mm
@@ -193,6 +193,19 @@ static void LTLoadAssociatedPhrases(McBopomofoLM &lm)
return YES;
}
++ (BOOL)checkIfUserPhraseExist:(NSString *)userPhrase key:(NSString *)key NS_SWIFT_NAME(checkIfExist(userPhrase:key:))
+{
+ string unigramKey = string(key.UTF8String);
+ vector 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/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/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." = "您目前選擇了「%@」,這個詞彙已經存在了";
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 @@
-
+
@@ -183,7 +183,7 @@
-
+
@@ -200,7 +200,7 @@
-
+
@@ -209,7 +209,7 @@
-
+
@@ -220,7 +220,7 @@
-
+
@@ -232,7 +232,7 @@
-
+
@@ -267,7 +267,7 @@
-
+
@@ -313,9 +313,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
-
+