vChewing-macOS/Packages/vChewing_MainAssembly/Sources/MainAssembly/IMEStateData.swift

300 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// 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
public struct IMEStateData: IMEStateDataProtocol {
private static var minCandidateLength: Int {
PrefMgr.shared.allowBoostingSingleKanjiAsUserPhrase ? 1 : 2
}
static var allowedMarkLengthRange: ClosedRange<Int> {
Self.minCandidateLength ... PrefMgr.shared.maxCandidateLength
}
public var displayedText: String = "" {
didSet {
if displayedText.rangeOfCharacter(from: .newlines) != nil {
displayedText = displayedText.trimmingCharacters(in: .newlines)
}
}
}
public var displayedTextConverted: String {
///
var result = ChineseConverter.kanjiConversionIfRequired(displayedText)
if result.utf16.count != displayedText.utf16.count
|| result.count != displayedText.count
{
result = displayedText
}
return result
}
// MARK: Cursor & Marker & Range for UTF8
public var cursor: Int = 0 {
didSet {
cursor = min(max(cursor, 0), displayedText.count)
}
}
public var marker: Int = 0 {
didSet {
marker = min(max(marker, 0), displayedText.count)
}
}
public var markedRange: Range<Int> {
min(cursor, marker) ..< max(cursor, marker)
}
// MARK: Cursor & Marker & Range for UTF16 (Read-Only)
/// IMK UTF8 emoji
/// Swift.utf16NSString.length()
///
public var u16Cursor: Int {
let upperBound = max(0, min(cursor, displayedText.count))
return displayedText.map(\.description)[0 ..< upperBound].joined().utf16.count
}
public var u16Marker: Int {
let upperBound = max(0, min(marker, displayedText.count))
return displayedText.map(\.description)[0 ..< upperBound].joined().utf16.count
}
public var u16MarkedRange: Range<Int> {
min(u16Cursor, u16Marker) ..< max(u16Cursor, u16Marker)
}
// MARK: Other data for non-empty states.
public var markedTargetExists: Bool {
let pair = userPhraseKVPair
return LMMgr.checkIfPhrasePairExists(
userPhrase: pair.value, mode: IMEApp.currentInputMode, keyArray: pair.keyArray
)
}
public var markedTargetIsCurrentlyFiltered: Bool {
let pair = userPhraseKVPair
return LMMgr.checkIfPhrasePairIsFiltered(
userPhrase: pair.value, mode: IMEApp.currentInputMode, keyArray: pair.keyArray
)
}
public var displayTextSegments = [String]() {
didSet {
displayedText = displayTextSegments.joined()
}
}
public var highlightAtSegment: Int?
public var reading: String = ""
public var markedReadings = [String]()
public var candidates = [(keyArray: [String], value: String)]()
public var textToCommit: String = ""
public var isFilterable: Bool {
guard isMarkedLengthValid else { return false } //
guard markedTargetExists else { return false } //
guard markedReadings.count == 1 else { return true } // 1
//
return LMMgr.countPhrasePairs(keyArray: markedReadings, mode: IMEApp.currentInputMode) > 1
}
public var isMarkedLengthValid: Bool {
Self.allowedMarkLengthRange.contains(markedRange.count)
}
// MARK: Tooltip neta.
public var tooltip: String = ""
public var tooltipDuration: Double = 1.0
public var tooltipBackupForInputting: String = ""
public var tooltipColorState: TooltipColorState = .normal
}
// 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 {
var readingThreadForDisplay: String {
var arrOutput = [String]()
for neta in markedReadings {
if neta.isEmpty { continue }
if neta.contains("_") {
arrOutput.append("??")
continue
}
neta.components(separatedBy: "-").forEach { subNeta in
var subNeta = subNeta
if !PrefMgr.shared.cassetteEnabled {
if PrefMgr.shared.showHanyuPinyinInCompositionBuffer,
PrefMgr.shared.alwaysShowTooltipTextsHorizontally || !SessionCtl.isVerticalTyping
{
// ->->調
subNeta = Tekkon.restoreToneOneInPhona(target: subNeta)
subNeta = Tekkon.cnvPhonaToHanyuPinyin(targetJoined: subNeta)
subNeta = Tekkon.cnvHanyuPinyinToTextbookStyle(targetJoined: subNeta)
} else {
subNeta = Tekkon.cnvPhonaToTextbookReading(target: subNeta)
}
}
arrOutput.append(subNeta)
}
}
return arrOutput.joined(separator: "\u{A0}")
}
var userPhraseKVPair: (keyArray: [String], value: String) {
let key = markedReadings
let value = displayedText.map(\.description)[markedRange].joined()
return (key, value)
}
mutating func updateTooltipForMarking() {
var tooltipForMarking: String {
let pair = userPhraseKVPair
if markedRange.isEmpty {
return ""
}
let text = pair.value
let readingDisplay = readingThreadForDisplay
if markedRange.count < Self.allowedMarkLengthRange.lowerBound {
tooltipColorState = .denialInsufficiency
return String(
format: NSLocalizedString(
"\"%@\" length must ≥ 2 for a user phrase.", comment: ""
) + "\n" + readingDisplay, text
)
} else if markedRange.count > Self.allowedMarkLengthRange.upperBound {
tooltipColorState = .denialOverflow
return String(
format: NSLocalizedString(
"\"%@\" length should ≤ %d for a user phrase.", comment: ""
) + "\n" + readingDisplay, text, Self.allowedMarkLengthRange.upperBound
)
}
if markedTargetExists {
tooltipColorState = .prompt
switch isFilterable {
case false:
return String(
format: NSLocalizedString(
"\"%@\" already exists:\n ENTER to boost, SHIFT+COMMAND+ENTER to nerf.",
comment: ""
) + "\n" + readingDisplay, text
)
case true:
return String(
format: NSLocalizedString(
"\"%@\" already exists:\n ENTER to boost, SHIFT+COMMAND+ENTER to nerf, \n BackSpace or Delete key to exclude.",
comment: ""
) + "\n" + readingDisplay, text
)
}
}
if markedTargetIsCurrentlyFiltered {
tooltipColorState = .information
return String(
format: NSLocalizedString("\"%@\" selected. ENTER to unfilter this phrase.", comment: "") + "\n"
+ readingDisplay,
text
)
}
tooltipColorState = .normal
return String(
format: NSLocalizedString("\"%@\" selected. ENTER to add user phrase.", comment: "") + "\n"
+ readingDisplay,
text
)
}
tooltip = tooltipForMarking
if PrefMgr.shared.phraseReplacementEnabled {
tooltipColorState = .warning
tooltip += "\n" + NSLocalizedString(
"⚠︎ Phrase replacement mode enabled, interfering user phrase entry.", comment: ""
)
}
}
}