TDKCandidates // Massive renovation + Cocoa legacy mode.

This commit is contained in:
ShikiSuen 2023-02-18 16:49:50 +08:00
parent 4a2db996a0
commit 2bfad15422
14 changed files with 1341 additions and 1337 deletions

View File

@ -1,221 +0,0 @@
// (c) 2022 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 Cocoa
import Shared
import SwiftUI
import SwiftUIBackports
// MARK: - Classes used by Candidate Window
/// class 便
public class CandidateCellData: Hashable {
public var locale = ""
public static var unifiedSize: Double = 16
public static var highlightBackground: NSColor = {
if #available(macOS 10.14, *) {
return .selectedContentBackgroundColor
}
return NSColor.alternateSelectedControlColor
}()
public var key: String
public var displayedText: String
public var size: Double { Self.unifiedSize }
public var isSelected: Bool = false
public var whichRow: Int = 0 //
public var whichColumn: Int = 0 //
public var index: Int = 0
public var subIndex: Int = 0
public var fontSizeCandidate: Double { CandidateCellData.unifiedSize }
public var fontSizeKey: Double { max(ceil(CandidateCellData.unifiedSize * 0.6), 11) }
public var fontColorKey: NSColor {
isSelected ? .selectedMenuItemTextColor.withAlphaComponent(0.8) : .secondaryLabelColor
}
public var fontColorCandidate: NSColor { isSelected ? .selectedMenuItemTextColor : .labelColor }
public init(key: String, displayedText: String, isSelected: Bool = false) {
self.key = key
self.displayedText = displayedText
self.isSelected = isSelected
}
public static func == (lhs: CandidateCellData, rhs: CandidateCellData) -> Bool {
lhs.key == rhs.key && lhs.displayedText == rhs.displayedText
}
public func hash(into hasher: inout Hasher) {
hasher.combine(key)
hasher.combine(displayedText)
}
public var cellLength: Int {
if displayedText.count <= 2 { return Int(ceil(size * 3)) }
return Int(ceil(attributedStringForLengthCalculation.boundingDimension.width))
}
public var attributedStringHeader: NSAttributedString {
let paraStyleKey = NSMutableParagraphStyle()
paraStyleKey.setParagraphStyle(NSParagraphStyle.default)
paraStyleKey.alignment = .natural
let paraStyle = NSMutableParagraphStyle()
paraStyle.setParagraphStyle(NSParagraphStyle.default)
paraStyle.alignment = .natural
let theFontForCandidateKey: NSFont = {
if #available(macOS 10.15, *) {
return NSFont.monospacedSystemFont(ofSize: fontSizeKey, weight: .regular)
}
return NSFont.monospacedDigitSystemFont(ofSize: fontSizeKey, weight: .regular)
}()
var attrKey: [NSAttributedString.Key: AnyObject] = [
.font: theFontForCandidateKey,
.paragraphStyle: paraStyleKey,
]
if isSelected {
attrKey[.foregroundColor] = NSColor.white.withAlphaComponent(0.8)
} else {
attrKey[.foregroundColor] = NSColor.secondaryLabelColor
}
let attrStrKey = NSMutableAttributedString(string: key, attributes: attrKey)
return attrStrKey
}
public var attributedStringForLengthCalculation: NSAttributedString {
let paraStyleKey = NSMutableParagraphStyle()
paraStyleKey.setParagraphStyle(NSParagraphStyle.default)
paraStyleKey.alignment = .natural
let paraStyle = NSMutableParagraphStyle()
paraStyle.setParagraphStyle(NSParagraphStyle.default)
paraStyle.alignment = .natural
paraStyle.lineBreakMode = .byWordWrapping
let attrCandidate: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: size, weight: .regular),
.paragraphStyle: paraStyle,
]
let attrStrCandidate = NSMutableAttributedString(string: displayedText + " ", attributes: attrCandidate)
return attrStrCandidate
}
public var attributedString: NSAttributedString {
let paraStyleKey = NSMutableParagraphStyle()
paraStyleKey.setParagraphStyle(NSParagraphStyle.default)
paraStyleKey.alignment = .natural
let paraStyle = NSMutableParagraphStyle()
paraStyle.setParagraphStyle(NSParagraphStyle.default)
paraStyle.alignment = .natural
paraStyle.lineBreakMode = .byWordWrapping
var attrCandidate: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: size, weight: .regular),
.paragraphStyle: paraStyle,
]
if isSelected {
attrCandidate[.foregroundColor] = NSColor.white
} else {
attrCandidate[.foregroundColor] = NSColor.labelColor
}
if #available(macOS 12, *) {
if UserDefaults.standard.bool(forKey: UserDef.kHandleDefaultCandidateFontsByLangIdentifier.rawValue) {
attrCandidate[.languageIdentifier] = self.locale as AnyObject
}
}
let attrStrCandidate = NSMutableAttributedString(string: displayedText, attributes: attrCandidate)
return attrStrCandidate
}
public var charDescriptions: String {
var result = displayedText
if displayedText.contains("("), displayedText.count > 2 {
result = displayedText.replacingOccurrences(of: "(", with: "").replacingOccurrences(of: ")", with: "")
}
return result.charDescriptions.joined(separator: "\n")
}
public var minWidthToDrawInSwiftUI: Double {
Double(cellLength) + ((displayedText.count > 2) ? 0 : fontSizeKey + 0) + ceil(fontSizeCandidate * 0.4)
}
}
// MARK: - Contents specifically made for macOS 12 and newer.
@available(macOS 12, *)
public extension CandidateCellData {
var attributedStringForSwiftUI: some View {
var result: some View {
ZStack(alignment: .leading) {
if isSelected {
Color(nsColor: CandidateCellData.highlightBackground).ignoresSafeArea().cornerRadius(6)
}
VStack(spacing: 0) {
HStack(spacing: 4) {
if UserDefaults.standard.bool(forKey: UserDef.kHandleDefaultCandidateFontsByLangIdentifier.rawValue) {
Text(AttributedString(attributedStringHeader))
Text(AttributedString(attributedString))
} else {
Text(verbatim: key).font(.system(size: fontSizeKey).monospaced())
.foregroundColor(.init(nsColor: fontColorKey)).lineLimit(1)
Text(verbatim: displayedText)
.font(.init(CTFontCreateUIFontForLanguage(.system, fontSizeCandidate, locale as CFString)!))
.foregroundColor(.init(nsColor: fontColorCandidate)).lineLimit(1)
}
}.padding(3)
}.frame(minWidth: minWidthToDrawInSwiftUI, alignment: .leading)
}.fixedSize(horizontal: false, vertical: true)
}
return result
}
}
// MARK: - Contents specifically made for macOS 10.15 and macOS 11.
@available(macOS 10.15, *)
public extension CandidateCellData {
var themeColorBackports: some View {
//
let result: Color = {
switch locale {
case "zh-Hans": return Color.red
case "zh-Hant": return Color.blue
case "ja": return Color.pink
default: return Color.accentColor
}
}()
return result.opacity(0.85)
}
var attributedStringForSwiftUIBackports: some View {
var result: some View {
ZStack(alignment: .leading) {
if isSelected {
themeColorBackports.cornerRadius(6)
VStack(spacing: 0) {
HStack(spacing: 4) {
Text(verbatim: key).font(.custom("Menlo", size: fontSizeKey))
.foregroundColor(Color.white.opacity(0.8)).lineLimit(1)
Text(verbatim: displayedText)
.font(.init(CTFontCreateUIFontForLanguage(.system, fontSizeCandidate, locale as CFString)!))
.foregroundColor(Color(white: 1)).lineLimit(1)
}.padding(3).foregroundColor(Color(white: 0.9))
}.frame(minWidth: minWidthToDrawInSwiftUI, alignment: .leading)
} else {
VStack(spacing: 0) {
HStack(spacing: 4) {
Text(verbatim: key).font(.custom("Menlo", size: fontSizeKey))
.foregroundColor(Color.secondary).lineLimit(1)
Text(verbatim: displayedText)
.font(.init(CTFontCreateUIFontForLanguage(.system, fontSizeCandidate, locale as CFString)!))
.foregroundColor(Color.primary).lineLimit(1)
}.padding(3).foregroundColor(Color(white: 0.9))
}.frame(minWidth: minWidthToDrawInSwiftUI, alignment: .leading)
}
}.fixedSize(horizontal: false, vertical: true)
}
return result
}
}

View File

@ -0,0 +1,191 @@
// (c) 2022 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 Cocoa
import Shared
import SwiftUI
import SwiftUIBackports
// MARK: - Candidate Cell
/// class 便
public class CandidateCellData: Hashable {
public var locale = ""
public static var unifiedSize: Double = 16
public var key: String
public var displayedText: String
public var size: Double { Self.unifiedSize }
public var isHighlighted: Bool = false
public var whichLine: Int = 0
//
public var index: Int = 0
// /
public var subIndex: Int = 0
public var charGlyphWidth: Double { ceil(size * 1.0125 + 7) }
public var fontSizeCandidate: Double { size }
public var fontSizeKey: Double { max(ceil(fontSizeCandidate * 0.6), 11) }
public var fontColorKey: NSColor {
isHighlighted ? .selectedMenuItemTextColor.withAlphaComponent(0.8) : .secondaryLabelColor
}
public var fontColorCandidate: NSColor { isHighlighted ? .selectedMenuItemTextColor : .labelColor }
public init(key: String, displayedText: String, isSelected: Bool = false) {
self.key = key
self.displayedText = displayedText
isHighlighted = isSelected
}
public static func == (lhs: CandidateCellData, rhs: CandidateCellData) -> Bool {
lhs.key == rhs.key && lhs.displayedText == rhs.displayedText
}
public func hash(into hasher: inout Hasher) {
hasher.combine(key)
hasher.combine(displayedText)
}
public func cellLength(isMatrix: Bool = true) -> Double {
let minLength = ceil(charGlyphWidth * 2 + size)
if displayedText.count <= 2, isMatrix { return minLength }
return ceil(attributedStringForLengthCalculation.boundingDimension.width)
}
public static let sharedParagraphStyle: NSParagraphStyle = {
let paraStyle = NSMutableParagraphStyle()
paraStyle.setParagraphStyle(NSParagraphStyle.default)
paraStyle.alignment = .natural
paraStyle.lineBreakMode = .byWordWrapping
return paraStyle
}()
var phraseFont: NSFont {
CTFontCreateUIFontForLanguage(.system, size, locale as CFString) ?? NSFont.systemFont(ofSize: size)
}
var highlightedNSColor: NSColor {
var result = NSColor.alternateSelectedControlColor
var colorBlendAmount: Double = NSApplication.isDarkMode ? 0.3 : 0.0
if #available(macOS 10.14, *), !NSApplication.isDarkMode, locale == "zh-Hant" {
colorBlendAmount = 0.15
}
//
switch locale {
case "zh-Hans":
result = NSColor.systemRed
case "zh-Hant":
result = NSColor.systemBlue
case "ja":
result = NSColor.systemBrown
default: break
}
var blendingAgainstTarget: NSColor = NSApplication.isDarkMode ? NSColor.black : NSColor.white
if #unavailable(macOS 10.14) {
colorBlendAmount = 0.3
blendingAgainstTarget = NSColor.white
}
return result.blended(withFraction: colorBlendAmount, of: blendingAgainstTarget)!
}
public var attributedStringForLengthCalculation: NSAttributedString {
let attrCandidate: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: size, weight: .regular),
.paragraphStyle: Self.sharedParagraphStyle,
]
let attrStrCandidate = NSAttributedString(string: displayedText + " ", attributes: attrCandidate)
return attrStrCandidate
}
public func attributedString(
noSpacePadding: Bool = true, withHighlight: Bool = false, isMatrix: Bool = false
) -> NSAttributedString {
let attrCandidate: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: size, weight: .regular),
.paragraphStyle: Self.sharedParagraphStyle,
]
let result: NSMutableAttributedString = {
if noSpacePadding {
let resultNeo = NSMutableAttributedString(string: " ", attributes: attrCandidate)
resultNeo.insert(attributedStringPhrase(isMatrix: isMatrix), at: 1)
resultNeo.insert(attributedStringHeader, at: 0)
return resultNeo
}
let resultNeo = NSMutableAttributedString(string: " ", attributes: attrCandidate)
resultNeo.insert(attributedStringPhrase(isMatrix: isMatrix), at: 2)
resultNeo.insert(attributedStringHeader, at: 1)
return resultNeo
}()
if withHighlight, isHighlighted {
result.addAttribute(
.backgroundColor, value: highlightedNSColor,
range: NSRange(location: 0, length: result.string.utf16.count)
)
}
return result
}
public var attributedStringHeader: NSAttributedString {
let theFontForCandidateKey: NSFont = {
if #available(macOS 10.15, *) {
return NSFont.monospacedSystemFont(ofSize: fontSizeKey, weight: .regular)
}
return NSFont.monospacedDigitSystemFont(ofSize: fontSizeKey, weight: .regular)
}()
var attrKey: [NSAttributedString.Key: AnyObject] = [
.font: theFontForCandidateKey,
.paragraphStyle: Self.sharedParagraphStyle,
]
if isHighlighted {
attrKey[.foregroundColor] = NSColor.white.withAlphaComponent(0.8)
} else {
attrKey[.foregroundColor] = NSColor.secondaryLabelColor
}
let attrStrKey = NSAttributedString(string: key, attributes: attrKey)
return attrStrKey
}
public func attributedStringPhrase(isMatrix: Bool = false) -> NSAttributedString {
var attrCandidate: [NSAttributedString.Key: AnyObject] = [
.font: phraseFont,
.paragraphStyle: Self.sharedParagraphStyle,
]
if isHighlighted {
attrCandidate[.foregroundColor] = NSColor.white
} else {
attrCandidate[.foregroundColor] = NSColor.labelColor
}
if #available(macOS 12, *) {
if UserDefaults.standard.bool(
forKey: UserDef.kLegacyCandidateViewTypesettingMethodEnabled.rawValue
) {
attrCandidate[.languageIdentifier] = self.locale as AnyObject
}
}
let delta: String = (isMatrix && displayedText.count < 2) ? "  " : ""
let attrStrCandidate = NSAttributedString(
string: displayedText + delta, attributes: attrCandidate
)
return attrStrCandidate
}
public var charDescriptions: [String] {
var result = displayedText
if displayedText.contains("("), displayedText.count > 2 {
result = displayedText.replacingOccurrences(of: "(", with: "").replacingOccurrences(of: ")", with: "")
}
return result.flatMap(\.unicodeScalars).compactMap {
let theName: String = $0.properties.name ?? ""
return String(format: "U+%02X %@", $0.value, theName)
}
}
public func minWidthToDraw(isMatrix: Bool = true) -> Double {
cellLength(isMatrix: isMatrix) + ceil(fontSizeKey * 0.1)
}
}

