diff --git a/Source/Modules/ControllerModules/InputState.swift b/Source/Modules/ControllerModules/InputState.swift index bfe0cab9..07915b45 100644 --- a/Source/Modules/ControllerModules/InputState.swift +++ b/Source/Modules/ControllerModules/InputState.swift @@ -139,9 +139,9 @@ enum InputState { /// 因為逐字選字模式不需要在組字區內存入任何東西,所以該狀態不受 .NotEmpty 的管轄。 class AssociatedPhrases: InputStateProtocol { public var type: StateType { .ofAssociatedPhrases } - private(set) var candidates: [String] = [] + private(set) var candidates: [(String, String)] = [] private(set) var isTypingVertical: Bool = false - init(candidates: [String], isTypingVertical: Bool) { + init(candidates: [(String, String)], isTypingVertical: Bool) { self.candidates = candidates self.isTypingVertical = isTypingVertical } @@ -407,10 +407,10 @@ enum InputState { /// .ChoosingCandidate: 叫出選字窗、允許使用者選字。 class ChoosingCandidate: NotEmpty { override public var type: StateType { .ofChooseCandidate } - private(set) var candidates: [String] + private(set) var candidates: [(String, String)] private(set) var isTypingVertical: Bool - init(composingBuffer: String, cursorIndex: Int, candidates: [String], isTypingVertical: Bool) { + init(composingBuffer: String, cursorIndex: Int, candidates: [(String, String)], isTypingVertical: Bool) { self.candidates = candidates self.isTypingVertical = isTypingVertical super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex) @@ -432,7 +432,7 @@ enum InputState { self.node = node let candidates = node.children?.map(\.title) ?? [String]() super.init( - composingBuffer: "", cursorIndex: 0, candidates: candidates, + composingBuffer: "", cursorIndex: 0, candidates: candidates.map { ("", $0) }, isTypingVertical: isTypingVertical ) } diff --git a/Source/Modules/ControllerModules/KeyHandler_Core.swift b/Source/Modules/ControllerModules/KeyHandler_Core.swift index 998f381e..51cd3e51 100644 --- a/Source/Modules/ControllerModules/KeyHandler_Core.swift +++ b/Source/Modules/ControllerModules/KeyHandler_Core.swift @@ -149,10 +149,10 @@ class KeyHandler { /// - Parameter key: 給定的聯想詞的開頭字。 /// - Returns: 抓取到的聯想詞陣列。 /// 不會是 nil,但那些負責接收結果的函式會對空白陣列結果做出正確的處理。 - func buildAssociatePhraseArray(withKey key: String) -> [String] { - var arrResult: [String] = [] + func buildAssociatePhraseArray(withKey key: String) -> [(String, String)] { + var arrResult: [(String, String)] = [] if currentLM.hasAssociatedPhrasesFor(key: key) { - arrResult.append(contentsOf: currentLM.associatedPhrasesFor(key: key)) + arrResult = currentLM.associatedPhrasesFor(key: key).map { ("", $0) } } return arrResult } @@ -162,21 +162,22 @@ class KeyHandler { /// - Parameters: /// - value: 給定之候選字字串。 /// - respectCursorPushing: 若該選項為 true,則會在選字之後始終將游標推送至選字厚的節錨的前方。 - func fixNode(value: String, respectCursorPushing: Bool = true) { + func fixNode(candidate: (String, String), respectCursorPushing: Bool = true) { + let theCandidate: Megrez.KeyValuePaired = .init(key: candidate.0, value: candidate.1) let adjustedCursor = max(0, min(actualCandidateCursor + (mgrPrefs.useRearCursorMode ? 1 : 0), compositorLength)) // 開始讓半衰模組觀察目前的狀況。 - let selectedNode: Megrez.NodeAnchor = compositor.fixNodeSelectedCandidate(value, at: adjustedCursor) + let selectedNode: Megrez.NodeAnchor = compositor.fixNodeWithCandidate(theCandidate, at: adjustedCursor) // 不要針對逐字選字模式啟用臨時半衰記憶模型。 if !mgrPrefs.useSCPCTypingMode { var addToUserOverrideModel = true // 所有讀音數與字符數不匹配的情況均不得塞入半衰記憶模組。 - if selectedNode.spanLength != value.count { + if selectedNode.spanLength != theCandidate.value.count { IME.prtDebugIntel("UOM: SpanningLength != value.count, dismissing.") addToUserOverrideModel = false } if addToUserOverrideModel { // 威注音的 SymbolLM 的 Score 是 -12,符合該條件的內容不得塞入半衰記憶模組。 - if selectedNode.node.scoreFor(candidate: value) <= -12 { + if selectedNode.node.scoreForPaired(candidate: theCandidate) <= -12 { IME.prtDebugIntel("UOM: Score <= -12, dismissing.") addToUserOverrideModel = false } @@ -186,7 +187,7 @@ class KeyHandler { // 令半衰記憶模組觀測給定的三元圖。 // 這個過程會讓半衰引擎根據當前上下文生成三元圖索引鍵。 currentUOM.observe( - walkedAnchors: walkedAnchors, cursorIndex: adjustedCursor, candidate: value, + walkedAnchors: walkedAnchors, cursorIndex: adjustedCursor, candidate: theCandidate.value, timestamp: NSDate().timeIntervalSince1970 ) } @@ -211,21 +212,21 @@ class KeyHandler { for anchor in walkedAnchors { if index >= width - kMaxComposingBufferNeedsToWalkSize { break } if anchor.node.score < Megrez.Node.kSelectedCandidateScore { - compositor.fixNodeSelectedCandidate(anchor.node.currentPair.value, at: index + anchor.spanLength) + compositor.fixNodeWithCandidate(anchor.node.currentPair, at: index + anchor.spanLength) } index += anchor.spanLength } } - /// 獲取候選字詞陣列資料內容。 - func getCandidatesArray(fixOrder: Bool = true) -> [String] { + /// 獲取候選字詞(包含讀音)陣列資料內容。 + func getCandidatesArray(fixOrder: Bool = true) -> [(String, String)] { var arrAnchors: [Megrez.NodeAnchor] = rawAnchorsOfNodes - var arrCandidates: [String] = [] + var arrCandidates: [Megrez.KeyValuePaired] = .init() /// 原理:nodes 這個回饋結果包含一堆子陣列,分別對應不同詞長的候選字。 /// 這裡先對陣列排序、讓最長候選字的子陣列的優先權最高。 /// 這個過程不會傷到子陣列內部的排序。 - if arrAnchors.isEmpty { return arrCandidates } + if arrAnchors.isEmpty { return .init() } // 讓更長的節錨排序靠前。 arrAnchors = arrAnchors.stableSort { $0.keyLength > $1.keyLength } @@ -235,19 +236,19 @@ class KeyHandler { // 選字窗的內容的康熙轉換 / JIS 轉換不能放在這裡處理,會影響選字有效性。 // 選字的原理是拿著具體的候選字詞的字串去當前的節錨下找出對應的候選字詞(X元圖)。 // 一旦在這裡轉換了,節錨內的某些元圖就無法被選中。 - arrCandidates.append(currentCandidate.value) + arrCandidates.append(.init(key: currentCandidate.key, value: currentCandidate.value)) } // 決定是否根據半衰記憶模組的建議來調整候選字詞的順序。 if !mgrPrefs.fetchSuggestionsFromUserOverrideModel || mgrPrefs.useSCPCTypingMode || fixOrder { - return arrCandidates + return arrCandidates.map { ($0.key, $0.value) } } let arrSuggestedUnigrams: [Megrez.Unigram] = fetchSuggestedCandidates().stableSort { $0.score > $1.score } - let arrSuggestedCandidates: [String] = arrSuggestedUnigrams.map(\.keyValue.value) + let arrSuggestedCandidates: [Megrez.KeyValuePaired] = arrSuggestedUnigrams.map(\.keyValue) arrCandidates = arrSuggestedCandidates.filter { arrCandidates.contains($0) } + arrCandidates arrCandidates = arrCandidates.deduplicate - arrCandidates = arrCandidates.stableSort { $0.count > $1.count } - return arrCandidates + arrCandidates = arrCandidates.stableSort { $0.key.split(separator: "-").count > $1.key.split(separator: "-").count } + return arrCandidates.map { ($0.key, $0.value) } } /// 向半衰引擎詢問可能的選字建議。拿到的結果會是一個單元圖陣列。 diff --git a/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift b/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift index 0e78df2a..5a91344f 100644 --- a/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift +++ b/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift @@ -274,7 +274,7 @@ extension KeyHandler { // MARK: End Key - var candidates: [String]! + var candidates: [(String, String)]! if let state = state as? InputState.ChoosingCandidate { candidates = state.candidates diff --git a/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift b/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift index 8f54848f..f07cac76 100644 --- a/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift +++ b/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift @@ -225,7 +225,7 @@ extension KeyHandler { ) if choosingCandidates.candidates.count == 1 { clear() - let text: String = choosingCandidates.candidates.first ?? "" + let text: String = choosingCandidates.candidates.first?.1 ?? "" stateCallback(InputState.Committing(textToCommit: text)) if !mgrPrefs.associatedPhrasesEnabled { diff --git a/Source/Modules/ControllerModules/KeyHandler_States.swift b/Source/Modules/ControllerModules/KeyHandler_States.swift index 401bcaf1..73132538 100644 --- a/Source/Modules/ControllerModules/KeyHandler_States.swift +++ b/Source/Modules/ControllerModules/KeyHandler_States.swift @@ -330,8 +330,8 @@ extension KeyHandler { ) if candidateState.candidates.count == 1 { clear() - if let strtextToCommit: String = candidateState.candidates.first { - stateCallback(InputState.Committing(textToCommit: strtextToCommit)) + if let candidateToCommit: (String, String) = candidateState.candidates.first { + stateCallback(InputState.Committing(textToCommit: candidateToCommit.1)) stateCallback(InputState.Empty()) } else { stateCallback(candidateState) @@ -802,7 +802,7 @@ extension KeyHandler { } let currentNode = currentAnchor.node - let currentValue = currentNode.currentPair.value + let currentPaired: Megrez.KeyValuePaired = currentNode.currentPair var currentIndex = 0 if currentNode.score < Megrez.Node.kSelectedCandidateScore { @@ -814,14 +814,14 @@ extension KeyHandler { /// 選中的話,則使用者可以直接摁下本函式對應的按鍵來輪替候選字即可。 /// (預設情況下是 (Shift+)Tab 來做正 (反) 向切換,但也可以用 /// Shift(+CMD)+Space 來切換、以應對臉書綁架 Tab 鍵的情況。 - if candidates[0] == currentValue { + if candidates[0].0 == currentPaired.key, candidates[0].1 == currentPaired.value { /// 如果第一個候選字詞是當前節點的候選字詞的值的話, /// 那就切到下一個(或上一個,也就是最後一個)候選字詞。 currentIndex = reverseModifier ? candidates.count - 1 : 1 } } else { for candidate in candidates { - if candidate == currentValue { + if candidate.0 == currentPaired.key, candidate.1 == currentPaired.value { if reverseModifier { if currentIndex == 0 { currentIndex = candidates.count - 1 @@ -841,7 +841,7 @@ extension KeyHandler { currentIndex = 0 } - fixNode(value: candidates[currentIndex], respectCursorPushing: false) + fixNode(candidate: candidates[currentIndex], respectCursorPushing: false) stateCallback(buildInputtingState) return true diff --git a/Source/Modules/ControllerModules/ctlInputMethod_Delegates.swift b/Source/Modules/ControllerModules/ctlInputMethod_Delegates.swift index d55f76f6..7b0f6f92 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_Delegates.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_Delegates.swift @@ -81,7 +81,7 @@ extension ctlInputMethod: ctlCandidateDelegate { } func ctlCandidate(_ controller: ctlCandidate, candidateAtIndex index: Int) - -> String + -> (String, String) { _ = controller // 防止格式整理工具毀掉與此對應的參數。 if let state = state as? InputState.ChoosingCandidate { @@ -89,7 +89,7 @@ extension ctlInputMethod: ctlCandidateDelegate { } else if let state = state as? InputState.AssociatedPhrases { return state.candidates[index] } - return "" + return ("", "") } func ctlCandidate(_ controller: ctlCandidate, didSelectCandidateAtIndex index: Int) { @@ -112,7 +112,7 @@ extension ctlInputMethod: ctlCandidateDelegate { if let state = state as? InputState.ChoosingCandidate { let selectedValue = state.candidates[index] - keyHandler.fixNode(value: selectedValue, respectCursorPushing: true) + keyHandler.fixNode(candidate: selectedValue, respectCursorPushing: true) let inputting = keyHandler.buildInputtingState @@ -137,10 +137,10 @@ extension ctlInputMethod: ctlCandidateDelegate { if let state = state as? InputState.AssociatedPhrases { let selectedValue = state.candidates[index] - handle(state: InputState.Committing(textToCommit: selectedValue)) + handle(state: InputState.Committing(textToCommit: selectedValue.1)) if mgrPrefs.associatedPhrasesEnabled, let associatePhrases = keyHandler.buildAssociatePhraseState( - withKey: selectedValue, isTypingVertical: state.isTypingVertical + withKey: selectedValue.1, isTypingVertical: state.isTypingVertical ), !associatePhrases.candidates.isEmpty { handle(state: associatePhrases) diff --git a/Source/Modules/ControllerModules/ctlInputMethod_HandleDisplay.swift b/Source/Modules/ControllerModules/ctlInputMethod_HandleDisplay.swift index fb1819a5..0f180a79 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_HandleDisplay.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_HandleDisplay.swift @@ -55,7 +55,7 @@ extension ctlInputMethod { return false } var isCandidateWindowVertical: Bool { - var candidates: [String] = [] + var candidates: [(String, String)] = .init() if let state = state as? InputState.ChoosingCandidate { candidates = state.candidates } else if let state = state as? InputState.AssociatedPhrases { @@ -63,13 +63,11 @@ extension ctlInputMethod { } if isTypingVertical { return true } // 以上是通用情形。接下來決定橫排輸入時是否使用縱排選字窗。 - candidates.sort { - $0.count > $1.count - } + // 因為在拿候選字陣列時已經排序過了,所以這裡不用再多排序。 // 測量每頁顯示候選字的累計總長度。如果太長的話就強制使用縱排候選字窗。 // 範例:「屬實牛逼」(會有一大串各種各樣的「鼠食牛Beer」的 emoji)。 let maxCandidatesPerPage = mgrPrefs.candidateKeys.count - let firstPageCandidates = candidates[0.. Int(round(Double(maxCandidatesPerPage) * 1.8)) // 上面這句如果是 true 的話,就會是縱排;反之則為橫排。 } diff --git a/Source/UI/CandidateUI/ctlCandidate.swift b/Source/UI/CandidateUI/ctlCandidate.swift index 29e06964..4142aff0 100644 --- a/Source/UI/CandidateUI/ctlCandidate.swift +++ b/Source/UI/CandidateUI/ctlCandidate.swift @@ -40,7 +40,7 @@ public class CandidateKeyLabel: NSObject { public protocol ctlCandidateDelegate: AnyObject { func candidateCountForController(_ controller: ctlCandidate) -> Int func ctlCandidate(_ controller: ctlCandidate, candidateAtIndex index: Int) - -> String + -> (String, String) func ctlCandidate( _ controller: ctlCandidate, didSelectCandidateAtIndex index: Int ) diff --git a/Source/UI/CandidateUI/ctlCandidateUniversal.swift b/Source/UI/CandidateUI/ctlCandidateUniversal.swift index 967bf988..2fc4dd31 100644 --- a/Source/UI/CandidateUI/ctlCandidateUniversal.swift +++ b/Source/UI/CandidateUI/ctlCandidateUniversal.swift @@ -559,7 +559,7 @@ extension ctlCandidateUniversal { } candidateView.set(keyLabelFont: keyLabelFont, candidateFont: candidateFont) - var candidates = [String]() + var candidates = [(String, String)]() let count = delegate.candidateCountForController(self) let keyLabelCount = keyLabels.count @@ -569,7 +569,7 @@ extension ctlCandidateUniversal { candidates.append(candidate) } candidateView.set( - keyLabels: keyLabels.map(\.displayedText), displayedCandidates: candidates + keyLabels: keyLabels.map(\.displayedText), displayedCandidates: candidates.map(\.1) ) var newSize = candidateView.sizeForView var frameRect = candidateView.frame diff --git a/vChewingTests/KeyHandlerTestsNormalCHS.swift b/vChewingTests/KeyHandlerTestsNormalCHS.swift index dd334d45..15e48e11 100644 --- a/vChewingTests/KeyHandlerTestsNormalCHS.swift +++ b/vChewingTests/KeyHandlerTestsNormalCHS.swift @@ -365,7 +365,7 @@ class KeyHandlerTestsNormalCHS: XCTestCase { XCTAssertTrue(state is InputState.ChoosingCandidate, "\(state)") if let state = state as? InputState.ChoosingCandidate { - XCTAssertTrue(state.candidates.contains("!")) + XCTAssertTrue(state.candidates.map(\.1).contains("!")) } mgrPrefs.halfWidthPunctuationEnabled = enabled } @@ -993,7 +993,7 @@ class KeyHandlerTestsNormalCHS: XCTestCase { XCTAssertEqual(state.composingBuffer, "你") XCTAssertEqual(state.cursorIndex, 1) let candidates = state.candidates - XCTAssertTrue(candidates.contains("你")) + XCTAssertTrue(candidates.map(\.1).contains("你")) } } @@ -1030,7 +1030,7 @@ class KeyHandlerTestsNormalCHS: XCTestCase { XCTAssertEqual(state.composingBuffer, "你") XCTAssertEqual(state.cursorIndex, 1) let candidates = state.candidates - XCTAssertTrue(candidates.contains("你")) + XCTAssertTrue(candidates.map(\.1).contains("你")) } mgrPrefs.chooseCandidateUsingSpace = enabled } diff --git a/vChewingTests/KeyHandlerTestsSCPCCHT.swift b/vChewingTests/KeyHandlerTestsSCPCCHT.swift index c8036823..25cb3747 100644 --- a/vChewingTests/KeyHandlerTestsSCPCCHT.swift +++ b/vChewingTests/KeyHandlerTestsSCPCCHT.swift @@ -85,7 +85,7 @@ class KeyHandlerTestsSCPCCHT: XCTestCase { XCTAssertTrue(state is InputState.ChoosingCandidate, "\(state)") if let state = state as? InputState.ChoosingCandidate { - XCTAssertTrue(state.candidates.contains(",")) + XCTAssertTrue(state.candidates.map(\.1).contains(",")) } } @@ -238,7 +238,7 @@ class KeyHandlerTestsSCPCCHT: XCTestCase { XCTAssertTrue(state is InputState.ChoosingCandidate, "\(state)") if let state = state as? InputState.ChoosingCandidate { - XCTAssertTrue(state.candidates.contains("你")) + XCTAssertTrue(state.candidates.map(\.1).contains("你")) } } @@ -315,7 +315,7 @@ class KeyHandlerTestsSCPCCHT: XCTestCase { print("\(state)") // XCTAssertTrue(state is InputState.AssociatedPhrases, "\(state)") // if let state = state as? InputState.AssociatedPhrases { - // XCTAssertTrue(state.candidates.contains("百五")) + // XCTAssertTrue(state.candidates.map(\.1).contains("百五")) // } mgrPrefs.associatedPhrasesEnabled = enabled }