vChewing-macOS/Source/Modules/IMEStateData.swift

267 lines
8.7 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 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 = ""
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 {
displayedText.charComponents[0..<cursor].joined().utf16.count
}
public var u16Marker: Int {
displayedText.charComponents[0..<marker].joined().utf16.count
}
public var u16MarkedRange: Range<Int> {
min(u16Cursor, u16Marker)..<max(u16Cursor, u16Marker)
}
// MARK: Other data for non-empty states.
public var isVerticalTyping = false
public var markedTargetExists: Bool {
let pair = userPhraseKVPair
return LMMgr.checkIfUserPhraseExist(
userPhrase: pair.1, mode: IMEApp.currentInputMode, key: pair.0
)
}
public var displayTextSegments = [String]() {
didSet {
displayedText = displayTextSegments.joined()
}
}
public var reading: String = ""
public var markedReadings = [String]()
public var candidates = [(String, String)]()
public var textToCommit: String = ""
public var tooltip: String = ""
public var tooltipBackupForInputting: String = ""
public var attributedStringPlaceholder: NSAttributedString = .init(
string: " ",
attributes: [
.underlineStyle: NSUnderlineStyle.single.rawValue,
.markedClauseSegment: 0,
]
)
public var isFilterable: Bool {
markedTargetExists ? isMarkedLengthValid : false
}
public var isMarkedLengthValid: Bool {
Self.allowedMarkLengthRange.contains(markedRange.count)
}
public var tooltipColorState: TooltipColorState = .normal
public var attributedStringNormal: NSAttributedString {
///
/// JIS
let attributedString = NSMutableAttributedString(string: displayedTextConverted)
var newBegin = 0
for (i, neta) in displayTextSegments.enumerated() {
attributedString.setAttributes(
[
/// .thick
.underlineStyle: NSUnderlineStyle.single.rawValue,
.markedClauseSegment: i,
], range: NSRange(location: newBegin, length: neta.utf16.count)
)
newBegin += neta.utf16.count
}
return attributedString
}
public var attributedStringMarking: NSAttributedString {
///
/// JIS
let attributedString = NSMutableAttributedString(string: displayedTextConverted)
let end = u16MarkedRange.upperBound
attributedString.setAttributes(
[
.underlineStyle: NSUnderlineStyle.single.rawValue,
.markedClauseSegment: 0,
], range: NSRange(location: 0, length: u16MarkedRange.lowerBound)
)
attributedString.setAttributes(
[
.underlineStyle: NSUnderlineStyle.thick.rawValue,
.markedClauseSegment: 1,
],
range: NSRange(
location: u16MarkedRange.lowerBound,
length: u16MarkedRange.upperBound - u16MarkedRange.lowerBound
)
)
attributedString.setAttributes(
[
.underlineStyle: NSUnderlineStyle.single.rawValue,
.markedClauseSegment: 2,
],
range: NSRange(
location: end,
length: displayedTextConverted.utf16.count - end
)
)
return attributedString
}
}
// MARK: - IMEState
extension IMEStateData {
public var doesUserPhraseExist: Bool {
let text = displayedText.charComponents[markedRange].joined()
let joined = markedReadings.joined(separator: "-")
return LMMgr.checkIfUserPhraseExist(
userPhrase: text, mode: IMEApp.currentInputMode, key: joined
)
}
public var readingThreadForDisplay: String {
var arrOutput = [String]()
for neta in markedReadings {
var neta = neta
if neta.isEmpty { continue }
if neta.contains("_") {
arrOutput.append("??")
continue
}
if PrefMgr.shared.showHanyuPinyinInCompositionBuffer,
PrefMgr.shared.alwaysShowTooltipTextsHorizontally || !isVerticalTyping
{
// ->->調
neta = Tekkon.restoreToneOneInZhuyinKey(target: neta)
neta = Tekkon.cnvPhonaToHanyuPinyin(target: neta)
neta = Tekkon.cnvHanyuPinyinToTextbookStyle(target: neta)
} else {
neta = Tekkon.cnvZhuyinChainToTextbookReading(target: neta)
}
arrOutput.append(neta)
}
return arrOutput.joined(separator: "\u{A0}")
}
public var userPhraseKVPair: (String, String) {
let key = markedReadings.joined(separator: "-")
let value = displayedText.charComponents[markedRange].joined()
return (key, value)
}
public var userPhraseDumped: String {
let pair = userPhraseKVPair
let nerfedScore = SessionCtl.areWeNerfing && markedTargetExists ? " -114.514" : ""
return "\(pair.1) \(pair.0)\(nerfedScore)"
}
public var userPhraseDumpedConverted: String {
let pair = userPhraseKVPair
let text = ChineseConverter.crossConvert(pair.1)
let nerfedScore = SessionCtl.areWeNerfing && markedTargetExists ? " -114.514" : ""
let convertedMark = "#𝙃𝙪𝙢𝙖𝙣𝘾𝙝𝙚𝙘𝙠𝙍𝙚𝙦𝙪𝙞𝙧𝙚𝙙"
return "\(text) \(pair.0)\(nerfedScore)\t\(convertedMark)"
}
public mutating func updateTooltipForMarking() {
var tooltipForMarking: String {
let pair = userPhraseKVPair
if PrefMgr.shared.phraseReplacementEnabled {
tooltipColorState = .warning
return NSLocalizedString(
"⚠︎ Phrase replacement mode enabled, interfering user phrase entry.", comment: ""
)
}
if markedRange.isEmpty {
return ""
}
let text = pair.1
let readingDisplay = readingThreadForDisplay
if markedRange.count < IMEStateData.allowedMarkLengthRange.lowerBound {
tooltipColorState = .denialInsufficiency
return String(
format: NSLocalizedString(
"\"%@\" length must ≥ 2 for a user phrase.", comment: ""
) + "\n" + readingDisplay, text
)
} else if markedRange.count > IMEStateData.allowedMarkLengthRange.upperBound {
tooltipColorState = .denialOverflow
return String(
format: NSLocalizedString(
"\"%@\" length should ≤ %d for a user phrase.", comment: ""
) + "\n" + readingDisplay, text, IMEStateData.allowedMarkLengthRange.upperBound
)
}
if markedTargetExists {
tooltipColorState = .prompt
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
)
}
tooltipColorState = .normal
return String(
format: NSLocalizedString("\"%@\" selected. ENTER to add user phrase.", comment: "") + "\n"
+ readingDisplay,
text
)
}
tooltip = tooltipForMarking
}
}