TDKCandidates // Removing SwiftUI support for now.
- SwiftUI is not suitable for necessities like writing a performance-critical candidate window.
This commit is contained in:
parent
1e077faf03
commit
4eacbf6f8c
|
@ -9,8 +9,6 @@
|
||||||
import AppKit
|
import AppKit
|
||||||
import CocoaExtension
|
import CocoaExtension
|
||||||
import Shared
|
import Shared
|
||||||
import SwiftExtension
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
private extension NSUserInterfaceLayoutOrientation {
|
private extension NSUserInterfaceLayoutOrientation {
|
||||||
var layoutTDK: CandidatePool.LayoutOrientation {
|
var layoutTDK: CandidatePool.LayoutOrientation {
|
||||||
|
@ -38,20 +36,6 @@ public class CtlCandidateTDK: CtlCandidate, NSWindowDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(macOS 10.15, *)
|
|
||||||
private var theView: some View {
|
|
||||||
VwrCandidateTDK(
|
|
||||||
controller: self, thePool: Self.thePool
|
|
||||||
).edgesIgnoringSafeArea(.top)
|
|
||||||
}
|
|
||||||
|
|
||||||
#if USING_STACK_VIEW_IN_TDK_COCOA
|
|
||||||
/// 該視圖模式因算法陳舊而不再維護。
|
|
||||||
private var theViewCocoa: NSStackView {
|
|
||||||
VwrCandidateTDKCocoa(controller: self, thePool: Self.thePool)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private var theViewAppKit: NSView {
|
private var theViewAppKit: NSView {
|
||||||
VwrCandidateTDKAppKit(controller: self, thePool: Self.thePool)
|
VwrCandidateTDKAppKit(controller: self, thePool: Self.thePool)
|
||||||
}
|
}
|
||||||
|
@ -121,47 +105,19 @@ public class CtlCandidateTDK: CtlCandidate, NSWindowDelegate {
|
||||||
delegate?.candidatePairHighlightChanged(at: highlightedIndex)
|
delegate?.candidatePairHighlightChanged(at: highlightedIndex)
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
updateNSWindowModern(window)
|
self.updateNSWindowModern(window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateNSWindowModern(_ window: NSWindow) {
|
func updateNSWindowModern(_ window: NSWindow) {
|
||||||
|
Self.currentView = theViewAppKit
|
||||||
window.isOpaque = false
|
window.isOpaque = false
|
||||||
window.backgroundColor = .clear
|
window.backgroundColor = .clear
|
||||||
viewCheck: do {
|
|
||||||
viewCheckCatalina: if #available(macOS 10.15, *) {
|
|
||||||
if useCocoa { break viewCheckCatalina }
|
|
||||||
Self.thePool.update()
|
|
||||||
Self.currentView = NSHostingView(rootView: theView)
|
|
||||||
break viewCheck
|
|
||||||
}
|
|
||||||
Self.currentView = theViewAppKit
|
|
||||||
}
|
|
||||||
window.contentView = Self.currentView
|
window.contentView = Self.currentView
|
||||||
window.setContentSize(Self.currentView.fittingSize)
|
window.setContentSize(Self.currentView.fittingSize)
|
||||||
delegate?.resetCandidateWindowOrigin()
|
delegate?.resetCandidateWindowOrigin()
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
delegate?.resetCandidateWindowOrigin()
|
|
||||||
}
|
|
||||||
|
|
||||||
override public func scrollWheel(with event: NSEvent) {
|
override public func scrollWheel(with event: NSEvent) {
|
||||||
guard useMouseScrolling else { return }
|
guard useMouseScrolling else { return }
|
||||||
handleMouseScroll(deltaX: event.deltaX, deltaY: event.deltaY)
|
handleMouseScroll(deltaX: event.deltaX, deltaY: event.deltaY)
|
||||||
|
|
|
@ -1,421 +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.
|
|
||||||
|
|
||||||
#if USING_STACK_VIEW_IN_TDK_COCOA
|
|
||||||
|
|
||||||
import AppKit
|
|
||||||
import Shared
|
|
||||||
|
|
||||||
/// 田所選字窗的 Cocoa 版本,繪製效率不受 SwiftUI 的限制。
|
|
||||||
public class VwrCandidateTDKCocoa: NSStackView {
|
|
||||||
public weak var controller: CtlCandidateTDK?
|
|
||||||
public var thePool: CandidatePool
|
|
||||||
private var lineDimension: CGSize = .zero
|
|
||||||
private var candidateAreaDimension: CGSize = .zero
|
|
||||||
|
|
||||||
// MARK: - Constructors.
|
|
||||||
|
|
||||||
public init(controller: CtlCandidateTDK? = nil, thePool pool: CandidatePool) {
|
|
||||||
self.controller = controller
|
|
||||||
thePool = pool
|
|
||||||
super.init(frame: .init(origin: .zero, size: .init(width: 114_514, height: 114_514)))
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(*, unavailable)
|
|
||||||
required init?(coder _: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Interface Renderer.
|
|
||||||
|
|
||||||
public extension VwrCandidateTDKCocoa {
|
|
||||||
func refresh() {
|
|
||||||
defer {
|
|
||||||
vCLog(Self.strForConstraintStatistics.description)
|
|
||||||
Self.strForConstraintStatistics = .init()
|
|
||||||
}
|
|
||||||
// 用來登記全部的行容器,方便在收尾階段統一設定 constraints。
|
|
||||||
var arrStackViewsOfLines = [NSStackView]()
|
|
||||||
// 清理兩個計數器。
|
|
||||||
lineDimension = .zero
|
|
||||||
candidateAreaDimension = .zero
|
|
||||||
// 容器自身美化。
|
|
||||||
edgeInsets = .init(top: 5, left: 5, bottom: 5, right: 5)
|
|
||||||
wantsLayer = true
|
|
||||||
layer?.backgroundColor = candidateListBackground.cgColor
|
|
||||||
layer?.cornerRadius = 10
|
|
||||||
// 現在開始準備容器內容。
|
|
||||||
let isVerticalListing: Bool = thePool.layout == .vertical
|
|
||||||
let candidateContainer = NSStackView()
|
|
||||||
// 這是行陳列方向,不是候選字詞陳列方向。
|
|
||||||
candidateContainer.orientation = isVerticalListing ? .horizontal : .vertical
|
|
||||||
candidateContainer.alignment = isVerticalListing ? .top : .leading
|
|
||||||
candidateContainer.spacing = 0
|
|
||||||
candidateContainer.setHuggingPriority(.fittingSizeCompression, for: .horizontal)
|
|
||||||
candidateContainer.setHuggingPriority(.fittingSizeCompression, for: .vertical)
|
|
||||||
for lineID in thePool.lineRangeForCurrentPage {
|
|
||||||
var theLine = thePool.candidateLines[lineID]
|
|
||||||
let vwrCurrentLine = generateLineContainer(&theLine)
|
|
||||||
candidateContainer.addView(vwrCurrentLine, in: isVerticalListing ? .top : .leading)
|
|
||||||
arrStackViewsOfLines.append(vwrCurrentLine)
|
|
||||||
}
|
|
||||||
if thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count > 0 {
|
|
||||||
thePool.lineRangeForFinalPageBlanked.enumerated().forEach { _ in
|
|
||||||
var theLine = [CandidateCellData]()
|
|
||||||
let copied = CandidatePool.blankCell.cleanCopy
|
|
||||||
for _ in 0 ..< thePool.maxLineCapacity {
|
|
||||||
theLine.append(copied)
|
|
||||||
}
|
|
||||||
let vwrCurrentLine = generateLineContainer(&theLine)
|
|
||||||
candidateContainer.addView(vwrCurrentLine, in: isVerticalListing ? .top : .leading)
|
|
||||||
arrStackViewsOfLines.append(vwrCurrentLine)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 處理行寬或列高。
|
|
||||||
switch thePool.layout {
|
|
||||||
case .vertical:
|
|
||||||
var accumulatedWidth: CGFloat = 0
|
|
||||||
var lines = [[CandidateCellData]]()
|
|
||||||
thePool.lineRangeForCurrentPage.forEach { lines.append(thePool.candidateLines[$0]) }
|
|
||||||
arrStackViewsOfLines.enumerated().forEach { viewLineID, vwrCurrentLine in
|
|
||||||
var columnWidth: CGFloat = 0
|
|
||||||
if (0 ..< lines.count).contains(viewLineID), !lines.isEmpty {
|
|
||||||
let line = Array(lines[viewLineID])
|
|
||||||
columnWidth = line.map(\.visualDimension.width).max() ?? lineDimension.width
|
|
||||||
} else {
|
|
||||||
columnWidth = CandidatePool.blankCell.visualDimension.width
|
|
||||||
}
|
|
||||||
accumulatedWidth += columnWidth
|
|
||||||
Self.makeSimpleConstraint(item: vwrCurrentLine, attribute: .width, relation: .equal, value: columnWidth)
|
|
||||||
Self.makeSimpleConstraint(item: vwrCurrentLine, attribute: .height, relation: .equal, value: lineDimension.height)
|
|
||||||
Self.addStatistics(vwrCurrentLine, memo: "vwrCurrentLine")
|
|
||||||
}
|
|
||||||
candidateAreaDimension.width = accumulatedWidth
|
|
||||||
candidateAreaDimension.height = lineDimension.height
|
|
||||||
case .horizontal:
|
|
||||||
arrStackViewsOfLines.forEach { vwrCurrentLine in
|
|
||||||
Self.makeSimpleConstraint(item: vwrCurrentLine, attribute: .width, relation: .equal, value: lineDimension.width)
|
|
||||||
Self.makeSimpleConstraint(item: vwrCurrentLine, attribute: .height, relation: .equal, value: lineDimension.height)
|
|
||||||
Self.addStatistics(vwrCurrentLine, memo: "vwrCurrentLine")
|
|
||||||
}
|
|
||||||
candidateAreaDimension.width = lineDimension.width
|
|
||||||
candidateAreaDimension.height = lineDimension.height * Double(thePool.maxLinesPerPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
Self.makeSimpleConstraint(item: candidateContainer, attribute: .width, relation: .equal, value: candidateAreaDimension.width)
|
|
||||||
Self.makeSimpleConstraint(item: candidateContainer, attribute: .height, relation: .equal, value: candidateAreaDimension.height)
|
|
||||||
Self.addStatistics(candidateContainer, memo: "candidateContainer")
|
|
||||||
|
|
||||||
let vwrPeripherals = Self.makeLabel(thePool.attributedDescriptionBottomPanes)
|
|
||||||
Self.makeSimpleConstraint(
|
|
||||||
item: vwrPeripherals, attribute: .height, relation: .greaterThanOrEqual,
|
|
||||||
value: vwrPeripherals.fittingSize.height
|
|
||||||
)
|
|
||||||
Self.makeSimpleConstraint(
|
|
||||||
item: vwrPeripherals, attribute: .width, relation: .greaterThanOrEqual,
|
|
||||||
value: vwrPeripherals.fittingSize.width
|
|
||||||
)
|
|
||||||
|
|
||||||
// 組裝。
|
|
||||||
let finalContainer = NSStackView()
|
|
||||||
let finalContainerOrientation: NSUserInterfaceLayoutOrientation = {
|
|
||||||
if thePool.maxLinesPerPage == 1, thePool.layout == .horizontal { return .horizontal }
|
|
||||||
return .vertical
|
|
||||||
}()
|
|
||||||
|
|
||||||
if finalContainerOrientation == .horizontal {
|
|
||||||
let vwrPeripheralMinWidth = vwrPeripherals.fittingSize.width + 3
|
|
||||||
Self.makeSimpleConstraint(item: vwrPeripherals, attribute: .width, relation: .greaterThanOrEqual, value: vwrPeripheralMinWidth)
|
|
||||||
finalContainer.spacing = 5
|
|
||||||
} else {
|
|
||||||
finalContainer.spacing = 2
|
|
||||||
Self.makeSimpleConstraint(item: vwrPeripherals, attribute: .width, relation: .greaterThanOrEqual, value: vwrPeripherals.fittingSize.width)
|
|
||||||
}
|
|
||||||
Self.addStatistics(vwrPeripherals, memo: "vwrPeripherals")
|
|
||||||
|
|
||||||
finalContainer.orientation = finalContainerOrientation
|
|
||||||
finalContainer.alignment = finalContainerOrientation == .vertical ? .leading : .centerY
|
|
||||||
finalContainer.addView(candidateContainer, in: .leading)
|
|
||||||
finalContainer.addView(vwrPeripherals, in: .leading)
|
|
||||||
Self.makeSimpleConstraint(
|
|
||||||
item: finalContainer, attribute: .width,
|
|
||||||
relation: .equal, value: finalContainer.fittingSize.width
|
|
||||||
)
|
|
||||||
Self.makeSimpleConstraint(
|
|
||||||
item: finalContainer, attribute: .height,
|
|
||||||
relation: .equal, value: finalContainer.fittingSize.height
|
|
||||||
)
|
|
||||||
Self.addStatistics(finalContainer, memo: "finalContainer")
|
|
||||||
|
|
||||||
// 更換容器內容為上文生成的新內容。
|
|
||||||
subviews.forEach { removeView($0) }
|
|
||||||
addView(finalContainer, in: .top)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Interface Components.
|
|
||||||
|
|
||||||
private extension VwrCandidateTDKCocoa {
|
|
||||||
private var candidateListBackground: NSColor {
|
|
||||||
let delta = NSApplication.isDarkMode ? 0.05 : 0.99
|
|
||||||
return .init(white: delta, alpha: 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func drawCellCocoa(_ theCell: CandidateCellData? = nil) -> NSView {
|
|
||||||
let theCell = theCell ?? CandidatePool.blankCell.cleanCopy
|
|
||||||
let cellLabel = VwrCandidateCell(cell: theCell)
|
|
||||||
cellLabel.target = self
|
|
||||||
Self.makeSimpleConstraint(item: cellLabel, attribute: .width, relation: .equal, value: cellLabel.fittingSize.width)
|
|
||||||
Self.makeSimpleConstraint(item: cellLabel, attribute: .height, relation: .equal, value: cellLabel.fittingSize.height)
|
|
||||||
Self.addStatistics(cellLabel, memo: "cellLabel")
|
|
||||||
let wrappedCell = NSStackView()
|
|
||||||
let padding: CGFloat = 3
|
|
||||||
wrappedCell.edgeInsets = .init(top: padding, left: padding, bottom: padding, right: padding)
|
|
||||||
wrappedCell.addView(cellLabel, in: .leading)
|
|
||||||
if theCell.isHighlighted {
|
|
||||||
wrappedCell.wantsLayer = true
|
|
||||||
wrappedCell.layer?.backgroundColor = theCell.themeColorCocoa.cgColor
|
|
||||||
wrappedCell.layer?.cornerRadius = padding * 2
|
|
||||||
}
|
|
||||||
let cellWidth = max(thePool.cellWidth(theCell).min ?? wrappedCell.fittingSize.width, wrappedCell.fittingSize.width)
|
|
||||||
let cellHeight = wrappedCell.fittingSize.height
|
|
||||||
wrappedCell.setHuggingPriority(.fittingSizeCompression, for: .horizontal)
|
|
||||||
wrappedCell.setHuggingPriority(.fittingSizeCompression, for: .vertical)
|
|
||||||
Self.makeSimpleConstraint(item: wrappedCell, attribute: .height, relation: .equal, value: cellHeight)
|
|
||||||
switch thePool.layout {
|
|
||||||
case .horizontal where thePool.isMatrix:
|
|
||||||
Self.makeSimpleConstraint(item: wrappedCell, attribute: .width, relation: .equal, value: cellWidth)
|
|
||||||
default:
|
|
||||||
Self.makeSimpleConstraint(item: wrappedCell, attribute: .width, relation: .greaterThanOrEqual, value: cellWidth)
|
|
||||||
}
|
|
||||||
Self.addStatistics(wrappedCell, memo: "wrappedCell")
|
|
||||||
theCell.visualDimension = .init(width: cellWidth, height: cellHeight)
|
|
||||||
return wrappedCell
|
|
||||||
}
|
|
||||||
|
|
||||||
private func lineBackground(isCurrentLine: Bool, isMatrix: Bool) -> NSColor {
|
|
||||||
if !isCurrentLine { return .clear }
|
|
||||||
let absBg: NSColor = NSApplication.isDarkMode ? .black : .white
|
|
||||||
switch thePool.layout {
|
|
||||||
case .horizontal where isMatrix:
|
|
||||||
return NSApplication.isDarkMode ? .controlTextColor.withAlphaComponent(0.05) : .white
|
|
||||||
case .vertical where isMatrix:
|
|
||||||
return absBg.withAlphaComponent(0.9)
|
|
||||||
default:
|
|
||||||
return .clear
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateLineContainer(_ theLine: inout [CandidateCellData]) -> NSStackView {
|
|
||||||
let isVerticalListing: Bool = thePool.layout == .vertical
|
|
||||||
let isMatrix = thePool.isMatrix
|
|
||||||
let vwrCurrentLine = NSStackView()
|
|
||||||
vwrCurrentLine.spacing = 0
|
|
||||||
vwrCurrentLine.orientation = isVerticalListing ? .vertical : .horizontal
|
|
||||||
var cellHeight = 0.0
|
|
||||||
var lineSize: CGSize = .zero
|
|
||||||
let isCurrentLine = theLine.hasHighlightedCell
|
|
||||||
theLine.forEach { theCell in
|
|
||||||
vwrCurrentLine.addView(drawCellCocoa(theCell), in: isVerticalListing ? .top : .leading)
|
|
||||||
switch thePool.layout {
|
|
||||||
case .horizontal:
|
|
||||||
lineSize.width += theCell.visualDimension.width
|
|
||||||
lineSize.height = max(lineSize.height, theCell.visualDimension.height)
|
|
||||||
case .vertical:
|
|
||||||
lineSize.width = max(lineSize.width, theCell.visualDimension.width)
|
|
||||||
lineSize.height += theCell.visualDimension.height
|
|
||||||
}
|
|
||||||
cellHeight = max(theCell.visualDimension.height, cellHeight)
|
|
||||||
}
|
|
||||||
let lineBg = lineBackground(isCurrentLine: isCurrentLine, isMatrix: isMatrix)
|
|
||||||
vwrCurrentLine.wantsLayer = isCurrentLine && isMatrix
|
|
||||||
if vwrCurrentLine.wantsLayer {
|
|
||||||
vwrCurrentLine.layer?.backgroundColor = lineBg.cgColor
|
|
||||||
vwrCurrentLine.layer?.cornerRadius = 6
|
|
||||||
}
|
|
||||||
vwrCurrentLine.alphaValue = isCurrentLine ? 1 : 0.85
|
|
||||||
lineDimension.width = max(lineSize.width, lineDimension.width)
|
|
||||||
switch thePool.layout {
|
|
||||||
case .horizontal: lineDimension.height = max(lineSize.height, lineDimension.height)
|
|
||||||
case .vertical: lineDimension.height = cellHeight * Double(thePool.maxLineCapacity)
|
|
||||||
}
|
|
||||||
return vwrCurrentLine
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func makeLabel(_ attrStr: NSAttributedString) -> NSTextField {
|
|
||||||
let textField = NSTextField()
|
|
||||||
textField.isSelectable = false
|
|
||||||
textField.isEditable = false
|
|
||||||
textField.isBordered = false
|
|
||||||
textField.backgroundColor = .clear
|
|
||||||
textField.allowsEditingTextAttributes = false
|
|
||||||
textField.preferredMaxLayoutWidth = textField.frame.width
|
|
||||||
textField.attributedStringValue = attrStr
|
|
||||||
textField.sizeToFit()
|
|
||||||
return textField
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Constraint Utilities
|
|
||||||
|
|
||||||
private extension VwrCandidateTDKCocoa {
|
|
||||||
static var strForConstraintStatistics = NSMutableString(string: "TDKCandidates Dimensions (Debug):\n")
|
|
||||||
|
|
||||||
static func addStatistics(_ target: NSView, memo: String = "") {
|
|
||||||
if Self.strForConstraintStatistics.length == 0 {
|
|
||||||
Self.strForConstraintStatistics.append("TDKCandidates Dimensions (Debug):\n")
|
|
||||||
}
|
|
||||||
Self.strForConstraintStatistics.append("\(target.fittingSize) \(memo)\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
static func makeSimpleConstraint(item: NSView, attribute: NSLayoutConstraint.Attribute, relation: NSLayoutConstraint.Relation, value: CGFloat) {
|
|
||||||
item.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
let widthConstraint = NSLayoutConstraint(
|
|
||||||
item: item, attribute: attribute, relatedBy: relation, toItem: nil,
|
|
||||||
attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: value
|
|
||||||
)
|
|
||||||
item.addConstraint(widthConstraint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Candidate Cell View
|
|
||||||
|
|
||||||
private extension VwrCandidateTDKCocoa {
|
|
||||||
class VwrCandidateCell: NSTextField {
|
|
||||||
public var cellData: CandidateCellData
|
|
||||||
public init(cell: CandidateCellData) {
|
|
||||||
cellData = cell
|
|
||||||
super.init(frame: .init(origin: .zero, size: .init(width: 114_514, height: 114_514)))
|
|
||||||
isSelectable = false
|
|
||||||
isEditable = false
|
|
||||||
isBordered = false
|
|
||||||
backgroundColor = .clear
|
|
||||||
allowsEditingTextAttributes = false
|
|
||||||
preferredMaxLayoutWidth = frame.width
|
|
||||||
attributedStringValue = cellData.attributedString()
|
|
||||||
sizeToFit()
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(*, unavailable)
|
|
||||||
required init?(coder _: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
// TODO: This doesn't work at all. (#TDKError_NSMenuDeconstruction)
|
|
||||||
theMenu?.cancelTrackingWithoutAnimation()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Mouse Actions.
|
|
||||||
|
|
||||||
override func mouseUp(with _: NSEvent) {
|
|
||||||
guard let target = target as? VwrCandidateTDKCocoa else { return }
|
|
||||||
target.didSelectCandidateAt(cellData.index)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func rightMouseUp(with event: NSEvent) {
|
|
||||||
guard let target = target as? VwrCandidateTDKCocoa else { return }
|
|
||||||
let index = cellData.index
|
|
||||||
let candidateText = cellData.displayedText
|
|
||||||
let isEnabled: Bool = target.controller?.delegate?.isCandidateContextMenuEnabled ?? false
|
|
||||||
guard isEnabled, !candidateText.isEmpty, index >= 0 else { return }
|
|
||||||
prepareMenu()
|
|
||||||
theMenu?.popUp(positioning: nil, at: event.locationInWindow, in: target)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Menu.
|
|
||||||
|
|
||||||
var theMenu: NSMenu?
|
|
||||||
|
|
||||||
private func prepareMenu() {
|
|
||||||
guard let thePool = (target as? VwrCandidateTDKCocoa)?.thePool else { return }
|
|
||||||
let newMenu = NSMenu()
|
|
||||||
let boostMenuItem = NSMenuItem(
|
|
||||||
title: "↑ \(cellData.displayedText)",
|
|
||||||
action: #selector(menuActionOfBoosting(_:)),
|
|
||||||
keyEquivalent: ""
|
|
||||||
)
|
|
||||||
boostMenuItem.target = self
|
|
||||||
newMenu.addItem(boostMenuItem)
|
|
||||||
|
|
||||||
let nerfMenuItem = NSMenuItem(
|
|
||||||
title: "↓ \(cellData.displayedText)",
|
|
||||||
action: #selector(menuActionOfNerfing(_:)),
|
|
||||||
keyEquivalent: ""
|
|
||||||
)
|
|
||||||
nerfMenuItem.target = self
|
|
||||||
newMenu.addItem(nerfMenuItem)
|
|
||||||
|
|
||||||
if thePool.isFilterable(target: cellData.index) {
|
|
||||||
let filterMenuItem = NSMenuItem(
|
|
||||||
title: "✖︎ \(cellData.displayedText)",
|
|
||||||
action: #selector(menuActionOfFiltering(_:)),
|
|
||||||
keyEquivalent: ""
|
|
||||||
)
|
|
||||||
filterMenuItem.target = self
|
|
||||||
newMenu.addItem(filterMenuItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
theMenu = newMenu
|
|
||||||
CtlCandidateTDK.currentMenu = newMenu
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func menuActionOfBoosting(_: Any? = nil) {
|
|
||||||
guard let target = target as? VwrCandidateTDKCocoa else { return }
|
|
||||||
target.didRightClickCandidateAt(cellData.index, action: .toBoost)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func menuActionOfNerfing(_: Any? = nil) {
|
|
||||||
guard let target = target as? VwrCandidateTDKCocoa else { return }
|
|
||||||
target.didRightClickCandidateAt(cellData.index, action: .toNerf)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func menuActionOfFiltering(_: Any? = nil) {
|
|
||||||
guard let target = target as? VwrCandidateTDKCocoa else { return }
|
|
||||||
target.didRightClickCandidateAt(cellData.index, action: .toFilter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Delegate Methods
|
|
||||||
|
|
||||||
private extension VwrCandidateTDKCocoa {
|
|
||||||
func didSelectCandidateAt(_ pos: Int) {
|
|
||||||
controller?.delegate?.candidatePairSelectionConfirmed(at: pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
func didRightClickCandidateAt(_ pos: Int, action: CandidateContextMenuAction) {
|
|
||||||
controller?.delegate?.candidatePairRightClicked(at: pos, action: action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Debug Module Using Swift UI.
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
@available(macOS 10.15, *)
|
|
||||||
public struct VwrCandidateTDKCocoaForSwiftUI: NSViewRepresentable {
|
|
||||||
public weak var controller: CtlCandidateTDK?
|
|
||||||
public var thePool: CandidatePool
|
|
||||||
|
|
||||||
public func makeNSView(context _: Context) -> VwrCandidateTDKCocoa {
|
|
||||||
let nsView = VwrCandidateTDKCocoa(thePool: thePool)
|
|
||||||
nsView.controller = controller
|
|
||||||
return nsView
|
|
||||||
}
|
|
||||||
|
|
||||||
public func updateNSView(_ nsView: VwrCandidateTDKCocoa, context _: Context) {
|
|
||||||
nsView.thePool = thePool
|
|
||||||
nsView.refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
|
@ -1,512 +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 AppKit
|
|
||||||
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
|
|
||||||
@Backport.StateObject 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(spacing: 0) {
|
|
||||||
mainViewHorizontal
|
|
||||||
if thePool.maxLinesPerPage == 1 {
|
|
||||||
rightPanes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
mainViewVertical.background(candidateListBackground)
|
|
||||||
}
|
|
||||||
if thePool.isMatrix || thePool.layout == .vertical {
|
|
||||||
statusBarContent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.fixedSize()
|
|
||||||
.background(candidateListBackground)
|
|
||||||
.cornerRadius(10)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Main Views.
|
|
||||||
|
|
||||||
@available(macOS 10.15, *)
|
|
||||||
private extension VwrCandidateTDK {
|
|
||||||
var mainViewHorizontal: some View {
|
|
||||||
Group {
|
|
||||||
VStack(alignment: .leading, spacing: 1.6) {
|
|
||||||
ForEach(thePool.lineRangeForCurrentPage, id: \.self) { rowIndex in
|
|
||||||
ZStack(alignment: .leading) {
|
|
||||||
lineBackground(lineID: rowIndex).cornerRadius(6).frame(minWidth: minLineWidth)
|
|
||||||
HStack(spacing: horizontalCellSpacing) {
|
|
||||||
ForEach(Array(thePool.candidateLines[rowIndex]), id: \.self) { currentCandidate in
|
|
||||||
drawCandidate(currentCandidate).fixedSize()
|
|
||||||
}
|
|
||||||
.opacity(rowIndex == thePool.currentLineNumber ? 1 : 0.85)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.id(rowIndex)
|
|
||||||
}
|
|
||||||
if thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count > 0 {
|
|
||||||
let copied = CandidatePool.blankCell.cleanCopy
|
|
||||||
ForEach(thePool.lineRangeForFinalPageBlanked, id: \.self) { _ in
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
attributedStringFor(cell: copied)
|
|
||||||
.frame(alignment: .topLeading)
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
Spacer()
|
|
||||||
}.frame(
|
|
||||||
minWidth: 0,
|
|
||||||
maxWidth: thePool.maxLinesPerPage != 1 ? .infinity : nil,
|
|
||||||
alignment: .topLeading
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.fixedSize()
|
|
||||||
.padding([.horizontal, .top], 5)
|
|
||||||
.padding([.bottom], thePool.maxLinesPerPage == 1 ? 5 : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
var mainViewVertical: some View {
|
|
||||||
Group {
|
|
||||||
HStack(alignment: .top, spacing: 4) {
|
|
||||||
ForEach(Array(thePool.lineRangeForCurrentPage.enumerated()), id: \.offset) { _, columnIndex in
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
ForEach(Array(thePool.candidateLines[columnIndex]), id: \.self) { currentCandidate in
|
|
||||||
drawCandidate(currentCandidate)
|
|
||||||
}
|
|
||||||
.opacity(columnIndex == thePool.currentLineNumber ? 1 : 0.85)
|
|
||||||
if thePool.candidateLines[columnIndex].count < thePool.maxLineCapacity {
|
|
||||||
let copied = CandidatePool.blankCell.cleanCopy
|
|
||||||
ForEach(0 ..< thePool.dummyCellsRequiredForCurrentLine, id: \.self) { _ in
|
|
||||||
drawCandidate(copied)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background(lineBackground(lineID: columnIndex)).cornerRadius(6)
|
|
||||||
.frame(
|
|
||||||
alignment: .topLeading
|
|
||||||
)
|
|
||||||
.id(columnIndex)
|
|
||||||
}
|
|
||||||
if thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count > 0 {
|
|
||||||
ForEach(Array(thePool.lineRangeForFinalPageBlanked.enumerated()), id: \.offset) { _, _ in
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
let copied = CandidatePool.blankCell.cleanCopy
|
|
||||||
ForEach(0 ..< thePool.maxLineCapacity, id: \.self) { _ in
|
|
||||||
attributedStringFor(cell: copied).fixedSize()
|
|
||||||
.frame(
|
|
||||||
width: ceil(CandidatePool.blankCell.cellLength(isMatrix: true)),
|
|
||||||
alignment: .topLeading
|
|
||||||
)
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
}
|
|
||||||
}.frame(
|
|
||||||
minWidth: 0,
|
|
||||||
maxWidth: .infinity,
|
|
||||||
alignment: .topLeading
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.fixedSize(horizontal: true, vertical: false)
|
|
||||||
.padding([.horizontal, .top], 5)
|
|
||||||
.padding([.bottom], 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Common Components.
|
|
||||||
|
|
||||||
@available(macOS 10.15, *)
|
|
||||||
extension VwrCandidateTDK {
|
|
||||||
func drawCandidate(_ cell: CandidateCellData) -> some View {
|
|
||||||
attributedStringFor(cell: cell)
|
|
||||||
.frame(minWidth: thePool.cellWidth(cell).min, maxWidth: thePool.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)
|
|
||||||
}
|
|
||||||
if thePool.isFilterable(target: cell.index) {
|
|
||||||
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 colorScheme == .dark ? Color.primary.opacity(0.05) : .white
|
|
||||||
case .vertical where isCurrentLineInMatrix:
|
|
||||||
return absoluteBackgroundColor.opacity(0.9)
|
|
||||||
default:
|
|
||||||
return Color.clear
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var minLineWidth: CGFloat? {
|
|
||||||
let spacings: CGFloat = horizontalCellSpacing * Double(thePool.maxLineCapacity - 1)
|
|
||||||
let maxWindowWith: CGFloat
|
|
||||||
= ceil(
|
|
||||||
Double(thePool.maxLineCapacity) * (CandidatePool.blankCell.cellLength())
|
|
||||||
+ spacings
|
|
||||||
)
|
|
||||||
return thePool.layout == .horizontal && thePool.isMatrix ? 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: Any] = [
|
|
||||||
.kern: 0,
|
|
||||||
.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.9))
|
|
||||||
}.fixedSize()
|
|
||||||
}
|
|
||||||
|
|
||||||
var rightPanes: some View {
|
|
||||||
HStack {
|
|
||||||
if !tooltip.isEmpty {
|
|
||||||
ZStack(alignment: .center) {
|
|
||||||
Circle().fill(CandidatePool.blankCell.themeColor.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))
|
|
||||||
}
|
|
||||||
HStack(alignment: .center, spacing: 0) {
|
|
||||||
positionLabelView
|
|
||||||
if controller?.delegate?.showReverseLookupResult ?? true {
|
|
||||||
if !firstReverseLookupResult.isEmpty {
|
|
||||||
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([.leading], 9)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.opacity(0.9)
|
|
||||||
.fixedSize()
|
|
||||||
.padding([.trailing], 12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var reverseLookupPane: some View {
|
|
||||||
HStack(alignment: .center, spacing: 2) {
|
|
||||||
let text = (thePool.maxLinesPerPage == 1) ? firstReverseLookupResult : reverseLookupResult.joined(separator: " ")
|
|
||||||
if !reverseLookupResult.joined().trimmingCharacters(in: .newlines).isEmpty {
|
|
||||||
Text(verbatim: "\(text.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 statusBarContent: some View {
|
|
||||||
HStack(alignment: .center) {
|
|
||||||
positionLabelView
|
|
||||||
if !tooltip.isEmpty {
|
|
||||||
Text(tooltip).lineLimit(1)
|
|
||||||
}
|
|
||||||
if controller?.delegate?.showReverseLookupResult ?? true, !reverseLookupResult.joined().isEmpty {
|
|
||||||
reverseLookupPane.padding(0)
|
|
||||||
}
|
|
||||||
Spacer(minLength: 0)
|
|
||||||
}
|
|
||||||
.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 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.black
|
|
||||||
} else {
|
|
||||||
return Color.white
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, *)
|
|
||||||
private extension VwrCandidateTDK {
|
|
||||||
func didSelectCandidateAt(_ pos: Int) {
|
|
||||||
controller?.delegate?.candidatePairSelectionConfirmed(at: pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
func didRightClickCandidateAt(_ pos: Int, action: CandidateContextMenuAction) {
|
|
||||||
controller?.delegate?.candidatePairRightClicked(at: pos, action: action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Preview
|
|
||||||
|
|
||||||
import SwiftExtension
|
|
||||||
|
|
||||||
@available(macOS 10.15, *)
|
|
||||||
struct VwrCandidateTDK_Previews: PreviewProvider {
|
|
||||||
@State static var testCandidates: [String] = [
|
|
||||||
"二十四歲是學生", "二十四歲", "昏睡紅茶", "食雪漢", "意味深", "學生", "便乗",
|
|
||||||
"🐂🍺🐂🍺", "🐃🍺", "🐂🍺", "🐃🐂🍺🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺",
|
|
||||||
"迫真", "驚愕", "論證", "正論", "惱", "悲", "屑", "食", "雪", "漢", "意", "味",
|
|
||||||
"深", "二", "十", "四", "歲", "是", "學", "生", "昏", "睡", "紅", "茶", "便", "乗",
|
|
||||||
"嗯", "哼", "啊",
|
|
||||||
]
|
|
||||||
@State static var reverseLookupResult = ["mmmmm", "dddd"]
|
|
||||||
@State static var tooltip = "📼"
|
|
||||||
@State static var oldOS: Bool = false
|
|
||||||
|
|
||||||
static var testCandidatesConverted: [(keyArray: [String], value: String)] {
|
|
||||||
testCandidates.map { candidate in
|
|
||||||
let firstValue: [String] = .init(repeating: "", count: candidate.count)
|
|
||||||
return (firstValue, candidate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static var thePoolX: CandidatePool {
|
|
||||||
let result = CandidatePool(
|
|
||||||
candidates: testCandidatesConverted, lines: 4,
|
|
||||||
selectionKeys: "123456", layout: .horizontal
|
|
||||||
)
|
|
||||||
result.reverseLookupResult = Self.reverseLookupResult
|
|
||||||
result.tooltip = Self.tooltip
|
|
||||||
result.highlight(at: 0)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
static var thePoolXS: CandidatePool {
|
|
||||||
let result = CandidatePool(
|
|
||||||
candidates: testCandidatesConverted, lines: 1,
|
|
||||||
selectionKeys: "123456", layout: .horizontal
|
|
||||||
)
|
|
||||||
result.reverseLookupResult = Self.reverseLookupResult
|
|
||||||
result.tooltip = Self.tooltip
|
|
||||||
result.highlight(at: 1)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
static var thePoolY: CandidatePool {
|
|
||||||
let result = CandidatePool(
|
|
||||||
candidates: testCandidatesConverted, lines: 4,
|
|
||||||
selectionKeys: "123456", layout: .vertical
|
|
||||||
)
|
|
||||||
result.reverseLookupResult = Self.reverseLookupResult
|
|
||||||
result.tooltip = Self.tooltip
|
|
||||||
result.flipPage(isBackward: false)
|
|
||||||
result.highlight(at: 2)
|
|
||||||
result.highlight(at: 21)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
static var thePoolYS: CandidatePool {
|
|
||||||
let result = CandidatePool(
|
|
||||||
candidates: testCandidatesConverted, 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
VStack {
|
|
||||||
HStack(alignment: .top) {
|
|
||||||
Text("田所選字窗 CG 模式").bold().font(Font.system(.title))
|
|
||||||
VStack {
|
|
||||||
VwrCandidateTDKAppKitForSwiftUI(controller: nil, thePool: thePoolX).fixedSize()
|
|
||||||
VwrCandidateTDKAppKitForSwiftUI(controller: nil, thePool: thePoolXS).fixedSize()
|
|
||||||
HStack {
|
|
||||||
VwrCandidateTDKAppKitForSwiftUI(controller: nil, thePool: thePoolY).fixedSize()
|
|
||||||
VwrCandidateTDKAppKitForSwiftUI(controller: nil, thePool: thePoolYS).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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#if USING_STACK_VIEW_IN_TDK_COCOA
|
|
||||||
VStack {
|
|
||||||
HStack(alignment: .top) {
|
|
||||||
Text("田所選字窗 Cocoa 模式").bold().font(Font.system(.title))
|
|
||||||
VStack {
|
|
||||||
VwrCandidateTDKCocoaForSwiftUI(controller: nil, thePool: thePoolX).fixedSize()
|
|
||||||
VwrCandidateTDKCocoaForSwiftUI(controller: nil, thePool: thePoolXS).fixedSize()
|
|
||||||
HStack {
|
|
||||||
VwrCandidateTDKCocoaForSwiftUI(controller: nil, thePool: thePoolY).fixedSize()
|
|
||||||
VwrCandidateTDKCocoaForSwiftUI(controller: nil, thePool: thePoolYS).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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue