Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,30 @@
return true
}

@discardableResult
public static func selectSystemABCInputSource() -> Bool {
// 1. Check if current ASCII capable keyboard input source is selectable, activated, and not vChewing.
if let currentASCII = TISCopyCurrentASCIICapableKeyboardInputSource()?.takeRetainedValue() {
if !currentASCII.identifier.lowercased().contains("vchewing"), currentASCII.isSelectable, currentASCII.isActivated {
return currentASCII.select()
}
}
// 2. Search for com.apple.keylayout.ABC among activated and selectable sources.
let allSources = rawTISInputSources(onlyASCII: true)
if let abcSource = allSources.first(where: { $0.identifier == "com.apple.keylayout.ABC" && $0.isSelectable && $0.isActivated }) {
return abcSource.select()
}
// 3. Fallback: Any selectable and activated ASCII layout that is not vChewing.
if let anyASCII = allSources.first(where: { !$0.identifier.lowercased().contains("vchewing") && $0.isSelectable && $0.isActivated }) {
return anyASCII.select()
}
// 4. Last resort: Any selectable ABC layout even if not marked activated yet.
if let abcSource = allSources.first(where: { $0.identifier == "com.apple.keylayout.ABC" && $0.isSelectable }) {
return abcSource.select()
}
return false
}