View File

@ -0,0 +1,85 @@
// (c) 2022 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 Foundation
import Shared
import SwiftUI
// MARK: - Contents specifically made for SwiftUI.
@available(macOS 10.15, *)
public extension CandidateCellData {
var themeColor: Color {
//
let result: Color = {
switch locale {
case "zh-Hans": return Color.red
case "zh-Hant": return Color.blue
case "ja": return Color(red: 0.64, green: 0.52, blue: 0.37)
default: return Color.accentColor
}
}()
return result.opacity(0.85)
}
var attributedStringForSwiftUIBackports: some View {
var result: some View {
ZStack(alignment: .leading) {
if isHighlighted {
themeColor.cornerRadius(6)
VStack(spacing: 0) {
HStack(spacing: 4) {
Text(verbatim: key).font(.custom("Menlo", size: fontSizeKey))
.foregroundColor(Color.white.opacity(0.8)).lineLimit(1)
Text(verbatim: displayedText)
.font(.init(CTFontCreateUIFontForLanguage(.system, fontSizeCandidate, locale as CFString)!))
.foregroundColor(Color(white: 1)).lineLimit(1)
}.padding(3).foregroundColor(Color(white: 0.9))
}.frame(alignment: .leading)
} else {
VStack(spacing: 0) {
HStack(spacing: 4) {
Text(verbatim: key).font(.custom("Menlo", size: fontSizeKey))
.foregroundColor(Color.secondary).lineLimit(1)
Text(verbatim: displayedText)
.font(.init(CTFontCreateUIFontForLanguage(.system, fontSizeCandidate, locale as CFString)!))
.foregroundColor(Color.primary).lineLimit(1)
}.padding(3).foregroundColor(Color(white: 0.9))
}.frame(alignment: .leading)
}
}.fixedSize(horizontal: false, vertical: true)
}
return result
}
@available(macOS 12, *)
var attributedStringForSwiftUI: some View {
var result: some View {
ZStack(alignment: .leading) {
if isHighlighted {
themeColor.ignoresSafeArea().cornerRadius(6)
}
VStack(spacing: 0) {
HStack(spacing: 4) {
if UserDefaults.standard.bool(forKey: UserDef.kLegacyCandidateViewTypesettingMethodEnabled.rawValue) {
Text(AttributedString(attributedStringHeader))
Text(AttributedString(attributedStringPhrase()))
} else {
Text(verbatim: key).font(.system(size: fontSizeKey).monospaced())
.foregroundColor(.init(nsColor: fontColorKey)).lineLimit(1)
Text(verbatim: displayedText)
.font(.init(CTFontCreateUIFontForLanguage(.system, fontSizeCandidate, locale as CFString)!))
.foregroundColor(.init(nsColor: fontColorCandidate)).lineLimit(1)
}
}.padding(3)
}.frame(alignment: .leading)
}.fixedSize(horizontal: false, vertical: true)
}
return result
}
}

View File

