CtlCandidateTDK // Rewrite.
This commit is contained in:
parent
e4a8f34075
commit
339cfb0ad4
|
@ -8,18 +8,20 @@
|
|||
|
||||
import AppKit
|
||||
import Shared
|
||||
import SwiftUI
|
||||
import SwiftUIBackports
|
||||
|
||||
// MARK: - Candidate Cell
|
||||
|
||||
/// 用來管理選字窗內顯示的候選字的單位。用 class 型別會比較方便一些。
|
||||
public class CandidateCellData: Hashable {
|
||||
public var visualDimension: CGSize = .zero
|
||||
public var visualOrigin: CGPoint = .zero
|
||||
public var locale = ""
|
||||
public static var unifiedSize: Double = 16
|
||||
public static var unifiedCharDimension: Double { ceil(unifiedSize * 1.0125 + 7) }
|
||||
public static var unifiedTextHeight: Double { ceil(unifiedSize * 19 / 16) }
|
||||
public var selectionKey: String
|
||||
public var displayedText: String
|
||||
public let displayedText: String
|
||||
public private(set) var textDimension: NSSize
|
||||
public var spanLength: Int
|
||||
public var size: Double { Self.unifiedSize }
|
||||
public var isHighlighted: Bool = false
|
||||
|
@ -29,7 +31,6 @@ public class CandidateCellData: Hashable {
|
|||
// 該候選字詞在當前行/列內的索引編號
|
||||
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 fontColorCandidate: NSColor { isHighlighted ? .selectedMenuItemTextColor : .controlTextColor }
|
||||
|
@ -64,6 +65,10 @@ public class CandidateCellData: Hashable {
|
|||
self.displayedText = displayedText
|
||||
spanLength = max(spanningLength ?? displayedText.count, 1)
|
||||
isHighlighted = isSelected
|
||||
textDimension = .init(width: ceil(Self.unifiedCharDimension * 1.4), height: Self.unifiedTextHeight)
|
||||
if displayedText.count > 1 {
|
||||
textDimension.width = attributedString().boundingDimension.width
|
||||
}
|
||||
}
|
||||
|
||||
public static func == (lhs: CandidateCellData, rhs: CandidateCellData) -> Bool {
|
||||
|
@ -76,9 +81,9 @@ public class CandidateCellData: Hashable {
|
|||
}
|
||||
|
||||
public func cellLength(isMatrix: Bool = true) -> Double {
|
||||
let minLength = ceil(charGlyphWidth * 2 + size * 1.25)
|
||||
let minLength = ceil(Self.unifiedCharDimension * 2 + size * 1.25)
|
||||
if displayedText.count <= 2, isMatrix { return minLength }
|
||||
return ceil(attributedStringPhrase().boundingDimension.width + charGlyphWidth)
|
||||
return textDimension.width
|
||||
}
|
||||
|
||||
// MARK: - Fonts and NSColors.
|
||||
|
@ -202,6 +207,20 @@ public class CandidateCellData: Hashable {
|
|||
return String(format: "U+%02X %@", $0.value, theName)
|
||||
}
|
||||
}
|
||||
|
||||
public func updateMetrics(pool thePool: CandidatePool, origin currentOrigin: CGPoint) {
|
||||
let padding = thePool.padding
|
||||
var cellDimension = textDimension
|
||||
if let givenWidth = thePool.cellWidth(self).min, displayedText.count <= 2 {
|
||||
cellDimension.width = max(cellDimension.width + 4 * padding, givenWidth)
|
||||
} else {
|
||||
cellDimension.width += 4 * padding
|
||||
}
|
||||
cellDimension.width = ceil(cellDimension.width)
|
||||
cellDimension.height = Self.unifiedTextHeight + 2 * padding
|
||||
visualDimension = cellDimension
|
||||
visualOrigin = currentOrigin
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Array Container Extension.
|
||||
|
|
|
@ -23,12 +23,33 @@ public class CandidatePool {
|
|||
public var reverseLookupResult: [String] = []
|
||||
public private(set) var highlightedIndex: Int = 0
|
||||
public private(set) var currentLineNumber = 0
|
||||
public var metrics: UIMetrics = .allZeroed
|
||||
|
||||
private var recordedLineRangeForCurrentPage: Range<Int>?
|
||||
private var previouslyRecordedLineRangeForPreviousPage: Range<Int>?
|
||||
|
||||
public struct UIMetrics {
|
||||
static var allZeroed: UIMetrics {
|
||||
.init(fittingSize: .zero, highlightedLine: .zero, highlightedCandidate: .zero, peripherals: .zero)
|
||||
}
|
||||
|
||||
let fittingSize: CGSize
|
||||
let highlightedLine: CGRect
|
||||
let highlightedCandidate: CGRect
|
||||
let peripherals: CGRect
|
||||
}
|
||||
|
||||
// MARK: - 動態變數
|
||||
|
||||
public let padding: CGFloat = 2
|
||||
public let originDelta: CGFloat = 5
|
||||
public let cellTextHeight = CandidatePool.shitCell.textDimension.height
|
||||
public let cellRadius: CGFloat = 4
|
||||
public var windowRadius: CGFloat { originDelta + cellRadius }
|
||||
|
||||
/// 當前資料池是否存在多列/多行候選字詞呈現。
|
||||
public var isMatrix: Bool { maxLinesPerPage > 1 }
|
||||
|
||||
/// 用來在初期化一個候選字詞資料池的時候研判「橫版多行選字窗每行最大應該塞多少個候選字詞」。
|
||||
/// 注意:該參數不用來計算視窗寬度,所以無須算上候選字詞間距。
|
||||
public var maxRowWidth: Double { ceil(Double(maxLineCapacity) * Self.blankCell.cellLength()) }
|
||||
|
@ -130,6 +151,7 @@ public class CandidatePool {
|
|||
candidateLines.append(currentColumn)
|
||||
recordedLineRangeForCurrentPage = fallbackedLineRangeForCurrentPage
|
||||
highlight(at: 0)
|
||||
updateMetrics()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,7 +276,7 @@ public extension CandidatePool {
|
|||
if layout != .vertical, maxLinesPerPage == 1 {
|
||||
min = max(minAccepted, cell.cellLength(isMatrix: false))
|
||||
} else if layout == .vertical, maxLinesPerPage == 1 {
|
||||
min = max(Double(CandidateCellData.unifiedSize * 6), 90)
|
||||
min = max(Double(CandidateCellData.unifiedSize * 6), ceil(cell.size * 5.6))
|
||||
}
|
||||
return (min, nil)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,141 @@
|
|||
|
||||
import AppKit
|
||||
|
||||
// MARK: - Using One Single NSAttributedString.
|
||||
// MARK: - UI Metrics.
|
||||
|
||||
extension CandidatePool {
|
||||
public func updateMetrics() {
|
||||
// 開工
|
||||
let initialOrigin: NSPoint = .init(x: originDelta, y: originDelta)
|
||||
var totalAccuSize: NSSize = .zero
|
||||
// Origin is at the top-left corner.
|
||||
var currentOrigin: NSPoint = initialOrigin
|
||||
var highlightedCellRect: CGRect = .zero
|
||||
var highlightedLineRect: CGRect = .zero
|
||||
var currentPageLines = candidateLines[lineRangeForCurrentPage]
|
||||
var blankLines = maxLinesPerPage - currentPageLines.count
|
||||
var fillBlankCells = true
|
||||
switch (layout, isMatrix) {
|
||||
case (.horizontal, false):
|
||||
blankLines = 0
|
||||
fillBlankCells = false
|
||||
case (.vertical, false): blankLines = 0
|
||||
case (_, true): break
|
||||
}
|
||||
while blankLines > 0 {
|
||||
currentPageLines.append(.init(repeating: Self.shitCell, count: maxLineCapacity))
|
||||
blankLines -= 1
|
||||
}
|
||||
Self.shitCell.updateMetrics(pool: self, origin: currentOrigin)
|
||||
Self.shitCell.isHighlighted = false
|
||||
let minimumCellDimension = Self.shitCell.visualDimension
|
||||
currentPageLines.forEach { currentLine in
|
||||
let currentLineOrigin = currentOrigin
|
||||
var accumulatedLineSize: NSSize = .zero
|
||||
var currentLineRect: CGRect { .init(origin: currentLineOrigin, size: accumulatedLineSize) }
|
||||
let lineHasHighlightedCell = currentLine.hasHighlightedCell
|
||||
currentLine.forEach { currentCell in
|
||||
currentCell.updateMetrics(pool: self, origin: currentOrigin)
|
||||
var cellDimension = currentCell.visualDimension
|
||||
if layout == .vertical || currentCell.displayedText.count <= 2 {
|
||||
cellDimension.width = max(minimumCellDimension.width, cellDimension.width)
|
||||
}
|
||||
cellDimension.height = max(minimumCellDimension.height, cellDimension.height)
|
||||
switch self.layout {
|
||||
case .horizontal:
|
||||
accumulatedLineSize.width += cellDimension.width
|
||||
accumulatedLineSize.height = max(accumulatedLineSize.height, cellDimension.height)
|
||||
case .vertical:
|
||||
accumulatedLineSize.height += cellDimension.height
|
||||
accumulatedLineSize.width = max(accumulatedLineSize.width, cellDimension.width)
|
||||
}
|
||||
if lineHasHighlightedCell {
|
||||
switch self.layout {
|
||||
case .horizontal where currentCell.isHighlighted: highlightedCellRect.size.width = cellDimension.width
|
||||
case .vertical: highlightedCellRect.size.width = max(highlightedCellRect.size.width, cellDimension.width)
|
||||
default: break
|
||||
}
|
||||
if currentCell.isHighlighted {
|
||||
highlightedCellRect.origin = currentOrigin
|
||||
highlightedCellRect.size.height = cellDimension.height
|
||||
}
|
||||
}
|
||||
switch self.layout {
|
||||
case .horizontal: currentOrigin.x += cellDimension.width
|
||||
case .vertical: currentOrigin.y += cellDimension.height
|
||||
}
|
||||
}
|
||||
if lineHasHighlightedCell {
|
||||
highlightedLineRect.origin = currentLineRect.origin
|
||||
switch self.layout {
|
||||
case .horizontal:
|
||||
highlightedLineRect.size.height = currentLineRect.size.height
|
||||
case .vertical:
|
||||
highlightedLineRect.size.width = currentLineRect.size.width
|
||||
}
|
||||
}
|
||||
switch self.layout {
|
||||
case .horizontal:
|
||||
highlightedLineRect.size.width = max(currentLineRect.size.width, highlightedLineRect.width)
|
||||
case .vertical:
|
||||
highlightedLineRect.size.height = max(currentLineRect.size.height, highlightedLineRect.height)
|
||||
currentLine.forEach { theCell in
|
||||
theCell.visualDimension.width = accumulatedLineSize.width
|
||||
}
|
||||
}
|
||||
// 終末處理
|
||||
switch self.layout {
|
||||
case .horizontal:
|
||||
currentOrigin.x = originDelta
|
||||
currentOrigin.y += accumulatedLineSize.height
|
||||
totalAccuSize.width = max(totalAccuSize.width, accumulatedLineSize.width)
|
||||
totalAccuSize.height += accumulatedLineSize.height
|
||||
case .vertical:
|
||||
currentOrigin.y = originDelta
|
||||
currentOrigin.x += accumulatedLineSize.width
|
||||
totalAccuSize.height = max(totalAccuSize.height, accumulatedLineSize.height)
|
||||
totalAccuSize.width += accumulatedLineSize.width
|
||||
}
|
||||
}
|
||||
if fillBlankCells {
|
||||
switch layout {
|
||||
case .horizontal:
|
||||
totalAccuSize.width = max(totalAccuSize.width, CGFloat(maxLineCapacity) * minimumCellDimension.width)
|
||||
highlightedLineRect.size.width = totalAccuSize.width
|
||||
case .vertical:
|
||||
totalAccuSize.height = CGFloat(maxLineCapacity) * minimumCellDimension.height
|
||||
}
|
||||
}
|
||||
// 繪製附加內容
|
||||
let strPeripherals = attributedDescriptionBottomPanes
|
||||
var dimensionPeripherals = strPeripherals.boundingDimension
|
||||
dimensionPeripherals.width = ceil(dimensionPeripherals.width)
|
||||
dimensionPeripherals.height = ceil(dimensionPeripherals.height)
|
||||
if finalContainerOrientation == .horizontal {
|
||||
totalAccuSize.width += 5
|
||||
dimensionPeripherals.width += 5
|
||||
let delta = max(CandidateCellData.unifiedTextHeight + padding * 2 - dimensionPeripherals.height, 0)
|
||||
currentOrigin = .init(x: totalAccuSize.width + originDelta, y: ceil(delta / 2) + originDelta)
|
||||
totalAccuSize.width += dimensionPeripherals.width
|
||||
} else {
|
||||
totalAccuSize.height += 2
|
||||
currentOrigin = .init(x: padding + originDelta, y: totalAccuSize.height + originDelta)
|
||||
totalAccuSize.height += dimensionPeripherals.height
|
||||
totalAccuSize.width = max(totalAccuSize.width, dimensionPeripherals.width)
|
||||
}
|
||||
let rectPeripherals = CGRect(origin: currentOrigin, size: dimensionPeripherals)
|
||||
totalAccuSize.width += originDelta * 2
|
||||
totalAccuSize.height += originDelta * 2
|
||||
metrics = .init(fittingSize: totalAccuSize, highlightedLine: highlightedLineRect, highlightedCandidate: highlightedCellRect, peripherals: rectPeripherals)
|
||||
}
|
||||
|
||||
private var finalContainerOrientation: NSUserInterfaceLayoutOrientation {
|
||||
if maxLinesPerPage == 1, layout == .horizontal { return .horizontal }
|
||||
return .vertical
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Using One Single NSAttributedString. (Some of them are for debug purposes.)
|
||||
|
||||
extension CandidatePool {
|
||||
// MARK: Candidate List with Peripherals.
|
||||
|
@ -143,6 +277,7 @@ extension CandidatePool {
|
|||
let positionCounterTextSize = max(ceil(CandidateCellData.unifiedSize * 0.7), 11)
|
||||
let attrTooltip: [NSAttributedString.Key: AnyObject] = [
|
||||
.font: Self.blankCell.phraseFontEmphasized(size: positionCounterTextSize),
|
||||
.foregroundColor: NSColor.textColor,
|
||||
]
|
||||
let tooltipText = NSAttributedString(
|
||||
string: " \(tooltip) ", attributes: attrTooltip
|
||||
|
@ -154,6 +289,7 @@ extension CandidatePool {
|
|||
let reverseLookupTextSize = max(ceil(CandidateCellData.unifiedSize * 0.6), 9)
|
||||
let attrReverseLookup: [NSAttributedString.Key: AnyObject] = [
|
||||
.font: Self.blankCell.phraseFont(size: reverseLookupTextSize),
|
||||
.foregroundColor: NSColor.textColor,
|
||||
]
|
||||
let attrReverseLookupSpacer: [NSAttributedString.Key: AnyObject] = [
|
||||
.font: Self.blankCell.phraseFont(size: reverseLookupTextSize),
|
||||
|
|
|
@ -41,9 +41,16 @@ public class CtlCandidateTDK: CtlCandidate, NSWindowDelegate {
|
|||
).edgesIgnoringSafeArea(.top)
|
||||
}
|
||||
|
||||
#if USING_STACK_VIEW_IN_TDK_COCOA
|
||||
/// 該視圖模式因算法陳舊而不再維護。
|
||||
private var theViewCocoa: NSStackView {
|
||||
VwrCandidateTDKCocoa(controller: self, thePool: Self.thePool)
|
||||
}
|
||||
#endif
|
||||
|
||||
private var theViewAppKit: NSView {
|
||||
VwrCandidateTDKAppKit(controller: self, thePool: Self.thePool)
|
||||
}
|
||||
|
||||
private var theViewLegacy: NSView {
|
||||
let textField = NSTextField()
|
||||
|
@ -120,7 +127,7 @@ public class CtlCandidateTDK: CtlCandidate, NSWindowDelegate {
|
|||
Self.currentView = NSHostingView(rootView: theView)
|
||||
break viewCheck
|
||||
}
|
||||
Self.currentView = theViewCocoa
|
||||
Self.currentView = theViewAppKit
|
||||
}
|
||||
window.contentView = Self.currentView
|
||||
window.setContentSize(Self.currentView.fittingSize)
|
||||
|
|
|
@ -0,0 +1,246 @@
|
|||
// (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
|
||||
|
||||
/// 田所選字窗的 AppKit 简单版本,繪製效率不受 SwiftUI 的限制。
|
||||
/// 該版本可以使用更少的系統資源來繪製選字窗。
|
||||
|
||||
public class VwrCandidateTDKAppKit: NSView {
|
||||
public weak var controller: CtlCandidateTDK?
|
||||
public var thePool: CandidatePool
|
||||
private var dimension: NSSize = .zero
|
||||
var action: Selector?
|
||||
weak var target: AnyObject?
|
||||
var theMenu: NSMenu?
|
||||
var clickedCell: CandidateCellData = CandidatePool.shitCell
|
||||
|
||||
// MARK: - Variables used for rendering the UI.
|
||||
|
||||
var padding: CGFloat { thePool.padding }
|
||||
var originDelta: CGFloat { thePool.originDelta }
|
||||
var cellRadius: CGFloat { thePool.cellRadius }
|
||||
var windowRadius: CGFloat { thePool.windowRadius }
|
||||
var isMatrix: Bool { thePool.isMatrix }
|
||||
|
||||
// MARK: - Constructors.
|
||||
|
||||
public init(controller: CtlCandidateTDK? = nil, thePool pool: CandidatePool) {
|
||||
self.controller = controller
|
||||
thePool = pool
|
||||
thePool.updateMetrics()
|
||||
super.init(frame: .init(origin: .zero, size: .init(width: 114_514, height: 114_514)))
|
||||
}
|
||||
|
||||
deinit {
|
||||
theMenu?.cancelTrackingWithoutAnimation()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Interface Renderer (with shared public variables).
|
||||
|
||||
public extension VwrCandidateTDKAppKit {
|
||||
override var isFlipped: Bool { true }
|
||||
|
||||
override var fittingSize: NSSize { thePool.metrics.fittingSize }
|
||||
|
||||
static var candidateListBackground: NSColor {
|
||||
let delta = NSApplication.isDarkMode ? 0.05 : 0.99
|
||||
return .init(white: delta, alpha: 1)
|
||||
}
|
||||
|
||||
override func draw(_: NSRect) {
|
||||
let sizesCalculated = thePool.metrics
|
||||
// 先塗底色
|
||||
let allRect = NSRect(origin: .zero, size: sizesCalculated.fittingSize)
|
||||
Self.candidateListBackground.setFill()
|
||||
NSBezierPath(roundedRect: allRect, xRadius: windowRadius, yRadius: windowRadius).fill()
|
||||
// 繪製高亮行背景與高亮候選字詞背景
|
||||
lineBackground(isCurrentLine: true, isMatrix: isMatrix).setFill()
|
||||
NSBezierPath(roundedRect: sizesCalculated.highlightedLine, xRadius: cellRadius, yRadius: cellRadius).fill()
|
||||
var cellHighlightedDrawn = false
|
||||
// 開始繪製候選字詞
|
||||
let allCells = thePool.candidateLines[thePool.lineRangeForCurrentPage].flatMap { $0 }
|
||||
allCells.forEach { currentCell in
|
||||
if currentCell.isHighlighted, !cellHighlightedDrawn {
|
||||
currentCell.themeColorCocoa.setFill()
|
||||
NSBezierPath(roundedRect: sizesCalculated.highlightedCandidate, xRadius: cellRadius, yRadius: cellRadius).fill()
|
||||
cellHighlightedDrawn = true
|
||||
}
|
||||
currentCell.attributedStringHeader.draw(at:
|
||||
.init(
|
||||
x: currentCell.visualOrigin.x + 2 * padding,
|
||||
y: currentCell.visualOrigin.y + ceil(currentCell.visualDimension.height * 0.2)
|
||||
)
|
||||
)
|
||||
currentCell.attributedStringPhrase(isMatrix: false).draw(
|
||||
at: .init(
|
||||
x: currentCell.visualOrigin.x + 2 * padding + ceil(currentCell.size * 0.6),
|
||||
y: currentCell.visualOrigin.y + padding
|
||||
)
|
||||
)
|
||||
}
|
||||
// 繪製附加內容
|
||||
let strPeripherals = thePool.attributedDescriptionBottomPanes
|
||||
strPeripherals.draw(at: sizesCalculated.peripherals.origin)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mouse Interaction Handlers.
|
||||
|
||||
public extension VwrCandidateTDKAppKit {
|
||||
private func findCell(from mouseEvent: NSEvent) -> Int? {
|
||||
var clickPoint = convert(mouseEvent.locationInWindow, to: self)
|
||||
clickPoint.y = bounds.height - clickPoint.y // 翻轉座標系
|
||||
guard bounds.contains(clickPoint) else { return nil }
|
||||
let flattenedCells = thePool.candidateLines[thePool.lineRangeForCurrentPage].flatMap { $0 }
|
||||
let x = flattenedCells.filter { theCell in
|
||||
NSPointInRect(clickPoint, .init(origin: theCell.visualOrigin, size: theCell.visualDimension))
|
||||
}.first
|
||||
guard let firstValidCell = x else { return nil }
|
||||
return firstValidCell.index
|
||||
}
|
||||
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
guard let cellIndex = findCell(from: event) else { return }
|
||||
guard cellIndex != thePool.highlightedIndex else { return }
|
||||
thePool.highlight(at: cellIndex)
|
||||
thePool.updateMetrics()
|
||||
setNeedsDisplay(bounds)
|
||||
}
|
||||
|
||||
override func mouseDragged(with event: NSEvent) {
|
||||
mouseDown(with: event)
|
||||
}
|
||||
|
||||
override func mouseUp(with event: NSEvent) {
|
||||
guard let cellIndex = findCell(from: event) else { return }
|
||||
didSelectCandidateAt(cellIndex)
|
||||
}
|
||||
|
||||
override func rightMouseUp(with event: NSEvent) {
|
||||
guard let cellIndex = findCell(from: event) else { return }
|
||||
clickedCell = thePool.candidateDataAll[cellIndex]
|
||||
let index = clickedCell.index
|
||||
let candidateText = clickedCell.displayedText
|
||||
let isEnabled: Bool = controller?.delegate?.isCandidateContextMenuEnabled ?? false
|
||||
guard isEnabled, !candidateText.isEmpty, index >= 0 else { return }
|
||||
prepareMenu()
|
||||
var clickPoint = convert(event.locationInWindow, to: self)
|
||||
clickPoint.y = bounds.height - clickPoint.y // 翻轉座標系
|
||||
theMenu?.popUp(positioning: nil, at: clickPoint, in: self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Context Menu.
|
||||
|
||||
private extension VwrCandidateTDKAppKit {
|
||||
private func prepareMenu() {
|
||||
let newMenu = NSMenu()
|
||||
let boostMenuItem = NSMenuItem(
|
||||
title: "↑ \(clickedCell.displayedText)",
|
||||
action: #selector(menuActionOfBoosting(_:)),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
boostMenuItem.target = self
|
||||
newMenu.addItem(boostMenuItem)
|
||||
|
||||
let nerfMenuItem = NSMenuItem(
|
||||
title: "↓ \(clickedCell.displayedText)",
|
||||
action: #selector(menuActionOfNerfing(_:)),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
nerfMenuItem.target = self
|
||||
newMenu.addItem(nerfMenuItem)
|
||||
|
||||
if thePool.isFilterable(target: clickedCell.index) {
|
||||
let filterMenuItem = NSMenuItem(
|
||||
title: "✖︎ \(clickedCell.displayedText)",
|
||||
action: #selector(menuActionOfFiltering(_:)),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
filterMenuItem.target = self
|
||||
newMenu.addItem(filterMenuItem)
|
||||
}
|
||||
|
||||
theMenu = newMenu
|
||||
CtlCandidateTDK.currentMenu = newMenu
|
||||
}
|
||||
|
||||
@objc func menuActionOfBoosting(_: Any? = nil) {
|
||||
didRightClickCandidateAt(clickedCell.index, action: .toBoost)
|
||||
}
|
||||
|
||||
@objc func menuActionOfNerfing(_: Any? = nil) {
|
||||
didRightClickCandidateAt(clickedCell.index, action: .toNerf)
|
||||
}
|
||||
|
||||
@objc func menuActionOfFiltering(_: Any? = nil) {
|
||||
didRightClickCandidateAt(clickedCell.index, action: .toFilter)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Delegate Methods
|
||||
|
||||
private extension VwrCandidateTDKAppKit {
|
||||
func didSelectCandidateAt(_ pos: Int) {
|
||||
controller?.delegate?.candidatePairSelectionConfirmed(at: pos)
|
||||
}
|
||||
|
||||
func didRightClickCandidateAt(_ pos: Int, action: CandidateContextMenuAction) {
|
||||
controller?.delegate?.candidatePairRightClicked(at: pos, action: action)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Extracted Internal Methods for UI Rendering.
|
||||
|
||||
private extension VwrCandidateTDKAppKit {
|
||||
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 var finalContainerOrientation: NSUserInterfaceLayoutOrientation {
|
||||
if thePool.maxLinesPerPage == 1, thePool.layout == .horizontal { return .horizontal }
|
||||
return .vertical
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Debug Module Using Swift UI.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@available(macOS 10.15, *)
|
||||
public struct VwrCandidateTDKAppKitForSwiftUI: NSViewRepresentable {
|
||||
public weak var controller: CtlCandidateTDK?
|
||||
public var thePool: CandidatePool
|
||||
|
||||
public func makeNSView(context _: Context) -> VwrCandidateTDKAppKit {
|
||||
let nsView = VwrCandidateTDKAppKit(thePool: thePool)
|
||||
nsView.controller = controller
|
||||
return nsView
|
||||
}
|
||||
|
||||
public func updateNSView(_ nsView: VwrCandidateTDKAppKit, context _: Context) {
|
||||
nsView.thePool = thePool
|
||||
}
|
||||
}
|
|
@ -6,6 +6,8 @@
|
|||
// 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
|
||||
|
||||
|
@ -206,7 +208,7 @@ private extension VwrCandidateTDKCocoa {
|
|||
case .horizontal where isMatrix:
|
||||
return NSApplication.isDarkMode ? .controlTextColor.withAlphaComponent(0.05) : .white
|
||||
case .vertical where isMatrix:
|
||||
return absBg.withAlphaComponent(0.13)
|
||||
return absBg.withAlphaComponent(0.9)
|
||||
default:
|
||||
return .clear
|
||||
}
|
||||
|
@ -415,3 +417,5 @@ public struct VwrCandidateTDKCocoaForSwiftUI: NSViewRepresentable {
|
|||
nsView.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -180,7 +180,7 @@ extension VwrCandidateTDK {
|
|||
case .horizontal where isCurrentLineInMatrix:
|
||||
return colorScheme == .dark ? Color.primary.opacity(0.05) : .white
|
||||
case .vertical where isCurrentLineInMatrix:
|
||||
return absoluteBackgroundColor.opacity(0.13)
|
||||
return absoluteBackgroundColor.opacity(0.9)
|
||||
default:
|
||||
return Color.clear
|
||||
}
|
||||
|
@ -308,9 +308,9 @@ extension VwrCandidateTDK {
|
|||
|
||||
var absoluteBackgroundColor: Color {
|
||||
if colorScheme == .dark {
|
||||
return Color(white: 0)
|
||||
return Color.black
|
||||
} else {
|
||||
return Color(white: 1)
|
||||
return Color.white
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -455,6 +455,32 @@ struct VwrCandidateTDK_Previews: PreviewProvider {
|
|||
}
|
||||
}
|
||||
}
|
||||
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))
|
||||
|
@ -480,5 +506,6 @@ struct VwrCandidateTDK_Previews: PreviewProvider {
|
|||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue