diff --git a/Packages/vChewing_Shared/Sources/Shared/Protocols/IMEStateProtocol.swift b/Packages/vChewing_Shared/Sources/Shared/Protocols/IMEStateProtocol.swift index 261c0ce1..905598bd 100644 --- a/Packages/vChewing_Shared/Sources/Shared/Protocols/IMEStateProtocol.swift +++ b/Packages/vChewing_Shared/Sources/Shared/Protocols/IMEStateProtocol.swift @@ -7,6 +7,7 @@ // requirements defined in MIT License. import Cocoa +import InputMethodKit // 所有 IMEState 均遵守該協定: public protocol IMEStateProtocol { @@ -32,6 +33,7 @@ public protocol IMEStateProtocol { var u16Cursor: Int { get } var cursor: Int { get set } var marker: Int { get set } + func attributedString(for session: IMKInputController) -> NSAttributedString } public protocol IMEStateDataProtocol { @@ -57,4 +59,7 @@ public protocol IMEStateDataProtocol { var userPhraseKVPair: (keyArray: [String], value: String) { get } var tooltipColorState: TooltipColorState { get set } mutating func updateTooltipForMarking() + func attributedStringNormal(for session: IMKInputController) -> NSAttributedString + func attributedStringMarking(for session: IMKInputController) -> NSAttributedString + func attributedStringPlaceholder(for session: IMKInputController) -> NSAttributedString } diff --git a/Source/Modules/IMEState.swift b/Source/Modules/IMEState.swift index fc2c4085..9df32025 100644 --- a/Source/Modules/IMEState.swift +++ b/Source/Modules/IMEState.swift @@ -6,6 +6,7 @@ // marks, or product names of Contributor, except as required to fulfill notice // requirements defined in MIT License. +import InputMethodKit import LangModelAssembly import Shared @@ -197,6 +198,14 @@ public extension IMEState { } } + func attributedString(for session: IMKInputController) -> NSAttributedString { + switch type { + case .ofMarking: return data.attributedStringMarking(for: session) + case .ofAssociates, .ofSymbolTable: return data.attributedStringPlaceholder(for: session) + default: return data.attributedStringNormal(for: session) + } + } + /// 該參數僅用作輔助判斷。在 InputHandler 內使用的話,必須再檢查 !compositor.isEmpty。 var hasComposition: Bool { switch type { diff --git a/Source/Modules/IMEStateData.swift b/Source/Modules/IMEStateData.swift index 19f428fa..b6019d36 100644 --- a/Source/Modules/IMEStateData.swift +++ b/Source/Modules/IMEStateData.swift @@ -6,6 +6,7 @@ // marks, or product names of Contributor, except as required to fulfill notice // requirements defined in MIT License. +import InputMethodKit import Shared import Tekkon import TooltipUI @@ -173,6 +174,72 @@ public struct IMEStateData: IMEStateDataProtocol { } } +// MARK: - AttributedString 生成器 + +public extension IMEStateData { + func attributedStringNormal(for session: IMKInputController) -> NSAttributedString { + /// 考慮到因為滑鼠點擊等其它行為導致的組字區內容遞交情況, + /// 這裡對組字區內容也加上康熙字轉換或者 JIS 漢字轉換處理。 + let attributedString = NSMutableAttributedString(string: displayedTextConverted) + var newBegin = 0 + for (i, neta) in displayTextSegments.enumerated() { + let rangeNow = NSRange(location: newBegin, length: neta.utf16.count) + /// 不能用 .thick,否則會看不到游標。 + var theAttributes: [NSAttributedString.Key: Any] + = session.mark(forStyle: kTSMHiliteConvertedText, at: rangeNow) + as? [NSAttributedString.Key: Any] + ?? [.underlineStyle: NSUnderlineStyle.single.rawValue] + theAttributes[.markedClauseSegment] = i + attributedString.setAttributes(theAttributes, range: rangeNow) + newBegin += neta.utf16.count + } + return attributedString + } + + func attributedStringMarking(for session: IMKInputController) -> NSAttributedString { + /// 考慮到因為滑鼠點擊等其它行為導致的組字區內容遞交情況, + /// 這裡對組字區內容也加上康熙字轉換或者 JIS 漢字轉換處理。 + let attributedString = NSMutableAttributedString(string: displayedTextConverted) + let u16MarkedRange = u16MarkedRange + let range1 = NSRange(location: 0, length: u16MarkedRange.lowerBound) + let range2 = NSRange( + location: u16MarkedRange.lowerBound, + length: u16MarkedRange.upperBound - u16MarkedRange.lowerBound + ) + let range3 = NSRange( + location: u16MarkedRange.upperBound, + length: displayedTextConverted.utf16.count - u16MarkedRange.upperBound + ) + var rawAttribute1: [NSAttributedString.Key: Any] + = session.mark(forStyle: kTSMHiliteConvertedText, at: range1) + as? [NSAttributedString.Key: Any] + ?? [.underlineStyle: NSUnderlineStyle.single.rawValue] + rawAttribute1[.markedClauseSegment] = 0 + var rawAttribute2: [NSAttributedString.Key: Any] + = session.mark(forStyle: kTSMHiliteSelectedConvertedText, at: range2) + as? [NSAttributedString.Key: Any] + ?? [.underlineStyle: NSUnderlineStyle.thick.rawValue] + rawAttribute2[.markedClauseSegment] = 1 + var rawAttribute3: [NSAttributedString.Key: Any] + = session.mark(forStyle: kTSMHiliteConvertedText, at: range3) + as? [NSAttributedString.Key: Any] + ?? [.underlineStyle: NSUnderlineStyle.single.rawValue] + rawAttribute3[.markedClauseSegment] = 2 + attributedString.setAttributes(rawAttribute1, range: range1) + attributedString.setAttributes(rawAttribute2, range: range2) + attributedString.setAttributes(rawAttribute3, range: range3) + return attributedString + } + + func attributedStringPlaceholder(for session: IMKInputController) -> NSAttributedString { + let attributes: [NSAttributedString.Key: Any] + = session.mark(forStyle: kTSMHiliteSelectedRawText, at: .zero) + as? [NSAttributedString.Key: Any] + ?? [.underlineStyle: NSUnderlineStyle.single.rawValue] + return .init(string: " ", attributes: attributes) + } +} + // MARK: - IMEState 工具函式 public extension IMEStateData {