@ -6,343 +6,200 @@
// marks, or product names of Contributor, except as required to fulfill notice // marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License. // requirements defined in MIT License.
import Cocoa import Foundation
import Shared import Shared
/// ///
public struct CandidatePool { public struct CandidatePool {
public let blankCell = CandidateCellData(key: " ", displayedText: " ", isSelected: false) public let blankCell: CandidateCellData
public var currentLayout: NSUserInterfaceLayoutOrientation = .horizontal public let maxLinesPerPage: Int
public private(set) var candidateDataAll: [CandidateCellData] = [] public let layout: LayoutOrientation
public private(set) var selectionKeys: String public let selectionKeys: String
public let candidateDataAll: [CandidateCellData]
public var candidateLines: [[CandidateCellData]] = []
public var tooltip: String = ""
public var reverseLookupResult: [String] = []
public private(set) var highlightedIndex: Int = 0 public private(set) var highlightedIndex: Int = 0
public private(set) var currentLineNumber = 0
// private var recordedLineRangeForCurrentPage: Range<Int>?
private var currentRowNumber = 0 private var previouslyRecordedLineRangeForPreviousPage: Range<Int>?
private var maxRowsPerPage = 3
private var maxRowCapacity: Int = 6
private var candidateRows: [[CandidateCellData]] = []
//
private var currentColumnNumber = 0
private var maxColumnsPerPage = 3
private var maxColumnCapacity: Int = 6
private var candidateColumns: [[CandidateCellData]] = []
// MARK: - // MARK: -
public var maxRowWidth: Int { Int(ceil((Double(maxRowCapacity + 3) * 2 - 0.5) * CandidateCellData.unifiedSize)) } ///
public var maxWindowWidth: Double { ///
Double(maxRowCapacity) * (blankCell.minWidthToDrawInSwiftUI + ceil(CandidateCellData.unifiedSize * 0.5)) public var maxRowWidth: Double { ceil(Double(maxLineCapacity) * blankCell.minWidthToDraw()) }
///
public var currentPositionLabelText: String {
(highlightedIndex + 1).description + "/" + candidateDataAll.count.description
} }
public var currentLineNumber: Int { ///
switch currentLayout { public var currentCandidate: CandidateCellData? {
case .horizontal: (0 ..< candidateDataAll.count).contains(highlightedIndex) ? candidateDataAll[highlightedIndex] : nil
return currentRowNumber
case .vertical:
return currentColumnNumber
@unknown default:
return 0
}
} }
public var candidateLines: [[CandidateCellData]] { ///
switch currentLayout { public var currentSelectedCandidateText: String? { currentCandidate?.displayedText ?? nil }
case .horizontal:
return candidateRows /// /
case .vertical: public var maxLineCapacity: Int { selectionKeys.count }
return candidateColumns
@unknown default: ///
return [] public var dummyCellsRequiredForCurrentLine: Int {
} maxLineCapacity - candidateLines[currentLineNumber].count
} }
public var maxLineCapacity: Int { ///
switch currentLayout { public var lineRangeForFinalPageBlanked: Range<Int> {
case .horizontal: 0 ..< (maxLinesPerPage - lineRangeForCurrentPage.count)
return maxRowCapacity
case .vertical:
return maxColumnCapacity
@unknown default:
return 0
}
} }
public var maxLinesPerPage: Int { ///
get { public var lineRangeForCurrentPage: Range<Int> {
switch currentLayout { recordedLineRangeForCurrentPage ?? fallbackedLineRangeForCurrentPage
case .horizontal:
return maxRowsPerPage
case .vertical:
return maxColumnsPerPage
@unknown default:
return 0
}
}
set {
switch currentLayout {
case .horizontal:
maxRowsPerPage = newValue
case .vertical:
maxColumnsPerPage = newValue
@unknown default:
return
}
}
} }
public var rangeForLastPageBlanked: Range<Int> { /// 退
switch currentLayout { public var fallbackedLineRangeForCurrentPage: Range<Int> {
case .horizontal: return rangeForLastHorizontalPageBlanked currentLineNumber ..< min(candidateLines.count, currentLineNumber + maxLinesPerPage)
case .vertical: return rangeForLastVerticalPageBlanked
@unknown default: return 0 ..< 0
}
}
public var rangeForCurrentPage: Range<Int> {
switch currentLayout {
case .horizontal: return rangeForCurrentHorizontalPage
case .vertical: return rangeForCurrentVerticalPage
@unknown default: return 0 ..< 0
}
} }
// MARK: - Constructors // MARK: - Constructors
/// ///
/// - Parameters: /// - Parameters:
/// - candidates: /// - candidates:
/// - columnCapacity: (, )
/// - selectionKeys: /// - selectionKeys:
/// - direction:
/// - locale: zh-Hanszh-Hant /// - locale: zh-Hanszh-Hant
public init( public init(
candidates: [String], columnCapacity: Int, columns: Int = 3, selectionKeys: String = "123456789", candidates: [String], lines: Int = 3, selectionKeys: String = "123456789",
locale: String = "" layout: LayoutOrientation = .vertical, locale: String = ""
) { ) {
maxColumnsPerPage = max(1, columns) self.layout = layout
maxColumnCapacity = max(1, columnCapacity) maxLinesPerPage = max(1, lines)
self.selectionKeys = selectionKeys blankCell = CandidateCellData(key: " ", displayedText: " ", isSelected: false)
candidateDataAll = candidates.map { .init(key: "0", displayedText: $0) } blankCell.locale = locale
self.selectionKeys = selectionKeys.isEmpty ? "123456789" : selectionKeys
var allCandidates = candidates.map { CandidateCellData(key: " ", displayedText: $0) }
if allCandidates.isEmpty { allCandidates.append(blankCell) }
candidateDataAll = allCandidates
var currentColumn: [CandidateCellData] = [] var currentColumn: [CandidateCellData] = []
for (i, candidate) in candidateDataAll.enumerated() { for (i, candidate) in candidateDataAll.enumerated() {
candidate.index = i candidate.index = i
candidate.whichColumn = candidateColumns.count candidate.whichLine = candidateLines.count
if currentColumn.count == maxColumnCapacity, !currentColumn.isEmpty { var isOverflown: Bool = (currentColumn.count == maxLineCapacity) && !currentColumn.isEmpty
candidateColumns.append(currentColumn) if layout == .horizontal {
isOverflown = isOverflown
|| currentColumn.map { $0.cellLength() }.reduce(0, +) >= maxRowWidth - candidate.cellLength()
}
if isOverflown {
candidateLines.append(currentColumn)
currentColumn.removeAll() currentColumn.removeAll()
candidate.whichColumn += 1 candidate.whichLine += 1
} }
candidate.subIndex = currentColumn.count candidate.subIndex = currentColumn.count
candidate.locale = locale candidate.locale = locale
currentColumn.append(candidate) currentColumn.append(candidate)
} }
candidateColumns.append(currentColumn) candidateLines.append(currentColumn)
currentLayout = .vertical recordedLineRangeForCurrentPage = fallbackedLineRangeForCurrentPage
highlight(at: 0)
}
} }
/// // MARK: - Public Functions (for all OS)
public extension CandidatePool {
///
enum LayoutOrientation {
case horizontal
case vertical
}
///
/// - Parameter isBackward:
/// - Returns:
@discardableResult mutating func flipPage(isBackward: Bool) -> Bool {
backupLineRangeForCurrentPage()
defer { flipLineRangeToNeighborPage(isBackward: isBackward) }
return consecutivelyFlipLines(isBackward: isBackward, count: maxLinesPerPage)
}
///
/// - Parameter subIndex:
/// - Returns: nil
func calculateCandidateIndex(subIndex: Int) -> Int? {
let arrCurrentLine = candidateLines[currentLineNumber]
if !(0 ..< arrCurrentLine.count).contains(subIndex) { return nil }
return arrCurrentLine[subIndex].index
}
///
/// - Parameters: /// - Parameters:
/// - candidates: /// - isBackward:
/// - rowCapacity: (, ) /// - count:
/// - selectionKeys: /// - Returns:
/// - locale: zh-Hanszh-Hant @discardableResult mutating func consecutivelyFlipLines(isBackward: Bool, count: Int) -> Bool {
public init( switch isBackward {
candidates: [String], rowCapacity: Int, rows: Int = 3, selectionKeys: String = "123456789", locale: String = "" case false where currentLineNumber == candidateLines.count - 1:
) { return highlightNeighborCandidate(isBackward: false)
maxRowsPerPage = max(1, rows) case true where currentLineNumber == 0:
maxRowCapacity = max(1, rowCapacity) return highlightNeighborCandidate(isBackward: true)
self.selectionKeys = selectionKeys default:
candidateDataAll = candidates.map { .init(key: "0", displayedText: $0) } if count <= 0 { return false }
var currentRow: [CandidateCellData] = [] for _ in 0 ..< min(maxLinesPerPage, count) {
for (i, candidate) in candidateDataAll.enumerated() { selectNewNeighborLine(isBackward: isBackward)
candidate.index = i
candidate.whichRow = candidateRows.count
let isOverflown: Bool = currentRow.map(\.cellLength).reduce(0, +) > maxRowWidth
if isOverflown || currentRow.count == maxRowCapacity, !currentRow.isEmpty {
candidateRows.append(currentRow)
currentRow.removeAll()
candidate.whichRow += 1
} }
candidate.subIndex = currentRow.count return true
candidate.locale = locale
currentRow.append(candidate)
}
candidateRows.append(currentRow)
currentLayout = .horizontal
}
// MARK: Public Functions
public mutating func selectNewNeighborLine(isForward: Bool) {
switch currentLayout {
case .horizontal: selectNewNeighborRow(direction: isForward ? .down : .up)
case .vertical: selectNewNeighborColumn(direction: isForward ? .right : .left)
@unknown default: break
} }
} }
public mutating func highlight(at indexSpecified: Int) { ///
switch currentLayout { /// - Parameter isBackward:
case .horizontal: highlightHorizontal(at: indexSpecified) /// - Returns:
case .vertical: highlightVertical(at: indexSpecified) @discardableResult mutating func highlightNeighborCandidate(isBackward: Bool) -> Bool {
@unknown default: break switch isBackward {
} case false where highlightedIndex >= candidateDataAll.count - 1:
vCLog("\n" + candidateDataAll[highlightedIndex].charDescriptions) highlight(at: 0)
return false
case true where highlightedIndex <= 0:
highlight(at: candidateDataAll.count - 1)
return false
default:
highlight(at: highlightedIndex + (isBackward ? -1 : 1))
return true
} }
} }
// MARK: - Private Functions ///
/// - Parameter indexSpecified:
extension CandidatePool { mutating func highlight(at indexSpecified: Int) {
private enum VerticalDirection {
case up
case down
}
private enum HorizontalDirection {
case left
case right
}
private var rangeForLastHorizontalPageBlanked: Range<Int> {
0 ..< (maxRowsPerPage - rangeForCurrentHorizontalPage.count)
}
private var rangeForLastVerticalPageBlanked: Range<Int> {
0 ..< (maxColumnsPerPage - rangeForCurrentVerticalPage.count)
}
private var rangeForCurrentHorizontalPage: Range<Int> {
currentRowNumber ..< min(candidateRows.count, currentRowNumber + maxRowsPerPage)
}
private var rangeForCurrentVerticalPage: Range<Int> {
currentColumnNumber ..< min(candidateColumns.count, currentColumnNumber + maxColumnsPerPage)
}
private mutating func selectNewNeighborRow(direction: VerticalDirection) {
let currentSubIndex = candidateDataAll[highlightedIndex].subIndex
var result = currentSubIndex
switch direction {
case .up:
if currentRowNumber <= 0 {
if candidateRows.isEmpty { break }
let firstRow = candidateRows[0]
let newSubIndex = min(currentSubIndex, firstRow.count - 1)
highlightHorizontal(at: firstRow[newSubIndex].index)
break
}
if currentRowNumber >= candidateRows.count - 1 { currentRowNumber = candidateRows.count - 1 }
if candidateRows[currentRowNumber].count != candidateRows[currentRowNumber - 1].count {
let ratio: Double = min(1, Double(currentSubIndex) / Double(candidateRows[currentRowNumber].count))
result = Int(floor(Double(candidateRows[currentRowNumber - 1].count) * ratio))
}
let targetRow = candidateRows[currentRowNumber - 1]
let newSubIndex = min(result, targetRow.count - 1)
highlightHorizontal(at: targetRow[newSubIndex].index)
case .down:
if currentRowNumber >= candidateRows.count - 1 {
if candidateRows.isEmpty { break }
let finalRow = candidateRows[candidateRows.count - 1]
let newSubIndex = min(currentSubIndex, finalRow.count - 1)
highlightHorizontal(at: finalRow[newSubIndex].index)
break
}
if candidateRows[currentRowNumber].count != candidateRows[currentRowNumber + 1].count {
let ratio: Double = min(1, Double(currentSubIndex) / Double(candidateRows[currentRowNumber].count))
result = Int(floor(Double(candidateRows[currentRowNumber + 1].count) * ratio))
}
let targetRow = candidateRows[currentRowNumber + 1]
let newSubIndex = min(result, targetRow.count - 1)
highlightHorizontal(at: targetRow[newSubIndex].index)
}
}
private mutating func selectNewNeighborColumn(direction: HorizontalDirection) {
let currentSubIndex = candidateDataAll[highlightedIndex].subIndex
switch direction {
case .left:
if currentColumnNumber <= 0 {
if candidateColumns.isEmpty { break }
let firstColumn = candidateColumns[0]
let newSubIndex = min(currentSubIndex, firstColumn.count - 1)
highlightVertical(at: firstColumn[newSubIndex].index)
break
}
if currentColumnNumber >= candidateColumns.count - 1 { currentColumnNumber = candidateColumns.count - 1 }
let targetColumn = candidateColumns[currentColumnNumber - 1]
let newSubIndex = min(currentSubIndex, targetColumn.count - 1)
highlightVertical(at: targetColumn[newSubIndex].index)
case .right:
if currentColumnNumber >= candidateColumns.count - 1 {
if candidateColumns.isEmpty { break }
let finalColumn = candidateColumns[candidateColumns.count - 1]
let newSubIndex = min(currentSubIndex, finalColumn.count - 1)
highlightVertical(at: finalColumn[newSubIndex].index)
break
}
let targetColumn = candidateColumns[currentColumnNumber + 1]
let newSubIndex = min(currentSubIndex, targetColumn.count - 1)
highlightVertical(at: targetColumn[newSubIndex].index)
}
}
private mutating func highlightHorizontal(at indexSpecified: Int) {
var indexSpecified = indexSpecified var indexSpecified = indexSpecified
let isBackward: Bool = indexSpecified > highlightedIndex
highlightedIndex = indexSpecified highlightedIndex = indexSpecified
if !(0 ..< candidateDataAll.count).contains(highlightedIndex) { if !(0 ..< candidateDataAll.count).contains(highlightedIndex) {
switch highlightedIndex { switch highlightedIndex {
case candidateDataAll.count...: case candidateDataAll.count...:
currentRowNumber = candidateRows.count - 1 currentLineNumber = candidateLines.count - 1
highlightedIndex = max(0, candidateDataAll.count - 1) highlightedIndex = max(0, candidateDataAll.count - 1)
indexSpecified = highlightedIndex indexSpecified = highlightedIndex
case ..<0: case ..<0:
highlightedIndex = 0 highlightedIndex = 0
currentRowNumber = 0 currentLineNumber = 0
indexSpecified = highlightedIndex indexSpecified = highlightedIndex
default: break default: break
} }
} }
for (i, candidate) in candidateDataAll.enumerated() { for (i, candidate) in candidateDataAll.enumerated() {
candidate.isSelected = (indexSpecified == i) candidate.isHighlighted = (indexSpecified == i)
if candidate.isSelected { currentRowNumber = candidate.whichRow } if candidate.isHighlighted { currentLineNumber = candidate.whichLine }
} }
for (i, candidateRow) in candidateRows.enumerated() { for (i, candidateColumn) in candidateLines.enumerated() {
if i != currentRowNumber { if i != currentLineNumber {
candidateRow.forEach {
$0.key = " "
}
} else {
for (i, neta) in candidateRow.enumerated() {
neta.key = selectionKeys.map(\.description)[i]
}
}
}
}
private mutating func highlightVertical(at indexSpecified: Int) {
var indexSpecified = indexSpecified
highlightedIndex = indexSpecified
if !(0 ..< candidateDataAll.count).contains(highlightedIndex) {
switch highlightedIndex {
case candidateDataAll.count...:
currentColumnNumber = candidateColumns.count - 1
highlightedIndex = max(0, candidateDataAll.count - 1)
indexSpecified = highlightedIndex
case ..<0:
highlightedIndex = 0
currentColumnNumber = 0
indexSpecified = highlightedIndex
default: break
}
}
for (i, candidate) in candidateDataAll.enumerated() {
candidate.isSelected = (indexSpecified == i)
if candidate.isSelected { currentColumnNumber = candidate.whichColumn }
}
for (i, candidateColumn) in candidateColumns.enumerated() {
if i != currentColumnNumber {
candidateColumn.forEach { candidateColumn.forEach {
$0.key = " " $0.key = " "
} }
@ -353,5 +210,167 @@ extension CandidatePool {
} }
} }
} }
if highlightedIndex != 0, indexSpecified == 0 {
recordedLineRangeForCurrentPage = fallbackedLineRangeForCurrentPage
} else {
fixLineRange(isBackward: isBackward)
}
}
}
// MARK: - Private Functions
private extension CandidatePool {
enum VerticalDirection {
case up
case down
}
enum HorizontalDirection {
case left
case right
}
///
var lineRangeForFirstPage: Range<Int> {
0 ..< min(maxLinesPerPage, candidateLines.count)
}
///
var lineRangeForFinalPage: Range<Int> {
max(0, candidateLines.count - maxLinesPerPage) ..< candidateLines.count
}
mutating func selectNewNeighborLine(isBackward: Bool) {
switch layout {
case .horizontal: selectNewNeighborRow(direction: isBackward ? .up : .down)
case .vertical: selectNewNeighborColumn(direction: isBackward ? .left : .right)
}
}
mutating func fixLineRange(isBackward: Bool = false) {
if !lineRangeForCurrentPage.contains(currentLineNumber) {
switch isBackward {
case false:
let theMin = currentLineNumber
let theMax = min(theMin + maxLinesPerPage, candidateLines.count)
recordedLineRangeForCurrentPage = theMin ..< theMax
case true:
let theMax = currentLineNumber + 1
let theMin = max(0, theMax - maxLinesPerPage)
recordedLineRangeForCurrentPage = theMin ..< theMax
}
}
}
mutating func backupLineRangeForCurrentPage() {
previouslyRecordedLineRangeForPreviousPage = lineRangeForCurrentPage
}
mutating func flipLineRangeToNeighborPage(isBackward: Bool = false) {
guard let prevRange = previouslyRecordedLineRangeForPreviousPage else { return }
var lowerBound = prevRange.lowerBound
var upperBound = prevRange.upperBound
//
lowerBound += maxLinesPerPage * (isBackward ? -1 : 1)
upperBound += maxLinesPerPage * (isBackward ? -1 : 1)
//
branch1: switch isBackward {
case false:
if upperBound < candidateLines.count { break branch1 }
if lowerBound < lineRangeForFinalPage.lowerBound { break branch1 }
let isOverFlipped = !lineRangeForFinalPage.contains(currentLineNumber)
recordedLineRangeForCurrentPage = isOverFlipped ? lineRangeForFirstPage : lineRangeForFinalPage
return
case true:
if lowerBound > 0 { break branch1 }
if upperBound > lineRangeForFirstPage.upperBound { break branch1 }
let isOverFlipped = !lineRangeForFirstPage.contains(currentLineNumber)
recordedLineRangeForCurrentPage = isOverFlipped ? lineRangeForFinalPage : lineRangeForFirstPage
return
}
let result = lowerBound ..< upperBound
if result.contains(currentLineNumber) {
recordedLineRangeForCurrentPage = result
return
}
//
}
mutating func selectNewNeighborRow(direction: VerticalDirection) {
let currentSubIndex = candidateDataAll[highlightedIndex].subIndex
var result = currentSubIndex
branch: switch direction {
case .up:
if currentLineNumber <= 0 {
if candidateLines.isEmpty { break }
let firstRow = candidateLines[0]
let newSubIndex = min(currentSubIndex, firstRow.count - 1)
highlight(at: firstRow[newSubIndex].index)
fixLineRange(isBackward: false)
break branch
}
if currentLineNumber >= candidateLines.count - 1 { currentLineNumber = candidateLines.count - 1 }
result = currentSubIndex
//
// if candidateLines[currentLineNumber].count != candidateLines[currentLineNumber - 1].count {
// let ratio: Double = min(1, Double(currentSubIndex) / Double(candidateLines[currentLineNumber].count))
// result = max(Int(floor(Double(candidateLines[currentLineNumber - 1].count) * ratio)), result)
// }
let targetRow = candidateLines[currentLineNumber - 1]
let newSubIndex = min(result, targetRow.count - 1)
highlight(at: targetRow[newSubIndex].index)
fixLineRange(isBackward: true)
case .down:
if currentLineNumber >= candidateLines.count - 1 {
if candidateLines.isEmpty { break }
let finalRow = candidateLines[candidateLines.count - 1]
let newSubIndex = min(currentSubIndex, finalRow.count - 1)
highlight(at: finalRow[newSubIndex].index)
fixLineRange(isBackward: true)
break branch
}
result = currentSubIndex
//
if candidateLines[currentLineNumber].count != candidateLines[currentLineNumber + 1].count {
let ratio: Double = min(1, Double(currentSubIndex) / Double(candidateLines[currentLineNumber].count))
result = max(Int(floor(Double(candidateLines[currentLineNumber + 1].count) * ratio)), result)
}
let targetRow = candidateLines[currentLineNumber + 1]
let newSubIndex = min(result, targetRow.count - 1)
highlight(at: targetRow[newSubIndex].index)
fixLineRange(isBackward: false)
}
}
mutating func selectNewNeighborColumn(direction: HorizontalDirection) {
let currentSubIndex = candidateDataAll[highlightedIndex].subIndex
switch direction {
case .left:
if currentLineNumber <= 0 {
if candidateLines.isEmpty { break }
let firstColumn = candidateLines[0]
let newSubIndex = min(currentSubIndex, firstColumn.count - 1)
highlight(at: firstColumn[newSubIndex].index)
break
}
if currentLineNumber >= candidateLines.count - 1 { currentLineNumber = candidateLines.count - 1 }
let targetColumn = candidateLines[currentLineNumber - 1]
let newSubIndex = min(currentSubIndex, targetColumn.count - 1)
highlight(at: targetColumn[newSubIndex].index)
fixLineRange(isBackward: true)
case .right:
if currentLineNumber >= candidateLines.count - 1 {
if candidateLines.isEmpty { break }
let finalColumn = candidateLines[candidateLines.count - 1]
let newSubIndex = min(currentSubIndex, finalColumn.count - 1)
highlight(at: finalColumn[newSubIndex].index)
break
}
let targetColumn = candidateLines[currentLineNumber + 1]
let newSubIndex = min(currentSubIndex, targetColumn.count - 1)
highlight(at: targetColumn[newSubIndex].index)
fixLineRange(isBackward: false)
}
} }
} }

