CtlCandidateTDK // Rewrite.

This commit is contained in:
ShikiSuen 2023-07-16 17:28:10 +08:00
parent e4a8f34075
commit 339cfb0ad4
7 changed files with 859 additions and 398 deletions

View File

@ -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.

View File

@ -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)
}

View File

@ -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),

View File

@ -41,8 +41,15 @@ public class CtlCandidateTDK: CtlCandidate, NSWindowDelegate {
).edgesIgnoringSafeArea(.top)
}
private var theViewCocoa: NSStackView {
VwrCandidateTDKCocoa(controller: self, thePool: Self.thePool)
#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 {
@ -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)

View File

@ -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
}
}

View File

@ -6,412 +6,416 @@
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
import Shared
#if USING_STACK_VIEW_IN_TDK_COCOA
/// 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
import AppKit
import Shared
// MARK: - Constructors.
/// 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
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()
}
// MARK: - Constructors.
@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.maxLinesPerPage > 1:
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.13)
default:
return .clear
}
}
private func generateLineContainer(_ theLine: inout [CandidateCellData]) -> NSStackView {
let isVerticalListing: Bool = thePool.layout == .vertical
let isMatrix = thePool.maxLinesPerPage > 1
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
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)))
isSelectable = false
isEditable = false
isBordered = false
backgroundColor = .clear
allowsEditingTextAttributes = false
preferredMaxLayoutWidth = frame.width
attributedStringValue = cellData.attributedString()
sizeToFit()
refresh()
}
@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: - Interface Renderer.
// 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)
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)
}
}
theMenu = newMenu
CtlCandidateTDK.currentMenu = newMenu
}
//
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)
}
@objc func menuActionOfBoosting(_: Any? = nil) {
guard let target = target as? VwrCandidateTDKCocoa else { return }
target.didRightClickCandidateAt(cellData.index, action: .toBoost)
}
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")
@objc func menuActionOfNerfing(_: Any? = nil) {
guard let target = target as? VwrCandidateTDKCocoa else { return }
target.didRightClickCandidateAt(cellData.index, action: .toNerf)
}
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
)
@objc func menuActionOfFiltering(_: Any? = nil) {
guard let target = target as? VwrCandidateTDKCocoa else { return }
target.didRightClickCandidateAt(cellData.index, action: .toFilter)
//
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: - Delegate Methods
// MARK: - Interface Components.
private extension VwrCandidateTDKCocoa {
func didSelectCandidateAt(_ pos: Int) {
controller?.delegate?.candidatePairSelectionConfirmed(at: pos)
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.maxLinesPerPage > 1:
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.maxLinesPerPage > 1
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
}
}
func didRightClickCandidateAt(_ pos: Int, action: CandidateContextMenuAction) {
controller?.delegate?.candidatePairRightClicked(at: pos, action: action)
}
}
// MARK: - Constraint Utilities
// MARK: - Debug Module Using Swift UI.
private extension VwrCandidateTDKCocoa {
static var strForConstraintStatistics = NSMutableString(string: "TDKCandidates Dimensions (Debug):\n")
import SwiftUI
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")
}
@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
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)
}
}
public func updateNSView(_ nsView: VwrCandidateTDKCocoa, context _: Context) {
nsView.thePool = thePool
nsView.refresh()
// 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

View File

@ -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
}
}
@ -457,13 +457,13 @@ struct VwrCandidateTDK_Previews: PreviewProvider {
}
VStack {
HStack(alignment: .top) {
Text("田所選字窗 Cocoa 模式").bold().font(Font.system(.title))
Text("田所選字窗 CG 模式").bold().font(Font.system(.title))
VStack {
VwrCandidateTDKCocoaForSwiftUI(controller: nil, thePool: thePoolX).fixedSize()
VwrCandidateTDKCocoaForSwiftUI(controller: nil, thePool: thePoolXS).fixedSize()
VwrCandidateTDKAppKitForSwiftUI(controller: nil, thePool: thePoolX).fixedSize()
VwrCandidateTDKAppKitForSwiftUI(controller: nil, thePool: thePoolXS).fixedSize()
HStack {
VwrCandidateTDKCocoaForSwiftUI(controller: nil, thePool: thePoolY).fixedSize()
VwrCandidateTDKCocoaForSwiftUI(controller: nil, thePool: thePoolYS).fixedSize()
VwrCandidateTDKAppKitForSwiftUI(controller: nil, thePool: thePoolY).fixedSize()
VwrCandidateTDKAppKitForSwiftUI(controller: nil, thePool: thePoolYS).fixedSize()
}
}
}
@ -480,5 +480,32 @@ struct VwrCandidateTDK_Previews: PreviewProvider {
}
}
}
#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
}
}