@discardableResult
public func deactivate() -> Bool {
TISDisableInputSource(self) == noErr
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ extension SessionHost {
// IMEApp 動作依賴。
host.isKeyboardJIS = { IMEApp.isKeyboardJIS }
host.buzz = { IMEApp.buzz() }
host.switchToSystemABCInputSource = { TISInputSource.selectSystemABCInputSource() }
// LMMgr 動作依賴。
host.isCoreDBConnected = { LMMgr.isCoreDBConnected }
host.syncLMPrefs = { LMMgr.syncLMPrefs() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,32 @@ extension MainAssemblyTests {
#expect(testSession.isFuriousCopilotCandidateWindowVisible)
}
}

/// 測試連續錯誤自動切換至 ABC 後,重新切回唯音時自動恢復為中文模式。
@Test
func test508_ActivationAfterAutoSwitchToABC_ResetsASCIIModeToChinese() throws {
#expect(testSession.isActivated)
InputSession.current = testSession
defer {
InputSession.isAutoSwitchedToABC = false
testSession.isASCIIMode = false
InputSession.isASCIIModeForAllClients = false
}

// 模擬自動切換至 ABC 後的狀態:isASCIIMode == true, isAutoSwitchedToABC == true
testSession.isASCIIMode = true
InputSession.isAutoSwitchedToABC = true

#expect(testSession.isASCIIMode == true)
#expect(InputSession.isAutoSwitchedToABC == true)

// 模擬使用者切回唯音:呼叫 performServerActivation
testSession.performServerActivation()

// 驗證 isAutoSwitchedToABC 已被重置為 false,且 isASCIIMode 已自動恢復為 false(中文模式)
#expect(InputSession.isAutoSwitchedToABC == false)
#expect(testSession.isASCIIMode == false)
#expect(InputSession.isASCIIModeForAllClients == false)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ extension SettingsPanesCocoa {
fixWidth: contentWidth,
prefUITab: .tabGeneral
)
UserDef.kAutoSwitchToAlphanumericalOnConsecutiveErrors.renderCocoa(
fixWidth: contentWidth,
prefUITab: .tabGeneral
)
UserDef.kConsecutiveTypingErrorsThreshold.renderCocoa(
fixWidth: contentWidth,
prefUITab: .tabGeneral
)
UserDef.kShowHanyuPinyinInCompositionBuffer.renderCocoa(
fixWidth: contentWidth,
prefUITab: .tabGeneral
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ public struct VwrSettingsPaneGeneral: View {
SpeechSputnik.shared.refreshStatus()
}
UserDef.kAutoCorrectReadingCombination.renderUI()
UserDef.kAutoSwitchToAlphanumericalOnConsecutiveErrors.renderUI()
UserDef.kConsecutiveTypingErrorsThreshold.renderUI()
UserDef.kShowHanyuPinyinInCompositionBuffer.renderUI()
UserDef.kKeepReadingUponCompositionError.renderUI()
UserDef.kClassicHaninKeyboardSymbolModeShortcutEnabled.renderUI()
Expand Down
6 changes: 6 additions & 0 deletions Packages/vChewing_Shared/Sources/Shared/PrefMgr_Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ public final class PrefMgr: PrefMgrProtocol, Sendable {
@AppProperty(userDef: .kAutoCorrectReadingCombination)
public var autoCorrectReadingCombination: Bool

@AppProperty(userDef: .kAutoSwitchToAlphanumericalOnConsecutiveErrors)
public var autoSwitchToAlphanumericalOnConsecutiveErrors: Bool

@AppProperty(userDef: .kConsecutiveTypingErrorsThreshold)
public var consecutiveTypingErrorsThreshold: Int

@AppProperty(userDef: .kAlsoConfirmAssociatedCandidatesByEnter)
public var alsoConfirmAssociatedCandidatesByEnter: Bool

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ public protocol PrefMgrProtocol {
var reducePOMLifetimeToNoMoreThan12Hours: Bool { get set }
var useFixedCandidateOrderOnSelection: Bool { get set }
var autoCorrectReadingCombination: Bool { get set }
var autoSwitchToAlphanumericalOnConsecutiveErrors: Bool { get set }
var consecutiveTypingErrorsThreshold: Int { get set }
var readingNarrationCoverage: Int { get set }
var alsoConfirmAssociatedCandidatesByEnter: Bool { get set }
var keepReadingUponCompositionError: Bool { get set }
Expand Down
21 changes: 21 additions & 0 deletions Packages/vChewing_Shared/Sources/Shared/UserDef/UserDef.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ nonisolated public enum UserDef: String, CaseIterable, Identifiable, Sendable {
case kFetchSuggestionsFromPerceptionOverrideModel = "FetchSuggestionsFromPerceptionOverrideModel"
case kUseFixedCandidateOrderOnSelection = "UseFixedCandidateOrderOnSelection"
case kAutoCorrectReadingCombination = "AutoCorrectReadingCombination"
case kAutoSwitchToAlphanumericalOnConsecutiveErrors = "AutoSwitchToAlphanumericalOnConsecutiveErrors"
case kConsecutiveTypingErrorsThreshold = "ConsecutiveTypingErrorsThreshold"
case kReadingNarrationCoverage = "ReadingNarrationCoverage"
case kAlsoConfirmAssociatedCandidatesByEnter = "AlsoConfirmAssociatedCandidatesByEnter"
case kKeepReadingUponCompositionError = "KeepReadingUponCompositionError"
Expand Down Expand Up @@ -374,6 +376,7 @@ nonisolated extension UserDef {
case .kSpecifyIntonationKeyBehavior: 0 ... 2
case .kSpecifyShiftBackSpaceKeyBehavior: 0 ... 2
case .kUpperCaseLetterKeyBehavior: 0 ... 4
case .kConsecutiveTypingErrorsThreshold: 3 ... 8
case .kReadingNarrationCoverage: 0 ... 2
case .kRomanNumeralOutputFormat: 0 ... 3
case .kSpecifyCmdOptCtrlEnterBehavior: 0 ... 4
Expand Down Expand Up @@ -497,6 +500,8 @@ nonisolated extension UserDef {
case .kFetchSuggestionsFromPerceptionOverrideModel: return .bool(true)
case .kUseFixedCandidateOrderOnSelection: return .bool(false)
case .kAutoCorrectReadingCombination: return .bool(true)
case .kAutoSwitchToAlphanumericalOnConsecutiveErrors: return .bool(true)
case .kConsecutiveTypingErrorsThreshold: return .integer(5)
case .kReadingNarrationCoverage: return .integer(0)
case .kAlsoConfirmAssociatedCandidatesByEnter: return .bool(false)
case .kKeepReadingUponCompositionError: return .bool(false)
Expand Down Expand Up @@ -898,6 +903,22 @@ nonisolated extension UserDef {
shortTitle: "i18n:UserDef.kAutoCorrectReadingCombination.shortTitle",
description: "i18n:UserDef.kAutoCorrectReadingCombination.description"
)
case .kAutoSwitchToAlphanumericalOnConsecutiveErrors: return .init(
userDef: self,
shortTitle: "i18n:UserDef.kAutoSwitchToAlphanumericalOnConsecutiveErrors.shortTitle",
description: "i18n:UserDef.kAutoSwitchToAlphanumericalOnConsecutiveErrors.description"
)
case .kConsecutiveTypingErrorsThreshold: return .init(
userDef: self,
shortTitle: "i18n:UserDef.kConsecutiveTypingErrorsThreshold.shortTitle",
description: "i18n:UserDef.kConsecutiveTypingErrorsThreshold.description",
options: {
var result = [Int: String]()
guard let validNumeralValueRange else { return nil }
validNumeralValueRange.forEach { result[$0] = $0.description }
return result.isEmpty ? nil : result
}()
)
case .kReadingNarrationCoverage: return .init(
userDef: self, shortTitle: "i18n:UserDef.kReadingNarrationCoverage.shortTitle",
description: "i18n:UserDef.kReadingNarrationCoverage.description",
Expand Down
10 changes: 10 additions & 0 deletions Packages/vChewing_Tekkon/Sources/Tekkon/Tekkon_Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ public enum Tekkon {
allowedConsonants + allowedSemivowels + allowedVowels + allowedIntonations
}

/// 所有合法的國語注音音節與合法前綴(不含聲調)集合。
public static let allValidMandarinSyllables: Set<String> = {
var set = Set<String>()
for (phona, _) in arrPhonaToHanyuPinyin {
if [" ", "ˊ", "ˇ", "ˋ", "˙"].contains(phona) { continue }
set.insert(phona)
}
return set
}()

// MARK: Internal

/// 原始轉換對照表資料貯存專用佇列(數字標調格式)。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ public final class InputHandler: @MainActor InputHandlerProtocol {
public var strCodePointBuffer = "" // 內碼輸入專用組碼區
public var calligrapher = "" // 磁帶專用組筆區
public var mixedAlphanumericalBuffer = "" // 混輸暫存 ASCII 緩衝區
public var consecutiveTypingErrors = [String]() // 連續輸入錯誤鍵暫存區
public var inFlightComposerKeys = [String]() // 當前注拼槽對應的鍵入字元暫存區
public var furiousTrail = [String]() // 狂拼模式:自動 chop 提交鍵對應的拼音字母 blob trail
public var furiousHighlightOverride: CandidateInState? // 狂拼 copilot 窗高亮候選(當拍消費)
public var furiousCoSegmentedOffers = [FuriousCoSegmentedOffer]() // 狂拼 copilot 窗聯合重切(P164)的替代切分 offers
Expand Down
Loading