View File

@ -0,0 +1,171 @@
// (c) 2022 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 Cocoa
extension CandidatePool {
public var attributedDescription: NSAttributedString {
switch layout {
case .horizontal: return attributedDescriptionHorizontal
case .vertical: return attributedDescriptionVertical
}
}
/// NSAttributedString
private var attributedDescriptionHorizontal: NSAttributedString {
let paragraphStyle = CandidateCellData.sharedParagraphStyle as! NSMutableParagraphStyle
paragraphStyle.lineSpacing = ceil(blankCell.size * 0.3)
paragraphStyle.lineBreakStrategy = .pushOut
let attrCandidate: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: blankCell.size, weight: .regular),
.paragraphStyle: paragraphStyle,
]
let result = NSMutableAttributedString(string: "", attributes: attrCandidate)
let spacer = NSAttributedString(string: " ", attributes: attrCandidate)
let lineFeed = NSAttributedString(string: "\n", attributes: attrCandidate)
for lineID in lineRangeForCurrentPage {
let arrLine = candidateLines[lineID]
arrLine.enumerated().forEach { cellID, currentCell in
let cellString = NSMutableAttributedString(
attributedString: currentCell.attributedString(
noSpacePadding: false, withHighlight: true, isMatrix: maxLinesPerPage > 1
)
)
if lineID != currentLineNumber {
cellString.addAttribute(
.foregroundColor, value: NSColor.controlTextColor,
range: .init(location: 0, length: cellString.string.utf16.count)
)
}
result.append(cellString)
if cellID < arrLine.count - 1 {
result.append(spacer)
}
}
if lineID < lineRangeForCurrentPage.upperBound - 1 || maxLinesPerPage > 1 {
result.append(lineFeed)
} else {
result.append(spacer)
}
}
//
result.append(attributedDescriptionBottomPanes)
return result
}
private var attributedDescriptionVertical: NSAttributedString {
let paragraphStyle = CandidateCellData.sharedParagraphStyle as! NSMutableParagraphStyle
paragraphStyle.lineSpacing = ceil(blankCell.size * 0.3)
paragraphStyle.lineBreakStrategy = .pushOut
let attrCandidate: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: blankCell.size, weight: .regular),
.paragraphStyle: paragraphStyle,
]
let result = NSMutableAttributedString(string: "", attributes: attrCandidate)
let spacer = NSMutableAttributedString(string: " ", attributes: attrCandidate)
let lineFeed = NSAttributedString(string: "\n", attributes: attrCandidate)
for (inlineIndex, _) in selectionKeys.enumerated() {
for (lineID, lineData) in candidateLines.enumerated() {
if !fallbackedLineRangeForCurrentPage.contains(lineID) { continue }
if !(0 ..< lineData.count).contains(inlineIndex) { continue }
let currentCell = lineData[inlineIndex]
let cellString = NSMutableAttributedString(
attributedString: currentCell.attributedString(
noSpacePadding: false, withHighlight: true, isMatrix: maxLinesPerPage > 1
)
)
if lineID != currentLineNumber {
cellString.addAttribute(
.foregroundColor, value: NSColor.gray,
range: .init(location: 0, length: cellString.string.utf16.count)
)
}
result.append(cellString)
if maxLinesPerPage > 1, currentCell.displayedText.count > 1 {
if currentCell.isHighlighted {
spacer.addAttribute(
.backgroundColor,
value: currentCell.highlightedNSColor,
range: .init(location: 0, length: spacer.string.utf16.count)
)
} else {
spacer.removeAttribute(
.backgroundColor,
range: .init(location: 0, length: spacer.string.utf16.count)
)
}
result.append(spacer)
}
}
result.append(lineFeed)
}
//
result.append(attributedDescriptionBottomPanes)
return result
}
private var attributedDescriptionBottomPanes: NSAttributedString {
let paragraphStyle = CandidateCellData.sharedParagraphStyle as! NSMutableParagraphStyle
paragraphStyle.lineSpacing = ceil(blankCell.size * 0.3)
paragraphStyle.lineBreakStrategy = .pushOut
let attrCandidate: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: blankCell.size, weight: .regular),
.paragraphStyle: paragraphStyle,
]
let result = NSMutableAttributedString(string: "", attributes: attrCandidate)
let positionCounterColorBG = NSApplication.isDarkMode
? NSColor(white: 0.215, alpha: 0.7)
: NSColor(white: 0.9, alpha: 0.7)
let positionCounterColorText = NSColor.controlTextColor
let positionCounterTextSize = max(ceil(CandidateCellData.unifiedSize * 0.7), 11)
let attrPositionCounter: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: positionCounterTextSize, weight: .bold),
.paragraphStyle: paragraphStyle,
.backgroundColor: positionCounterColorBG,
.foregroundColor: positionCounterColorText,
]
let positionCounter = NSAttributedString(
string: " \(currentPositionLabelText) ", attributes: attrPositionCounter
)
result.append(positionCounter)
if !tooltip.isEmpty {
let attrTooltip: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: positionCounterTextSize, weight: .regular),
.paragraphStyle: paragraphStyle,
]
let tooltipText = NSAttributedString(
string: " \(tooltip) ", attributes: attrTooltip
)
result.append(tooltipText)
}
if !reverseLookupResult.isEmpty {
let reverseLookupTextSize = max(ceil(CandidateCellData.unifiedSize * 0.6), 9)
let reverseLookupColorBG = NSApplication.isDarkMode
? NSColor(white: 0.1, alpha: 1)
: NSColor(white: 0.9, alpha: 1)
let attrReverseLookup: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: reverseLookupTextSize, weight: .regular),
.paragraphStyle: paragraphStyle,
.backgroundColor: reverseLookupColorBG,
]
let attrReverseLookupSpacer: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: reverseLookupTextSize, weight: .regular),
.paragraphStyle: paragraphStyle,
]
for neta in reverseLookupResult {
result.append(NSAttributedString(string: " ", attributes: attrReverseLookupSpacer))
result.append(NSAttributedString(string: " \(neta) ", attributes: attrReverseLookup))
if maxLinesPerPage == 1 { break }
}
}
result.addAttribute(.paragraphStyle, value: paragraphStyle, range: .init(location: 0, length: result.string.utf16.count))
return result
}
}

View File

@ -9,61 +9,44 @@
import Cocoa import Cocoa
import CocoaExtension import CocoaExtension
import Shared import Shared
import SwiftExtension
import SwiftUI import SwiftUI
@available(macOS 10.15, *) private extension NSUserInterfaceLayoutOrientation {
var layoutTDK: CandidatePool.LayoutOrientation {
switch self {
case .horizontal:
return .horizontal
case .vertical:
return .vertical
@unknown default:
return .horizontal
}
}
}
public class CtlCandidateTDK: CtlCandidate { public class CtlCandidateTDK: CtlCandidate {
public var maxLinesPerPage: Int = 0 public var maxLinesPerPage: Int = 0
public var isLegacyMode: Bool = false
private static var thePoolHorizontal: CandidatePool = .init(candidates: [], rowCapacity: 6) private static var thePool: CandidatePool = .init(candidates: [])
private static var thePoolVertical: CandidatePool = .init(candidates: [], columnCapacity: 6)
private static var currentView: NSView = .init() private static var currentView: NSView = .init()
@available(macOS 12, *) @available(macOS 10.15, *)
private var theViewHorizontal: some View { private var theView: some View {
VwrCandidateHorizontal( VwrCandidateTDK(
controller: self, thePool: Self.thePoolHorizontal, controller: self, thePool: Self.thePool
tooltip: tooltip, reverseLookupResult: reverseLookupResult
).edgesIgnoringSafeArea(.top) ).edgesIgnoringSafeArea(.top)
} }
@available(macOS 12, *) private var theViewLegacy: NSView {
private var theViewVertical: some View { let textField = NSTextField(
VwrCandidateVertical( labelWithAttributedString: Self.thePool.attributedDescription
controller: self, thePool: Self.thePoolVertical, )
tooltip: tooltip, reverseLookupResult: reverseLookupResult textField.isSelectable = false
).edgesIgnoringSafeArea(.top) textField.allowsEditingTextAttributes = false
} textField.preferredMaxLayoutWidth = textField.frame.width
textField.backgroundColor = .controlBackgroundColor
private var theViewHorizontalBackports: some View { return textField
VwrCandidateHorizontalBackports(
controller: self, thePool: Self.thePoolHorizontal,
tooltip: tooltip, reverseLookupResult: reverseLookupResult
).edgesIgnoringSafeArea(.top)
}
private var theViewVerticalBackports: some View {
VwrCandidateVerticalBackports(
controller: self, thePool: Self.thePoolVertical,
tooltip: tooltip, reverseLookupResult: reverseLookupResult
).edgesIgnoringSafeArea(.top)
}
private var thePool: CandidatePool {
get {
switch currentLayout {
case .horizontal: return Self.thePoolHorizontal
case .vertical: return Self.thePoolVertical
@unknown default: return .init(candidates: [], rowCapacity: 0)
}
}
set {
switch currentLayout {
case .horizontal: Self.thePoolHorizontal = newValue
case .vertical: Self.thePoolVertical = newValue
@unknown default: break
}
}
} }
// MARK: - Constructors // MARK: - Constructors
@ -93,165 +76,108 @@ public class CtlCandidateTDK: CtlCandidate {
// MARK: - Public functions // MARK: - Public functions
override public func reloadData() { override public func reloadData() {
CandidateCellData.highlightBackground = highlightedColor()
CandidateCellData.unifiedSize = candidateFont.pointSize CandidateCellData.unifiedSize = candidateFont.pointSize
guard let delegate = delegate else { return } guard let delegate = delegate else { return }
Self.thePool = .init(
switch currentLayout { candidates: delegate.candidatePairs(conv: true).map(\.1), lines: maxLinesPerPage,
case .horizontal: selectionKeys: delegate.selectionKeys, layout: currentLayout.layoutTDK, locale: locale
Self.thePoolHorizontal = .init(
candidates: delegate.candidatePairs(conv: true).map(\.1), rowCapacity: 6,
rows: maxLinesPerPage, selectionKeys: delegate.selectionKeys, locale: locale
) )
Self.thePoolHorizontal.highlight(at: 0) Self.thePool.tooltip = tooltip
case .vertical: Self.thePool.reverseLookupResult = reverseLookupResult
Self.thePoolVertical = .init( Self.thePool.highlight(at: 0)
candidates: delegate.candidatePairs(conv: true).map(\.1), columnCapacity: 6,
columns: maxLinesPerPage, selectionKeys: delegate.selectionKeys, locale: locale
)
Self.thePoolVertical.highlight(at: 0)
@unknown default:
return
}
updateDisplay() updateDisplay()
} }
override open func updateDisplay() { override open func updateDisplay() {
guard let window = window else { return } guard let window = window else { return }
reverseLookupResult = delegate?.reverseLookup(for: currentSelectedCandidateText) ?? [] if let currentCandidateText = Self.thePool.currentSelectedCandidateText {
switch currentLayout { reverseLookupResult = delegate?.reverseLookup(for: currentCandidateText) ?? []
case .horizontal: Self.thePool.reverseLookupResult = reverseLookupResult
}
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
if #available(macOS 12, *) { if #available(macOS 10.15, *) {
Self.currentView = NSHostingView(rootView: theViewHorizontal) if isLegacyMode {
} else { updateNSWindowLegacy(window)
Self.currentView = NSHostingView(rootView: theViewHorizontalBackports)
}
let newSize = Self.currentView.fittingSize
window.contentView = Self.currentView
window.setContentSize(newSize)
}
case .vertical:
DispatchQueue.main.async { [self] in
if #available(macOS 12, *) {
Self.currentView = NSHostingView(rootView: theViewVertical)
} else {
Self.currentView = NSHostingView(rootView: theViewVerticalBackports)
}
let newSize = Self.currentView.fittingSize
window.contentView = Self.currentView
window.setContentSize(newSize)
}
@unknown default:
return return
} }
window.isOpaque = false
window.backgroundColor = NSColor.clear
Self.currentView = NSHostingView(rootView: theView)
let newSize = Self.currentView.fittingSize
window.contentView = Self.currentView
window.setContentSize(newSize)
} else {
updateNSWindowLegacy(window)
}
}
} }
func updateNSWindowLegacy(_ window: NSWindow) {
window.isOpaque = true
window.backgroundColor = NSColor.controlBackgroundColor
let viewToDraw = theViewLegacy
let coreSize = viewToDraw.fittingSize
let padding: Double = 5
let outerSize: NSSize = .init(
width: coreSize.width + 2 * padding,
height: coreSize.height + 2 * padding
)
let innerOrigin: NSPoint = .init(x: padding, y: padding)
let outerRect: NSRect = .init(origin: .zero, size: outerSize)
viewToDraw.setFrameOrigin(innerOrigin)
Self.currentView = NSView(frame: outerRect)
Self.currentView.addSubview(viewToDraw)
window.contentView = Self.currentView
window.setContentSize(outerSize)
}
// Already implemented in CandidatePool.
@discardableResult override public func showNextPage() -> Bool { @discardableResult override public func showNextPage() -> Bool {
showNextLine(count: thePool.maxLinesPerPage) defer { updateDisplay() }
} return Self.thePool.flipPage(isBackward: false)
@discardableResult override public func showNextLine() -> Bool {
showNextLine(count: 1)
}
public func showNextLine(count: Int) -> Bool {
if thePool.currentLineNumber == thePool.candidateLines.count - 1 {
return highlightNextCandidate()
}
if count <= 0 { return false }
for _ in 0 ..< min(thePool.maxLinesPerPage, count) {
thePool.selectNewNeighborLine(isForward: true)
}
updateDisplay()
return true
} }
// Already implemented in CandidatePool.
@discardableResult override public func showPreviousPage() -> Bool { @discardableResult override public func showPreviousPage() -> Bool {
showPreviousLine(count: thePool.maxLinesPerPage) defer { updateDisplay() }
return Self.thePool.flipPage(isBackward: true)
} }
// Already implemented in CandidatePool.
@discardableResult override public func showPreviousLine() -> Bool { @discardableResult override public func showPreviousLine() -> Bool {
showPreviousLine(count: 1) defer { updateDisplay() }
return Self.thePool.consecutivelyFlipLines(isBackward: true, count: 1)
} }
public func showPreviousLine(count: Int) -> Bool { // Already implemented in CandidatePool.
if thePool.currentLineNumber == 0 { @discardableResult override public func showNextLine() -> Bool {
return highlightPreviousCandidate() defer { updateDisplay() }
} return Self.thePool.consecutivelyFlipLines(isBackward: false, count: 1)
if count <= 0 { return false }
for _ in 0 ..< min(thePool.maxLinesPerPage, count) {
thePool.selectNewNeighborLine(isForward: false)
}
updateDisplay()
return true
} }
// Already implemented in CandidatePool.
@discardableResult override public func highlightNextCandidate() -> Bool { @discardableResult override public func highlightNextCandidate() -> Bool {
if thePool.highlightedIndex == thePool.candidateDataAll.count - 1 { defer { updateDisplay() }
thePool.highlight(at: 0) return Self.thePool.highlightNeighborCandidate(isBackward: false)
updateDisplay()
return false
}
thePool.highlight(at: thePool.highlightedIndex + 1)
updateDisplay()
return true
} }
// Already implemented in CandidatePool.
@discardableResult override public func highlightPreviousCandidate() -> Bool { @discardableResult override public func highlightPreviousCandidate() -> Bool {
if thePool.highlightedIndex == 0 { defer { updateDisplay() }
thePool.highlight(at: thePool.candidateDataAll.count - 1) return Self.thePool.highlightNeighborCandidate(isBackward: true)
updateDisplay()
return false
}
thePool.highlight(at: thePool.highlightedIndex - 1)
updateDisplay()
return true
} }
override public func candidateIndexAtKeyLabelIndex(_ id: Int) -> Int { // Already implemented in CandidatePool.
let arrCurrentLine = thePool.candidateLines[thePool.currentLineNumber] override public func candidateIndexAtKeyLabelIndex(_ id: Int) -> Int? {
if !(0 ..< arrCurrentLine.count).contains(id) { return -114_514 } Self.thePool.calculateCandidateIndex(subIndex: id)
let actualID = max(0, min(id, arrCurrentLine.count - 1))
return arrCurrentLine[actualID].index
} }
// Already implemented in CandidatePool.
override public var highlightedIndex: Int { override public var highlightedIndex: Int {
get { thePool.highlightedIndex } get { Self.thePool.highlightedIndex }
set { set {
thePool.highlight(at: newValue) Self.thePool.highlight(at: newValue)
updateDisplay() updateDisplay()
} }
} }
} }
@available(macOS 10.15, *)
extension CtlCandidateTDK {
private var isMontereyAvailable: Bool {
if #unavailable(macOS 12) { return false }
return true
}
private var currentSelectedCandidateText: String {
if thePool.candidateDataAll.count > highlightedIndex {
return thePool.candidateDataAll[highlightedIndex].displayedText
}
return ""
}
}
@available(macOS 10.15, *)
public extension CtlCandidateTDK {
var highlightedColorUIBackports: some View {
//
let result: Color = {
switch locale {
case "zh-Hans": return Color.red
case "zh-Hant": return Color.blue
case "ja": return Color.pink
default: return Color.accentColor
}
}()
return result.opacity(0.85)
}
}

View File

@ -0,0 +1,508 @@
// (c) 2022 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 Cocoa
import Shared
import SwiftUI
import SwiftUIBackports
// MARK: - Main View
@available(macOS 10.15, *)
public struct VwrCandidateTDK: View {
public weak var controller: CtlCandidateTDK?
@Environment(\.colorScheme) var colorScheme
@State public var thePool: CandidatePool
@State public var forceCatalinaCompatibility: Bool = false
var tooltip: String { thePool.tooltip }
var reverseLookupResult: [String] { thePool.reverseLookupResult }
let horizontalCellSpacing: CGFloat = 0
public var body: some View {
Group {
VStack(alignment: .leading, spacing: 0) {
switch thePool.layout {
case .horizontal:
ZStack {
candidateListBackground
HStack {
mainViewHorizontal
if thePool.maxLinesPerPage == 1 {
rightPanes
}
}
}
default:
mainViewVertical.background(candidateListBackground)
}
if thePool.maxLinesPerPage > 1 || thePool.layout == .vertical {
if controller?.delegate?.showReverseLookupResult ?? true, !tooltip.isEmpty {
ZStack(alignment: .leading) {
bottomPanelBackgroundTDK
.opacity(colorScheme == .dark ? 0 : 0.35)
reverseLookupPane.padding([.top, .horizontal], 4).padding([.bottom], 2)
}
}
statusBar.background(
bottomPanelBackgroundTDK
.opacity(colorScheme == .dark ? 0.35 : 0.5)
)
}
}
.frame(minWidth: windowWidth, maxWidth: windowWidth)
.background(candidateListBackground)
.overlay(
RoundedRectangle(cornerRadius: 10).stroke(
absoluteBackgroundColor.opacity(colorScheme == .dark ? 1 : 0.1), lineWidth: 0.5
)
)
.cornerRadius(10)
}
}
}
// MARK: - Main Views.
@available(macOS 10.15, *)
extension VwrCandidateTDK {
var mainViewHorizontal: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: 1.6) {
ForEach(thePool.lineRangeForCurrentPage, id: \.self) { rowIndex in
ZStack(alignment: .leading) {
lineBackground(lineID: rowIndex).cornerRadius(6)
HStack(spacing: horizontalCellSpacing) {
ForEach(Array(thePool.candidateLines[rowIndex]), id: \.self) { currentCandidate in
drawCandidate(currentCandidate).fixedSize()
}
}
}
.opacity(rowIndex == thePool.currentLineNumber ? 1 : 0.95)
.id(rowIndex)
}
if thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count > 0 {
ForEach(thePool.lineRangeForFinalPageBlanked, id: \.self) { _ in
HStack(spacing: 0) {
attributedStringFor(cell: thePool.blankCell)
.frame(alignment: .topLeading)
.contentShape(Rectangle())
Spacer()
}.frame(
minWidth: 0,
maxWidth: thePool.maxLinesPerPage != 1 ? .infinity : nil,
alignment: .topLeading
)
}
}
}
}
.fixedSize(horizontal: thePool.maxLinesPerPage == 1, vertical: true)
.padding([.horizontal, .top], 5)
.padding([.bottom], thePool.maxLinesPerPage == 1 ? 5 : 2)
}
var mainViewVertical: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 4) {
ForEach(Array(thePool.lineRangeForCurrentPage.enumerated()), id: \.offset) { loopIndex, columnIndex in
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(thePool.candidateLines[columnIndex]), id: \.self) { currentCandidate in
drawCandidate(currentCandidate)
}
if thePool.candidateLines[columnIndex].count < thePool.maxLineCapacity {
ForEach(0 ..< thePool.dummyCellsRequiredForCurrentLine, id: \.self) { _ in
drawCandidate(thePool.blankCell)
}
}
}
.background(lineBackground(lineID: columnIndex)).cornerRadius(6)
.opacity(columnIndex == thePool.currentLineNumber ? 1 : 0.95)
.frame(
minWidth: thePool.maxLinesPerPage == 1
? max(Double(CandidateCellData.unifiedSize * 6), 90)
: nil,
alignment: .topLeading
)
.id(columnIndex)
if thePool.maxLinesPerPage > 1, thePool.maxLinesPerPage <= loopIndex + 1 {
Spacer(minLength: 0)
}
}
if thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count > 0 {
ForEach(Array(thePool.lineRangeForFinalPageBlanked.enumerated()), id: \.offset) { loopIndex, _ in
VStack(alignment: .leading, spacing: 0) {
ForEach(0 ..< thePool.maxLineCapacity, id: \.self) { _ in
attributedStringFor(cell: thePool.blankCell).fixedSize()
.frame(
width: ceil(thePool.blankCell.minWidthToDraw(isMatrix: true)),
alignment: .topLeading
)
.contentShape(Rectangle())
}
}.frame(
minWidth: 0,
maxWidth: .infinity,
alignment: .topLeading
)
if thePool.maxLinesPerPage > 1,
loopIndex >= thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count - 1
{
Spacer(minLength: 0)
}
}
}
}
}
.fixedSize(horizontal: true, vertical: false)
.padding([.horizontal, .top], 5)
.padding([.bottom], thePool.maxLinesPerPage == 1 ? 5 : 2)
}
}
// MARK: - Common Components.
@available(macOS 10.15, *)
extension VwrCandidateTDK {
func drawCandidate(_ cell: CandidateCellData) -> some View {
attributedStringFor(cell: cell)
.frame(minWidth: cellWidth(cell).min, maxWidth: cellWidth(cell).max, alignment: .topLeading)
.contentShape(Rectangle())
.onTapGesture { didSelectCandidateAt(cell.index) }
.contextMenu {
if controller?.delegate?.isCandidateContextMenuEnabled ?? false {
Button {
didRightClickCandidateAt(cell.index, action: .toBoost)
} label: {
Text("" + cell.displayedText)
}
Button {
didRightClickCandidateAt(cell.index, action: .toNerf)
} label: {
Text("" + cell.displayedText)
}
Button {
didRightClickCandidateAt(cell.index, action: .toFilter)
} label: {
Text("✖︎ " + cell.displayedText)
}
}
}
}
func lineBackground(lineID: Int) -> Color {
let isCurrentLineInMatrix = lineID == thePool.currentLineNumber && thePool.maxLinesPerPage != 1
switch thePool.layout {
case .horizontal where isCurrentLineInMatrix:
return Color.primary.opacity(0.05)
case .vertical where isCurrentLineInMatrix:
return absoluteBackgroundColor.opacity(0.15)
default:
return Color.clear
}
}
func cellWidth(_ cell: CandidateCellData) -> (min: CGFloat?, max: CGFloat?) {
let minAccepted = ceil(thePool.blankCell.minWidthToDraw(isMatrix: false) * 1.1)
let defaultMin: CGFloat = cell.minWidthToDraw(isMatrix: thePool.maxLinesPerPage != 1)
var min: CGFloat = defaultMin
if thePool.layout != .vertical, thePool.maxLinesPerPage == 1 {
min = max(minAccepted, cell.minWidthToDraw(isMatrix: false))
}
return (min, nil)
}
var windowWidth: CGFloat? {
let paddings: CGFloat = 10.0
let spacings: CGFloat = horizontalCellSpacing * Double(thePool.maxLineCapacity - 1)
let maxWindowWith: CGFloat
= ceil(
Double(thePool.maxLineCapacity) * (thePool.blankCell.minWidthToDraw())
+ paddings + spacings
)
return thePool.layout == .horizontal && thePool.maxLinesPerPage > 1 ? maxWindowWith : nil
}
var firstReverseLookupResult: String {
reverseLookupResult.first?.trimmingCharacters(in: .newlines) ?? ""
}
///
/// - Remark: SwiftUI Text
/// View .fixedSize()
/// View 0 -0.5
/// - Parameters:
/// - text:
/// - fontSize:
/// - isBold:
/// - Returns:
func getTextWidth(text: String, fontSize: CGFloat, isBold: Bool) -> CGFloat? {
guard !text.isEmpty else { return nil }
let attributes: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.systemFont(ofSize: fontSize, weight: isBold ? .bold : .regular),
.paragraphStyle: CandidateCellData.sharedParagraphStyle,
]
let attrString = NSAttributedString(string: text, attributes: attributes)
return ceil(attrString.boundingDimension.width)
}
var positionLabelView: some View {
ZStack {
Color(white: colorScheme == .dark ? 0.215 : 0.9).cornerRadius(4)
Text(thePool.currentPositionLabelText).lineLimit(1)
.font(.system(size: max(ceil(CandidateCellData.unifiedSize * 0.7), 11), weight: .bold))
.frame(
width: getTextWidth(
text: thePool.currentPositionLabelText,
fontSize: max(ceil(CandidateCellData.unifiedSize * 0.7), 11),
isBold: true
)
)
.padding([.horizontal], 2)
.foregroundColor(.primary.opacity(0.8))
}.fixedSize()
}
var rightPanes: some View {
HStack {
if !tooltip.isEmpty {
ZStack(alignment: .center) {
Circle().fill(highlightBackgroundTDK.opacity(0.8))
Text(tooltip.first?.description ?? "").padding(2).font(.system(size: CandidateCellData.unifiedSize))
}.frame(width: ceil(CandidateCellData.unifiedSize * 1.7), height: ceil(CandidateCellData.unifiedSize * 1.7))
}
VStack(alignment: .center, spacing: 1) {
positionLabelView
if controller?.delegate?.showReverseLookupResult ?? true {
if !firstReverseLookupResult.isEmpty {
ZStack(alignment: .center) {
Color(white: colorScheme == .dark ? 0.2 : 0.9).cornerRadius(4)
Text(firstReverseLookupResult)
.font(.system(size: max(ceil(CandidateCellData.unifiedSize * 0.6), 9)))
.frame(
width: getTextWidth(
text: firstReverseLookupResult,
fontSize: max(ceil(CandidateCellData.unifiedSize * 0.6), 9),
isBold: false
)
)
.opacity(0.8).padding([.horizontal], 4)
}.fixedSize()
}
}
}
.opacity(0.9)
.fixedSize()
.padding([.trailing], 12)
}
}
var reverseLookupPane: some View {
HStack(alignment: .center, spacing: 2) {
if thePool.maxLinesPerPage == 1 {
if !firstReverseLookupResult.isEmpty {
ZStack(alignment: .center) {
Color(white: colorScheme == .dark ? 0.3 : 0.9).cornerRadius(3)
Text("\(firstReverseLookupResult.trimmingCharacters(in: .newlines))")
.lineLimit(1).padding([.horizontal], 2)
}.fixedSize()
}
} else {
Text("").opacity(0.8)
ForEach(reverseLookupResult, id: \.self) { currentResult in
ZStack(alignment: .center) {
Color(white: colorScheme == .dark ? 0.3 : 0.9).cornerRadius(3)
Text("\(currentResult.trimmingCharacters(in: .newlines))")
.lineLimit(1).padding([.horizontal], 2)
}.fixedSize()
}
}
}
.font(.system(size: max(ceil(CandidateCellData.unifiedSize * 0.6), 9)))
.foregroundColor(colorScheme == .light ? Color(white: 0.1) : Color(white: 0.9))
}
var statusBar: some View {
HStack(alignment: .center) {
if !tooltip.isEmpty {
Text(tooltip).lineLimit(1)
} else {
if controller?.delegate?.showReverseLookupResult ?? true, tooltip.isEmpty {
reverseLookupPane.padding(0)
}
}
Spacer()
positionLabelView
}
.font(.system(size: max(ceil(CandidateCellData.unifiedSize * 0.7), 11), weight: .bold))
.padding([.bottom, .horizontal], 7).padding([.top], 2)
.fixedSize(horizontal: false, vertical: true)
}
var highlightBackgroundTDK: Color {
tooltip.isEmpty ? Color(white: colorScheme == .dark ? 0.2 : 0.9) : thePool.blankCell.themeColor
}
var candidateListBackground: some View {
Group {
absoluteBackgroundColor
if colorScheme == .dark {
Color.primary.opacity(0.05)
} else {
Color.primary.opacity(0.01)
}
}
}
var absoluteBackgroundColor: Color {
if colorScheme == .dark {
return Color(white: 0)
} else {
return Color(white: 1)
}
}
var bottomPanelBackgroundTDK: Color {
Color(white: colorScheme == .dark ? 0.145 : 0.95)
}
func attributedStringFor(cell theCell: CandidateCellData) -> some View {
let defaultResult = theCell.attributedStringForSwiftUIBackports
if forceCatalinaCompatibility {
return defaultResult
}
if #available(macOS 12, *) {
return theCell.attributedStringForSwiftUI
}
return defaultResult
}
}
// MARK: - Delegate Methods
@available(macOS 10.15, *)
extension VwrCandidateTDK {
func didSelectCandidateAt(_ pos: Int) {
controller?.delegate?.candidatePairSelected(at: pos)
}
func didRightClickCandidateAt(_ pos: Int, action: CandidateContextMenuAction) {
controller?.delegate?.candidatePairRightClicked(at: pos, action: action)
}
}
// MARK: - Preview
import SwiftExtension
@available(macOS 10.15, *)
struct AttributedLabel_Previews: PreviewProvider {
@State static var testCandidates: [String] = [
"二十四歲是學生", "二十四歲", "昏睡紅茶", "食雪漢", "意味深", "學生", "便乗",
"🐂🍺🐂🍺", "🐃🍺", "🐂🍺", "🐃🐂🍺🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺",
"迫真", "驚愕", "論證", "正論", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "", "", "", "便", "",
"", "", "",
]
@State static var reverseLookupResult = ["mmmmm", "dddd"]
@State static var tooltip = "📼"
@State static var oldOS: Bool = true
static var thePoolX: CandidatePool {
var result = CandidatePool(
candidates: testCandidates, lines: 4,
selectionKeys: "123456", layout: .horizontal
)
result.reverseLookupResult = Self.reverseLookupResult
result.tooltip = Self.tooltip
result.highlight(at: 0)
return result
}
static var thePoolXS: CandidatePool {
var result = CandidatePool(
candidates: testCandidates, lines: 1,
selectionKeys: "123456", layout: .horizontal
)
result.reverseLookupResult = Self.reverseLookupResult
result.tooltip = Self.tooltip
result.highlight(at: 1)
return result
}
static var thePoolY: CandidatePool {
var result = CandidatePool(
candidates: testCandidates, lines: 4,
selectionKeys: "123456", layout: .vertical
)
result.reverseLookupResult = Self.reverseLookupResult
result.tooltip = Self.tooltip
result.flipPage(isBackward: false)
result.highlight(at: 2)
return result
}
static var thePoolYS: CandidatePool {
var result = CandidatePool(
candidates: testCandidates, lines: 1,
selectionKeys: "123456", layout: .vertical
)
result.reverseLookupResult = Self.reverseLookupResult
result.tooltip = Self.tooltip
result.highlight(at: 1)
return result
}
static var candidateListBackground: Color {
if NSApplication.isDarkMode {
return Color(white: 0.05)
} else {
return Color(white: 0.99)
}
}
static var previews: some View {
VStack {
HStack(alignment: .top) {
Text("田所選字窗 效能模式").bold().font(Font.system(.title))
VStack {
AttributedLabel(attributedString: Self.thePoolX.attributedDescription)
.padding(5)
.background(candidateListBackground)
.cornerRadius(10).fixedSize()
AttributedLabel(attributedString: Self.thePoolXS.attributedDescription)
.padding(5)
.background(candidateListBackground)
.cornerRadius(10).fixedSize()
HStack {
AttributedLabel(attributedString: Self.thePoolY.attributedDescription)
.padding(5)
.background(candidateListBackground)
.cornerRadius(10).fixedSize()
AttributedLabel(attributedString: Self.thePoolYS.attributedDescription)
.padding(5)
.background(candidateListBackground)
.cornerRadius(10).fixedSize()
}
}
}
Divider()
HStack(alignment: .top) {
Text("田所選字窗 SwiftUI 模式").bold().font(Font.system(.title))
VStack {
VwrCandidateTDK(controller: nil, thePool: thePoolX, forceCatalinaCompatibility: oldOS).fixedSize()
VwrCandidateTDK(controller: nil, thePool: thePoolXS, forceCatalinaCompatibility: oldOS).fixedSize()
HStack {
VwrCandidateTDK(controller: nil, thePool: thePoolY, forceCatalinaCompatibility: oldOS).fixedSize()
VwrCandidateTDK(controller: nil, thePool: thePoolYS, forceCatalinaCompatibility: oldOS).fixedSize()
}
}
}
}
}
}

View File

@ -1,161 +0,0 @@
// (c) 2022 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 Cocoa
import Shared
import SwiftUI
// MARK: - Some useless tests
@available(macOS 12, *)
struct CandidatePoolViewUIHorizontal_Previews: PreviewProvider {
@State static var testCandidates: [String] = [
"二十四歲是學生", "二十四歲", "昏睡紅茶", "食雪漢", "意味深", "學生", "便乗",
"🐂🍺🐂🍺", "🐃🍺", "🐂🍺", "🐃🐂🍺🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺",
"迫真", "驚愕", "論證", "正論", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "", "", "", "便", "",
"", "", "",
]
static var thePool: CandidatePool {
var result = CandidatePool(candidates: testCandidates, rowCapacity: 6)
// 使
result.highlight(at: 5)
return result
}
static var previews: some View {
VwrCandidateHorizontal(controller: nil, thePool: thePool).fixedSize()
}
}
@available(macOS 12, *)
public struct VwrCandidateHorizontal: View {
public weak var controller: CtlCandidateTDK?
@Environment(\.colorScheme) var colorScheme
@State public var thePool: CandidatePool
@State public var tooltip: String = ""
@State public var reverseLookupResult: [String] = []
private var positionLabel: String {
(thePool.highlightedIndex + 1).description + "/" + thePool.candidateDataAll.count.description
}
private func didSelectCandidateAt(_ pos: Int) {
if let delegate = controller?.delegate {
delegate.candidatePairSelected(at: pos)
}
}
private func didRightClickCandidateAt(_ pos: Int, action: CandidateContextMenuAction) {
if let delegate = controller?.delegate {
delegate.candidatePairRightClicked(at: pos, action: action)
}
}
public var body: some View {
VStack(alignment: .leading, spacing: 0) {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: 1.6) {
ForEach(thePool.rangeForCurrentPage, id: \.self) { rowIndex in
HStack(spacing: ceil(CandidateCellData.unifiedSize * 0.35)) {
ForEach(Array(thePool.candidateLines[rowIndex]), id: \.self) { currentCandidate in
currentCandidate.attributedStringForSwiftUI.fixedSize()
.contentShape(Rectangle())
.frame(alignment: .topLeading)
.onTapGesture { didSelectCandidateAt(currentCandidate.index) }
.contextMenu {
if controller?.delegate?.isCandidateContextMenuEnabled ?? false {
Button {
didRightClickCandidateAt(currentCandidate.index, action: .toBoost)
} label: {
Text("" + currentCandidate.displayedText)
}
Button {
didRightClickCandidateAt(currentCandidate.index, action: .toNerf)
} label: {
Text("" + currentCandidate.displayedText)
}
Button {
didRightClickCandidateAt(currentCandidate.index, action: .toFilter)
} label: {
Text("✖︎ " + currentCandidate.displayedText)
}
}
}
}
Spacer()
}.frame(
minWidth: 0,
maxWidth: .infinity,
alignment: .topLeading
).id(rowIndex)
Divider()
}
if thePool.maxLinesPerPage - thePool.rangeForCurrentPage.count > 0 {
ForEach(thePool.rangeForLastPageBlanked, id: \.self) { _ in
HStack(spacing: 0) {
thePool.blankCell.attributedStringForSwiftUI
.contentShape(Rectangle())
.frame(alignment: .topLeading)
Spacer()
}.frame(
minWidth: 0,
maxWidth: .infinity,
alignment: .topLeading
)
Divider()
}
}
}
}
.fixedSize(horizontal: false, vertical: true)
.padding([.horizontal], 5).padding([.top], 5).padding([.bottom], -1)
if controller?.delegate?.showReverseLookupResult ?? true {
ZStack(alignment: .leading) {
Color(white: colorScheme == .dark ? 0.15 : 0.97)
HStack(alignment: .center, spacing: 4) {
Text("")
ForEach(reverseLookupResult, id: \.self) { currentResult in
ZStack(alignment: .center) {
Color(white: colorScheme == .dark ? 0.3 : 0.9).cornerRadius(3)
Text(" \(currentResult.trimmingCharacters(in: .newlines)) ").lineLimit(1)
}.fixedSize()
}
}
.font(.system(size: max(CandidateCellData.unifiedSize * 0.6, 9)))
.padding([.horizontal], 4).padding([.vertical], 4)
.foregroundColor(colorScheme == .light ? Color(white: 0.1) : Color(white: 0.9))
}
}
ZStack(alignment: .trailing) {
Color(nsColor: tooltip.isEmpty ? .windowBackgroundColor : CandidateCellData.highlightBackground)
.ignoresSafeArea()
HStack(alignment: .center) {
if !tooltip.isEmpty {
Text(tooltip).lineLimit(1)
Spacer()
}
Text(positionLabel).lineLimit(1)
}
.font(.system(size: max(CandidateCellData.unifiedSize * 0.7, 11), weight: .bold))
.padding(7).foregroundColor(
.init(nsColor: tooltip.isEmpty ? .controlTextColor : .selectedMenuItemTextColor.withAlphaComponent(0.9))
)
}
.fixedSize(horizontal: false, vertical: true)
}
.frame(minWidth: thePool.maxWindowWidth, maxWidth: thePool.maxWindowWidth)
.background(Color(nsColor: NSColor.controlBackgroundColor).ignoresSafeArea())
.overlay(
RoundedRectangle(cornerRadius: 10).stroke(
Color(white: colorScheme == .dark ? 0 : 1).opacity(colorScheme == .dark ? 1 : 0.1), lineWidth: 0.5
)
)
.cornerRadius(10)
}
}

View File

@ -1,171 +0,0 @@
// (c) 2022 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 Cocoa
import Shared
import SwiftUI
// MARK: - Some useless tests
@available(macOS 12, *)
struct CandidatePoolViewUIVertical_Previews: PreviewProvider {
@State static var testCandidates: [String] = [
"二十四歲是學生", "二十四歲", "昏睡紅茶", "食雪漢", "意味深", "學生", "便乗",
"🐂🍺🐂🍺", "🐃🍺", "🐂🍺", "🐃🐂🍺🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺",
"迫真", "驚愕", "論證", "正論", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "", "", "", "便", "",
"", "", "",
]
static var thePool: CandidatePool {
var result = CandidatePool(candidates: testCandidates, columnCapacity: 6, selectionKeys: "123456789")
// 使
result.highlight(at: 5)
return result
}
static var previews: some View {
VwrCandidateVertical(controller: nil, thePool: thePool).fixedSize()
}
}
@available(macOS 12, *)
public struct VwrCandidateVertical: View {
public weak var controller: CtlCandidateTDK?
@Environment(\.colorScheme) var colorScheme
@State public var thePool: CandidatePool
@State public var tooltip: String = ""
@State public var reverseLookupResult: [String] = []
private var positionLabel: String {
(thePool.highlightedIndex + 1).description + "/" + thePool.candidateDataAll.count.description
}
private func didSelectCandidateAt(_ pos: Int) {
if let delegate = controller?.delegate {
delegate.candidatePairSelected(at: pos)
}
}
private func didRightClickCandidateAt(_ pos: Int, action: CandidateContextMenuAction) {
if let delegate = controller?.delegate {
delegate.candidatePairRightClicked(at: pos, action: action)
}
}
public var body: some View {
VStack(alignment: .leading, spacing: 0) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 10) {
ForEach(Array(thePool.rangeForCurrentPage.enumerated()), id: \.offset) { loopIndex, columnIndex in
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(thePool.candidateLines[columnIndex]), id: \.self) { currentCandidate in
HStack(spacing: 0) {
currentCandidate.attributedStringForSwiftUI.fixedSize(horizontal: false, vertical: true)
.frame(
maxWidth: .infinity,
alignment: .topLeading
)
.contentShape(Rectangle())
.onTapGesture { didSelectCandidateAt(currentCandidate.index) }
.contextMenu {
if controller?.delegate?.isCandidateContextMenuEnabled ?? false {
Button {
didRightClickCandidateAt(currentCandidate.index, action: .toBoost)
} label: {
Text("" + currentCandidate.displayedText)
}
Button {
didRightClickCandidateAt(currentCandidate.index, action: .toNerf)
} label: {
Text("" + currentCandidate.displayedText)
}
Button {
didRightClickCandidateAt(currentCandidate.index, action: .toFilter)
} label: {
Text("✖︎ " + currentCandidate.displayedText)
}
}
}
}
}
}.frame(
minWidth: Double(CandidateCellData.unifiedSize * 5),
alignment: .topLeading
).id(columnIndex)
if loopIndex < thePool.maxLinesPerPage - 1 {
Divider()
} else if thePool.maxLinesPerPage > 1 {
Spacer(minLength: 0)
}
}
if thePool.maxLinesPerPage - thePool.rangeForCurrentPage.count > 0 {
ForEach(Array(thePool.rangeForLastPageBlanked.enumerated()), id: \.offset) { loopIndex, _ in
VStack(alignment: .leading, spacing: 0) {
ForEach(0 ..< thePool.maxLineCapacity, id: \.self) { _ in
thePool.blankCell.attributedStringForSwiftUI.fixedSize()
.frame(width: Double(CandidateCellData.unifiedSize * 5), alignment: .topLeading)
.contentShape(Rectangle())
}
}.frame(
minWidth: 0,
maxWidth: .infinity,
alignment: .topLeading
)
if loopIndex < thePool.maxLinesPerPage - thePool.rangeForCurrentPage.count - 1 {
Divider()
} else if thePool.maxLinesPerPage > 1 {
Spacer(minLength: 0)
}
}
}
}
}
.fixedSize(horizontal: true, vertical: false).padding(5)
if controller?.delegate?.showReverseLookupResult ?? true {
ZStack(alignment: .leading) {
Color(white: colorScheme == .dark ? 0.15 : 0.97)
HStack(alignment: .center, spacing: 4) {
Text("")
ForEach(reverseLookupResult, id: \.self) { currentResult in
ZStack(alignment: .center) {
Color(white: colorScheme == .dark ? 0.3 : 0.9).cornerRadius(3)
Text(" \(currentResult.trimmingCharacters(in: .newlines)) ").lineLimit(1)
}.fixedSize()
}
}
.font(.system(size: max(CandidateCellData.unifiedSize * 0.6, 9)))
.padding([.horizontal], 4).padding([.vertical], 4)
.foregroundColor(colorScheme == .light ? Color(white: 0.1) : Color(white: 0.9))
}
}
ZStack(alignment: .trailing) {
Color(nsColor: tooltip.isEmpty ? .windowBackgroundColor : CandidateCellData.highlightBackground)
.ignoresSafeArea()
HStack(alignment: .center) {
if !tooltip.isEmpty {
Text(tooltip).lineLimit(1)
Spacer()
}
Text(positionLabel).lineLimit(1)
}
.font(.system(size: max(CandidateCellData.unifiedSize * 0.7, 11), weight: .bold))
.padding(7).foregroundColor(
.init(nsColor: tooltip.isEmpty ? .controlTextColor : .selectedMenuItemTextColor.withAlphaComponent(0.9))
)
}
.fixedSize(horizontal: false, vertical: true)
}
.background(Color(nsColor: NSColor.controlBackgroundColor).ignoresSafeArea())
.overlay(
RoundedRectangle(cornerRadius: 10).stroke(
Color(white: colorScheme == .dark ? 0 : 1).opacity(colorScheme == .dark ? 1 : 0.1), lineWidth: 0.5
)
)
.cornerRadius(10)
}
}

View File

@ -1,166 +0,0 @@
// (c) 2022 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 Cocoa
import Shared
import SwiftUI
import SwiftUIBackports
// MARK: - Some useless tests
@available(macOS 10.15, *)
struct CandidatePoolViewUIHorizontalBackports_Previews: PreviewProvider {
@State static var testCandidates: [String] = [
"二十四歲是學生", "二十四歲", "昏睡紅茶", "食雪漢", "意味深", "學生", "便乗",
"🐂🍺🐂🍺", "🐃🍺", "🐂🍺", "🐃🐂🍺🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺",
"迫真", "驚愕", "論證", "正論", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "", "", "", "便", "",
"", "", "",
]
static var thePool: CandidatePool {
var result = CandidatePool(candidates: testCandidates, rowCapacity: 6)
// 使
result.highlight(at: 5)
return result
}
static var previews: some View {
VwrCandidateHorizontalBackports(controller: nil, thePool: thePool).fixedSize()
}
}
@available(macOS 10.15, *)
public struct VwrCandidateHorizontalBackports: View {
public weak var controller: CtlCandidateTDK?
@Environment(\.colorScheme) var colorScheme
@State public var thePool: CandidatePool
@State public var tooltip: String = ""
@State public var reverseLookupResult: [String] = []
private var positionLabel: String {
(thePool.highlightedIndex + 1).description + "/" + thePool.candidateDataAll.count.description
}
private func didSelectCandidateAt(_ pos: Int) {
if let delegate = controller?.delegate {
delegate.candidatePairSelected(at: pos)
}
}
private func didRightClickCandidateAt(_ pos: Int, action: CandidateContextMenuAction) {
if let delegate = controller?.delegate {
delegate.candidatePairRightClicked(at: pos, action: action)
}
}
public var body: some View {
VStack(alignment: .leading, spacing: 0) {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: 1.6) {
ForEach(thePool.rangeForCurrentPage, id: \.self) { rowIndex in
HStack(spacing: ceil(CandidateCellData.unifiedSize * 0.35)) {
ForEach(Array(thePool.candidateLines[rowIndex]), id: \.self) { currentCandidate in
currentCandidate.attributedStringForSwiftUIBackports.fixedSize()
.contentShape(Rectangle())
.frame(alignment: .topLeading)
.onTapGesture { didSelectCandidateAt(currentCandidate.index) }
.contextMenu {
if controller?.delegate?.isCandidateContextMenuEnabled ?? false {
Button {
didRightClickCandidateAt(currentCandidate.index, action: .toBoost)
} label: {
Text("" + currentCandidate.displayedText)
}
Button {
didRightClickCandidateAt(currentCandidate.index, action: .toNerf)
} label: {
Text("" + currentCandidate.displayedText)
}
Button {
didRightClickCandidateAt(currentCandidate.index, action: .toFilter)
} label: {
Text("✖︎ " + currentCandidate.displayedText)
}
}
}
}
Spacer()
}.frame(
minWidth: 0,
maxWidth: .infinity,
alignment: .topLeading
).id(rowIndex)
Divider()
}
if thePool.maxLinesPerPage - thePool.rangeForCurrentPage.count > 0 {
ForEach(thePool.rangeForLastPageBlanked, id: \.self) { _ in
HStack(spacing: 0) {
thePool.blankCell.attributedStringForSwiftUIBackports
.contentShape(Rectangle())
.frame(alignment: .topLeading)
Spacer()
}.frame(
minWidth: 0,
maxWidth: .infinity,
alignment: .topLeading
)
Divider()
}
}
}
}
.fixedSize(horizontal: false, vertical: true)
.padding([.horizontal], 5).padding([.top], 5).padding([.bottom], -1)
if controller?.delegate?.showReverseLookupResult ?? true {
ZStack(alignment: .leading) {
Color(white: colorScheme == .dark ? 0.15 : 0.97)
HStack(alignment: .center, spacing: 4) {
Text("")
ForEach(reverseLookupResult, id: \.self) { currentResult in
ZStack(alignment: .center) {
Color(white: colorScheme == .dark ? 0.3 : 0.9).cornerRadius(3)
Text(" \(currentResult.trimmingCharacters(in: .newlines)) ").lineLimit(1)
}.fixedSize()
}
}
.font(.system(size: max(CandidateCellData.unifiedSize * 0.6, 9)))
.padding([.horizontal], 4).padding([.vertical], 4)
.foregroundColor(colorScheme == .light ? Color(white: 0.1) : Color(white: 0.9))
}
}
ZStack(alignment: .trailing) {
if tooltip.isEmpty {
Color(white: colorScheme == .dark ? 0.2 : 0.9)
} else {
Color(white: colorScheme == .dark ? 0.0 : 1)
controller?.highlightedColorUIBackports
}
HStack(alignment: .center) {
if !tooltip.isEmpty {
Text(tooltip).lineLimit(1)
Spacer()
}
Text(positionLabel).lineLimit(1)
}
.font(.system(size: max(CandidateCellData.unifiedSize * 0.7, 11), weight: .bold))
.padding(7).foregroundColor(
tooltip.isEmpty && colorScheme == .light ? Color(white: 0.1) : Color(white: 0.9)
)
}
.fixedSize(horizontal: false, vertical: true)
}
.frame(minWidth: thePool.maxWindowWidth, maxWidth: thePool.maxWindowWidth)
.background(Color(white: colorScheme == .dark ? 0.1 : 1))
.overlay(
RoundedRectangle(cornerRadius: 10).stroke(
Color(white: colorScheme == .dark ? 0 : 1).opacity(colorScheme == .dark ? 1 : 0.1), lineWidth: 0.5
)
)
.cornerRadius(10)
}
}

View File

@ -1,176 +0,0 @@
// (c) 2022 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 Cocoa
import Shared
import SwiftUI
import SwiftUIBackports
// MARK: - Some useless tests
@available(macOS 10.15, *)
struct CandidatePoolViewUIVerticalBackports_Previews: PreviewProvider {
@State static var testCandidates: [String] = [
"二十四歲是學生", "二十四歲", "昏睡紅茶", "食雪漢", "意味深", "學生", "便乗",
"🐂🍺🐂🍺", "🐃🍺", "🐂🍺", "🐃🐂🍺🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺",
"迫真", "驚愕", "論證", "正論", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "", "", "", "便", "",
"", "", "",
]
static var thePool: CandidatePool {
var result = CandidatePool(candidates: testCandidates, columnCapacity: 6, selectionKeys: "123456789")
// 使
result.highlight(at: 5)
return result
}
static var previews: some View {
VwrCandidateVerticalBackports(controller: nil, thePool: thePool).fixedSize()
}
}
@available(macOS 10.15, *)
public struct VwrCandidateVerticalBackports: View {
public weak var controller: CtlCandidateTDK?
@Environment(\.colorScheme) var colorScheme
@State public var thePool: CandidatePool
@State public var tooltip: String = ""
@State public var reverseLookupResult: [String] = []
private var positionLabel: String {
(thePool.highlightedIndex + 1).description + "/" + thePool.candidateDataAll.count.description
}
private func didSelectCandidateAt(_ pos: Int) {
if let delegate = controller?.delegate {
delegate.candidatePairSelected(at: pos)
}
}
private func didRightClickCandidateAt(_ pos: Int, action: CandidateContextMenuAction) {
if let delegate = controller?.delegate {
delegate.candidatePairRightClicked(at: pos, action: action)
}
}
public var body: some View {
VStack(alignment: .leading, spacing: 0) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 10) {
ForEach(Array(thePool.rangeForCurrentPage.enumerated()), id: \.offset) { loopIndex, columnIndex in
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(thePool.candidateLines[columnIndex]), id: \.self) { currentCandidate in
HStack(spacing: 0) {
currentCandidate.attributedStringForSwiftUIBackports.fixedSize(horizontal: false, vertical: true)
.frame(
maxWidth: .infinity,
alignment: .topLeading
)
.contentShape(Rectangle())
.onTapGesture { didSelectCandidateAt(currentCandidate.index) }
.contextMenu {
if controller?.delegate?.isCandidateContextMenuEnabled ?? false {
Button {
didRightClickCandidateAt(currentCandidate.index, action: .toBoost)
} label: {
Text("" + currentCandidate.displayedText)
}
Button {
didRightClickCandidateAt(currentCandidate.index, action: .toNerf)
} label: {
Text("" + currentCandidate.displayedText)
}
Button {
didRightClickCandidateAt(currentCandidate.index, action: .toFilter)
} label: {
Text("✖︎ " + currentCandidate.displayedText)
}
}
}
}
}
}.frame(
minWidth: Double(CandidateCellData.unifiedSize * 5),
alignment: .topLeading
).id(columnIndex)
if loopIndex < thePool.maxLinesPerPage - 1 {
Divider()
} else if thePool.maxLinesPerPage > 1 {
Spacer(minLength: 0)
}
}
if thePool.maxLinesPerPage - thePool.rangeForCurrentPage.count > 0 {
ForEach(Array(thePool.rangeForLastPageBlanked.enumerated()), id: \.offset) { loopIndex, _ in
VStack(alignment: .leading, spacing: 0) {
ForEach(0 ..< thePool.maxLineCapacity, id: \.self) { _ in
thePool.blankCell.attributedStringForSwiftUIBackports.fixedSize()
.frame(width: Double(CandidateCellData.unifiedSize * 5), alignment: .topLeading)
.contentShape(Rectangle())
}
}.frame(
minWidth: 0,
maxWidth: .infinity,
alignment: .topLeading
)
if loopIndex < thePool.maxLinesPerPage - thePool.rangeForCurrentPage.count - 1 {
Divider()
} else if thePool.maxLinesPerPage > 1 {
Spacer(minLength: 0)
}
}
}
}
}
.fixedSize(horizontal: true, vertical: false).padding(5)
if controller?.delegate?.showReverseLookupResult ?? true {
ZStack(alignment: .leading) {
Color(white: colorScheme == .dark ? 0.15 : 0.97)
HStack(alignment: .center, spacing: 4) {
Text("")
ForEach(reverseLookupResult, id: \.self) { currentResult in
ZStack(alignment: .center) {
Color(white: colorScheme == .dark ? 0.3 : 0.9).cornerRadius(3)
Text(" \(currentResult.trimmingCharacters(in: .newlines)) ").lineLimit(1)
}.fixedSize()
}
}
.font(.system(size: max(CandidateCellData.unifiedSize * 0.6, 9)))
.padding([.horizontal], 4).padding([.vertical], 4)
.foregroundColor(colorScheme == .light ? Color(white: 0.1) : Color(white: 0.9))
}
}
ZStack(alignment: .trailing) {
if tooltip.isEmpty {
Color(white: colorScheme == .dark ? 0.2 : 0.9)
} else {
Color(white: colorScheme == .dark ? 0.0 : 1)
controller?.highlightedColorUIBackports
}
HStack(alignment: .center) {
if !tooltip.isEmpty {
Text(tooltip).lineLimit(1)
Spacer()
}
Text(positionLabel).lineLimit(1)
}
.font(.system(size: max(CandidateCellData.unifiedSize * 0.7, 11), weight: .bold))
.padding(7).foregroundColor(
tooltip.isEmpty && colorScheme == .light ? Color(white: 0.1) : Color(white: 0.9)
)
}
.fixedSize(horizontal: false, vertical: true)
}
.background(Color(white: colorScheme == .dark ? 0.1 : 1))
.overlay(
RoundedRectangle(cornerRadius: 10).stroke(
Color(white: colorScheme == .dark ? 0 : 1).opacity(colorScheme == .dark ? 1 : 0.1), lineWidth: 0.5
)
)
.cornerRadius(10)
}
}

View File

@ -20,7 +20,7 @@ final class CandidatePoolTests: XCTestCase {
] ]
func testPoolHorizontal() throws { func testPoolHorizontal() throws {
let pool = CandidatePool(candidates: testCandidates, rowCapacity: 6) let pool = CandidatePool(candidates: testCandidates, selectionKeys: "123456", layout: .horizontal)
var strOutput = "" var strOutput = ""
pool.candidateLines.forEach { pool.candidateLines.forEach {
$0.forEach { $0.forEach {
@ -33,7 +33,7 @@ final class CandidatePoolTests: XCTestCase {
} }
func testPoolVertical() throws { func testPoolVertical() throws {
let pool = CandidatePool(candidates: testCandidates, columnCapacity: 6) let pool = CandidatePool(candidates: testCandidates, selectionKeys: "123456", layout: .vertical)
var strOutput = "" var strOutput = ""
pool.candidateLines.forEach { pool.candidateLines.forEach {
$0.forEach { $0.forEach {

View File

@ -14,7 +14,6 @@ public extension PrefMgr {
func fixOddPreferences() { func fixOddPreferences() {
// macOS 10.15 使 SwiftUI // macOS 10.15 使 SwiftUI
if #unavailable(macOS 10.15) { if #unavailable(macOS 10.15) {
useIMKCandidateWindow = true
legacyCandidateViewTypesettingMethodEnabled = false legacyCandidateViewTypesettingMethodEnabled = false
togglingAlphanumericalModeWithRShift = false togglingAlphanumericalModeWithRShift = false
togglingAlphanumericalModeWithLShift = false togglingAlphanumericalModeWithLShift = false

View File

@ -86,15 +86,15 @@ public extension SessionCtl {
/// NSWindow() /// NSWindow()
candidateUI?.visible = false candidateUI?.visible = false
/// ///
if #available(macOS 10.15, *) { if #available(macOS 10.13, *) {
candidateUI = candidateUI =
PrefMgr.shared.useIMKCandidateWindow PrefMgr.shared.useIMKCandidateWindow
? CtlCandidateIMK(candidateLayout) : CtlCandidateTDK(candidateLayout) ? CtlCandidateIMK(candidateLayout) : CtlCandidateTDK(candidateLayout)
if let candidateTDK = candidateUI as? CtlCandidateTDK { if let candidateTDK = candidateUI as? CtlCandidateTDK {
candidateTDK.maxLinesPerPage = isVerticalTyping ? 1 : 3 candidateTDK.maxLinesPerPage = isVerticalTyping ? 1 : 4
} }
} else { } else {
candidateUI = CtlCandidateIMK(candidateLayout) candidateUI = CtlCandidateTDK(candidateLayout)
} }
candidateUI?.candidateFont = Self.candidateFont( candidateUI?.candidateFont = Self.candidateFont(