TDKCandidates // Rewrite the UI using NSStackView.

* The mouse wheel scrolling operation currently gives a disastrous experience with TrackPad, hence getting disabled for now.
This commit is contained in:
ShikiSuen 2023-02-25 15:38:22 +08:00
parent 7180bb2798
commit 949f140671
7 changed files with 552 additions and 231 deletions

View File

@ -30,12 +30,13 @@ public class CandidateCellData: Hashable {
public var charGlyphWidth: Double { ceil(size * 1.0125 + 7) } public var charGlyphWidth: Double { ceil(size * 1.0125 + 7) }
public var fontSizeCandidate: Double { size } public var fontSizeCandidate: Double { size }
public var fontSizeKey: Double { max(ceil(fontSizeCandidate * 0.6), 11) } public var fontSizeKey: Double { max(ceil(fontSizeCandidate * 0.6), 11) }
public var fontColorCandidate: NSColor { isHighlighted ? .selectedMenuItemTextColor : .controlTextColor }
public var fontColorKey: NSColor { public var fontColorKey: NSColor {
isHighlighted ? .selectedMenuItemTextColor.withAlphaComponent(0.8) : .secondaryLabelColor isHighlighted
? .selectedMenuItemTextColor.withAlphaComponent(0.9)
: .init(red: 142 / 255, green: 142 / 255, blue: 147 / 255, alpha: 1)
} }
public var fontColorCandidate: NSColor { isHighlighted ? .selectedMenuItemTextColor : .labelColor }
public init(key: String, displayedText: String, isSelected: Bool = false) { public init(key: String, displayedText: String, isSelected: Bool = false) {
self.key = key self.key = key
self.displayedText = displayedText self.displayedText = displayedText
@ -52,11 +53,60 @@ public class CandidateCellData: Hashable {
} }
public func cellLength(isMatrix: Bool = true) -> Double { public func cellLength(isMatrix: Bool = true) -> Double {
let minLength = ceil(charGlyphWidth * 2 + size) let minLength = ceil(charGlyphWidth * 2 + size * 1.25)
if displayedText.count <= 2, isMatrix { return minLength } if displayedText.count <= 2, isMatrix { return minLength }
return ceil(attributedStringForLengthCalculation.boundingDimension.width) return ceil(attributedStringPhrase().boundingDimension.width + charGlyphWidth)
} }
// MARK: - Fonts and NSColors.
func selectionKeyFont(size: CGFloat? = nil) -> NSFont {
let size: CGFloat = size ?? fontSizeKey
if #available(macOS 10.15, *) {
return NSFont.monospacedSystemFont(ofSize: fontSizeKey, weight: .regular)
}
return NSFont(name: "Menlo", size: size) ?? phraseFont(size: size)
}
func phraseFont(size: CGFloat? = nil) -> NSFont {
let size: CGFloat = size ?? fontSizeCandidate
// 退
// macOS 10.9 - 10.12 DefaultFontFallbacks CTPresetFallbacks
// var result: NSFont?
// compatibility: if #unavailable(macOS 10.11) {
// var fontIDs = [String]()
// switch locale {
// case "zh-Hans": fontIDs = ["PingFang SC", "Noto Sans CJK SC", "Hiragino Sans GB"]
// case "zh-Hant": fontIDs = ["PingFang TC", "Noto Sans CJK TC", "LiHei Pro"]
// case "ja": fontIDs = ["PingFang JA", "Noto Sans CJK JP", "Hiragino Kaku Gothic ProN W3"]
// default: break compatibility
// }
// fallback: for psName in fontIDs {
// result = NSFont(name: psName, size: size)
// guard result == nil else { break compatibility }
// }
// }
let defaultResult: CTFont? = CTFontCreateUIFontForLanguage(.system, size, locale as CFString)
return defaultResult ?? NSFont.systemFont(ofSize: size)
}
func phraseFontEmphasized(size: CGFloat? = nil) -> NSFont {
let size: CGFloat = size ?? fontSizeCandidate
let result: CTFont? = CTFontCreateUIFontForLanguage(.emphasizedSystem, size, locale as CFString)
return result ?? NSFont.systemFont(ofSize: size)
}
var themeColorCocoa: NSColor {
switch locale {
case "zh-Hans": return .init(red: 255 / 255, green: 64 / 255, blue: 53 / 255, alpha: 0.85)
case "zh-Hant": return .init(red: 5 / 255, green: 127 / 255, blue: 255 / 255, alpha: 0.85)
case "ja": return .init(red: 167 / 255, green: 137 / 255, blue: 99 / 255, alpha: 0.85)
default: return .init(red: 5 / 255, green: 127 / 255, blue: 255 / 255, alpha: 0.85)
}
}
// MARK: - Basic NSAttributedString Components.
public static let sharedParagraphStyle: NSParagraphStyle = { public static let sharedParagraphStyle: NSParagraphStyle = {
let paraStyle = NSMutableParagraphStyle() let paraStyle = NSMutableParagraphStyle()
paraStyle.setParagraphStyle(NSParagraphStyle.default) paraStyle.setParagraphStyle(NSParagraphStyle.default)
@ -65,65 +115,28 @@ public class CandidateCellData: Hashable {
return paraStyle return paraStyle
}() }()
var phraseFont: NSFont {
CTFontCreateUIFontForLanguage(.system, size, locale as CFString) ?? NSFont.systemFont(ofSize: size)
}
var highlightedNSColor: NSColor {
var result = NSColor.alternateSelectedControlColor
var colorBlendAmount: Double = NSApplication.isDarkMode ? 0.3 : 0.0
if #available(macOS 10.14, *), !NSApplication.isDarkMode, locale == "zh-Hant" {
colorBlendAmount = 0.15
}
//
switch locale {
case "zh-Hans":
result = NSColor.systemRed
case "zh-Hant":
result = NSColor.systemBlue
case "ja":
result = NSColor.systemBrown
default: break
}
var blendingAgainstTarget: NSColor = NSApplication.isDarkMode ? NSColor.black : NSColor.white
if #unavailable(macOS 10.14) {
colorBlendAmount = 0.3
blendingAgainstTarget = NSColor.white
}
return result.blended(withFraction: colorBlendAmount, of: blendingAgainstTarget)!
}
public var attributedStringForLengthCalculation: NSAttributedString {
let attrCandidate: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: size, weight: .regular),
.paragraphStyle: Self.sharedParagraphStyle,
]
let attrStrCandidate = NSAttributedString(string: displayedText + " ", attributes: attrCandidate)
return attrStrCandidate
}
public func attributedString( public func attributedString(
noSpacePadding: Bool = true, withHighlight: Bool = false, isMatrix: Bool = false noSpacePadding: Bool = true, withHighlight: Bool = false, isMatrix: Bool = false
) -> NSAttributedString { ) -> NSAttributedString {
let attrCandidate: [NSAttributedString.Key: AnyObject] = [ let attrSpace: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: size, weight: .regular), .font: phraseFont(size: size),
.paragraphStyle: Self.sharedParagraphStyle, .paragraphStyle: Self.sharedParagraphStyle,
] ]
let result: NSMutableAttributedString = { let result: NSMutableAttributedString = {
if noSpacePadding { if noSpacePadding {
let resultNeo = NSMutableAttributedString(string: " ", attributes: attrCandidate) let resultNeo = NSMutableAttributedString(string: " ", attributes: attrSpace)
resultNeo.insert(attributedStringPhrase(isMatrix: isMatrix), at: 1) resultNeo.insert(attributedStringPhrase(isMatrix: isMatrix), at: 1)
resultNeo.insert(attributedStringHeader, at: 0) resultNeo.insert(attributedStringHeader, at: 0)
return resultNeo return resultNeo
} }
let resultNeo = NSMutableAttributedString(string: " ", attributes: attrCandidate) let resultNeo = NSMutableAttributedString(string: " ", attributes: attrSpace)
resultNeo.insert(attributedStringPhrase(isMatrix: isMatrix), at: 2) resultNeo.insert(attributedStringPhrase(isMatrix: isMatrix), at: 2)
resultNeo.insert(attributedStringHeader, at: 1) resultNeo.insert(attributedStringHeader, at: 1)
return resultNeo return resultNeo
}() }()
if withHighlight, isHighlighted { if withHighlight, isHighlighted {
result.addAttribute( result.addAttribute(
.backgroundColor, value: highlightedNSColor, .backgroundColor, value: themeColorCocoa,
range: NSRange(location: 0, length: result.string.utf16.count) range: NSRange(location: 0, length: result.string.utf16.count)
) )
} }
@ -131,35 +144,21 @@ public class CandidateCellData: Hashable {
} }
public var attributedStringHeader: NSAttributedString { public var attributedStringHeader: NSAttributedString {
let theFontForCandidateKey: NSFont = { let attrKey: [NSAttributedString.Key: AnyObject] = [
if #available(macOS 10.15, *) { .font: selectionKeyFont(size: fontSizeKey),
return NSFont.monospacedSystemFont(ofSize: fontSizeKey, weight: .regular)
}
return NSFont.monospacedDigitSystemFont(ofSize: fontSizeKey, weight: .regular)
}()
var attrKey: [NSAttributedString.Key: AnyObject] = [
.font: theFontForCandidateKey,
.paragraphStyle: Self.sharedParagraphStyle, .paragraphStyle: Self.sharedParagraphStyle,
.foregroundColor: fontColorKey,
] ]
if isHighlighted {
attrKey[.foregroundColor] = NSColor.white.withAlphaComponent(0.8)
} else {
attrKey[.foregroundColor] = NSColor.secondaryLabelColor
}
let attrStrKey = NSAttributedString(string: key, attributes: attrKey) let attrStrKey = NSAttributedString(string: key, attributes: attrKey)
return attrStrKey return attrStrKey
} }
public func attributedStringPhrase(isMatrix: Bool = false) -> NSAttributedString { public func attributedStringPhrase(isMatrix: Bool = false) -> NSAttributedString {
var attrCandidate: [NSAttributedString.Key: AnyObject] = [ var attrCandidate: [NSAttributedString.Key: AnyObject] = [
.font: phraseFont, .font: phraseFont(size: size),
.paragraphStyle: Self.sharedParagraphStyle, .paragraphStyle: Self.sharedParagraphStyle,
.foregroundColor: fontColorCandidate,
] ]
if isHighlighted {
attrCandidate[.foregroundColor] = NSColor.white
} else {
attrCandidate[.foregroundColor] = NSColor.labelColor
}
if #available(macOS 12, *) { if #available(macOS 12, *) {
if UserDefaults.standard.bool( if UserDefaults.standard.bool(
forKey: UserDef.kLegacyCandidateViewTypesettingMethodEnabled.rawValue forKey: UserDef.kLegacyCandidateViewTypesettingMethodEnabled.rawValue
@ -184,8 +183,15 @@ public class CandidateCellData: Hashable {
return String(format: "U+%02X %@", $0.value, theName) return String(format: "U+%02X %@", $0.value, theName)
} }
} }
}
public func minWidthToDraw(isMatrix: Bool = true) -> Double { // MARK: - Array Container Extension.
cellLength(isMatrix: isMatrix) + ceil(fontSizeKey * 0.1)
public extension Array where Element == CandidateCellData {
var hasHighlightedCell: Bool {
for neta in self {
if neta.isHighlighted { return true }
}
return false
} }
} }

View File

@ -12,6 +12,7 @@ import Shared
/// ///
public struct CandidatePool { public struct CandidatePool {
public let blankCell: CandidateCellData public let blankCell: CandidateCellData
public let shitCell: CandidateCellData // cell
public let maxLinesPerPage: Int public let maxLinesPerPage: Int
public let layout: LayoutOrientation public let layout: LayoutOrientation
public let selectionKeys: String public let selectionKeys: String
@ -29,7 +30,7 @@ public struct CandidatePool {
/// ///
/// ///
public var maxRowWidth: Double { ceil(Double(maxLineCapacity) * blankCell.minWidthToDraw()) } public var maxRowWidth: Double { ceil(Double(maxLineCapacity) * blankCell.cellLength()) }
/// ///
public var currentPositionLabelText: String { public var currentPositionLabelText: String {
@ -82,6 +83,7 @@ public struct CandidatePool {
self.layout = layout self.layout = layout
maxLinesPerPage = max(1, lines) maxLinesPerPage = max(1, lines)
blankCell = CandidateCellData(key: " ", displayedText: " ", isSelected: false) blankCell = CandidateCellData(key: " ", displayedText: " ", isSelected: false)
shitCell = CandidateCellData(key: " ", displayedText: "💩", isSelected: false)
blankCell.locale = locale blankCell.locale = locale
self.selectionKeys = selectionKeys.isEmpty ? "123456789" : selectionKeys self.selectionKeys = selectionKeys.isEmpty ? "123456789" : selectionKeys
var allCandidates = candidates.map { CandidateCellData(key: " ", displayedText: $0) } var allCandidates = candidates.map { CandidateCellData(key: " ", displayedText: $0) }
@ -94,7 +96,7 @@ public struct CandidatePool {
var isOverflown: Bool = (currentColumn.count == maxLineCapacity) && !currentColumn.isEmpty var isOverflown: Bool = (currentColumn.count == maxLineCapacity) && !currentColumn.isEmpty
if layout == .horizontal { if layout == .horizontal {
isOverflown = isOverflown isOverflown = isOverflown
|| currentColumn.map { $0.cellLength() }.reduce(0, +) >= maxRowWidth - candidate.cellLength() || currentColumn.map { $0.cellLength() }.reduce(0, +) > maxRowWidth - candidate.cellLength()
} }
if isOverflown { if isOverflown {
candidateLines.append(currentColumn) candidateLines.append(currentColumn)
@ -216,9 +218,21 @@ public extension CandidatePool {
fixLineRange(isBackward: isBackward) fixLineRange(isBackward: isBackward)
} }
} }
func cellWidth(_ cell: CandidateCellData) -> (min: CGFloat?, max: CGFloat?) {
let minAccepted = ceil(shitCell.cellLength(isMatrix: false))
let defaultMin: CGFloat = cell.cellLength(isMatrix: maxLinesPerPage != 1)
var min: CGFloat = defaultMin
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)
}
return (min, nil)
}
} }
// MARK: - Private Functions // MARK: - Privates.
private extension CandidatePool { private extension CandidatePool {
enum VerticalDirection { enum VerticalDirection {

View File

@ -8,7 +8,11 @@
import Cocoa import Cocoa
// MARK: - Using One Single NSAttributedString.
extension CandidatePool { extension CandidatePool {
// MARK: Candidate List with Peripherals.
public var attributedDescription: NSAttributedString { public var attributedDescription: NSAttributedString {
switch layout { switch layout {
case .horizontal: return attributedDescriptionHorizontal case .horizontal: return attributedDescriptionHorizontal
@ -16,13 +20,12 @@ extension CandidatePool {
} }
} }
/// NSAttributedString private var sharedParagraphStyle: NSParagraphStyle { CandidateCellData.sharedParagraphStyle }
private var attributedDescriptionHorizontal: NSAttributedString { private var attributedDescriptionHorizontal: NSAttributedString {
let paragraphStyle = CandidateCellData.sharedParagraphStyle as! NSMutableParagraphStyle let paragraphStyle = sharedParagraphStyle
paragraphStyle.lineSpacing = ceil(blankCell.size * 0.3)
paragraphStyle.lineBreakStrategy = .pushOut
let attrCandidate: [NSAttributedString.Key: AnyObject] = [ let attrCandidate: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: blankCell.size, weight: .regular), .font: blankCell.phraseFont(size: blankCell.size),
.paragraphStyle: paragraphStyle, .paragraphStyle: paragraphStyle,
] ]
let result = NSMutableAttributedString(string: "", attributes: attrCandidate) let result = NSMutableAttributedString(string: "", attributes: attrCandidate)
@ -59,11 +62,9 @@ extension CandidatePool {
} }
private var attributedDescriptionVertical: NSAttributedString { private var attributedDescriptionVertical: NSAttributedString {
let paragraphStyle = CandidateCellData.sharedParagraphStyle as! NSMutableParagraphStyle let paragraphStyle = sharedParagraphStyle
paragraphStyle.lineSpacing = ceil(blankCell.size * 0.3)
paragraphStyle.lineBreakStrategy = .pushOut
let attrCandidate: [NSAttributedString.Key: AnyObject] = [ let attrCandidate: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: blankCell.size, weight: .regular), .font: blankCell.phraseFont(size: blankCell.size),
.paragraphStyle: paragraphStyle, .paragraphStyle: paragraphStyle,
] ]
let result = NSMutableAttributedString(string: "", attributes: attrCandidate) let result = NSMutableAttributedString(string: "", attributes: attrCandidate)
@ -90,7 +91,7 @@ extension CandidatePool {
if currentCell.isHighlighted { if currentCell.isHighlighted {
spacer.addAttribute( spacer.addAttribute(
.backgroundColor, .backgroundColor,
value: currentCell.highlightedNSColor, value: currentCell.themeColorCocoa,
range: .init(location: 0, length: spacer.string.utf16.count) range: .init(location: 0, length: spacer.string.utf16.count)
) )
} else { } else {
@ -109,63 +110,60 @@ extension CandidatePool {
return result return result
} }
private var attributedDescriptionBottomPanes: NSAttributedString { // MARK: Peripherals
let paragraphStyle = CandidateCellData.sharedParagraphStyle as! NSMutableParagraphStyle
paragraphStyle.lineSpacing = ceil(blankCell.size * 0.3) public var attributedDescriptionBottomPanes: NSAttributedString {
paragraphStyle.lineBreakStrategy = .pushOut let paragraphStyle = sharedParagraphStyle
let attrCandidate: [NSAttributedString.Key: AnyObject] = [ let result = NSMutableAttributedString(string: "")
.font: NSFont.monospacedDigitSystemFont(ofSize: blankCell.size, weight: .regular), result.append(attributedDescriptionPositionCounter)
.paragraphStyle: paragraphStyle, if !tooltip.isEmpty { result.append(attributedDescriptionTooltip) }
] if !reverseLookupResult.isEmpty { result.append(attributedDescriptionReverseLookp) }
let result = NSMutableAttributedString(string: "", attributes: attrCandidate) result.addAttribute(.paragraphStyle, value: paragraphStyle, range: .init(location: 0, length: result.string.utf16.count))
return result
}
private var attributedDescriptionPositionCounter: NSAttributedString {
let positionCounterColorBG = NSApplication.isDarkMode let positionCounterColorBG = NSApplication.isDarkMode
? NSColor(white: 0.215, alpha: 0.7) ? NSColor(white: 0.215, alpha: 0.7)
: NSColor(white: 0.9, alpha: 0.7) : NSColor(white: 0.9, alpha: 0.7)
let positionCounterColorText = NSColor.controlTextColor let positionCounterColorText = NSColor.controlTextColor
let positionCounterTextSize = max(ceil(CandidateCellData.unifiedSize * 0.7), 11) let positionCounterTextSize = max(ceil(CandidateCellData.unifiedSize * 0.7), 11)
let attrPositionCounter: [NSAttributedString.Key: AnyObject] = [ let attrPositionCounter: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: positionCounterTextSize, weight: .bold), .font: blankCell.phraseFontEmphasized(size: positionCounterTextSize),
.paragraphStyle: paragraphStyle,
.backgroundColor: positionCounterColorBG, .backgroundColor: positionCounterColorBG,
.foregroundColor: positionCounterColorText, .foregroundColor: positionCounterColorText,
] ]
let positionCounter = NSAttributedString( let positionCounter = NSAttributedString(
string: " \(currentPositionLabelText) ", attributes: attrPositionCounter string: " \(currentPositionLabelText) ", attributes: attrPositionCounter
) )
result.append(positionCounter) return positionCounter
}
if !tooltip.isEmpty { private var attributedDescriptionTooltip: NSAttributedString {
let attrTooltip: [NSAttributedString.Key: AnyObject] = [ let positionCounterTextSize = max(ceil(CandidateCellData.unifiedSize * 0.7), 11)
.font: NSFont.monospacedDigitSystemFont(ofSize: positionCounterTextSize, weight: .regular), let attrTooltip: [NSAttributedString.Key: AnyObject] = [
.paragraphStyle: paragraphStyle, .font: blankCell.phraseFontEmphasized(size: positionCounterTextSize),
] ]
let tooltipText = NSAttributedString( let tooltipText = NSAttributedString(
string: " \(tooltip) ", attributes: attrTooltip string: " \(tooltip) ", attributes: attrTooltip
) )
result.append(tooltipText) return tooltipText
} }
if !reverseLookupResult.isEmpty { private var attributedDescriptionReverseLookp: NSAttributedString {
let reverseLookupTextSize = max(ceil(CandidateCellData.unifiedSize * 0.6), 9) let reverseLookupTextSize = max(ceil(CandidateCellData.unifiedSize * 0.6), 9)
let reverseLookupColorBG = NSApplication.isDarkMode let attrReverseLookup: [NSAttributedString.Key: AnyObject] = [
? NSColor(white: 0.1, alpha: 1) .font: blankCell.phraseFont(size: reverseLookupTextSize),
: NSColor(white: 0.9, alpha: 1) ]
let attrReverseLookup: [NSAttributedString.Key: AnyObject] = [ let attrReverseLookupSpacer: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: reverseLookupTextSize, weight: .regular), .font: blankCell.phraseFont(size: reverseLookupTextSize),
.paragraphStyle: paragraphStyle, ]
.backgroundColor: reverseLookupColorBG, let result = NSMutableAttributedString(string: "", attributes: attrReverseLookupSpacer)
] for neta in reverseLookupResult {
let attrReverseLookupSpacer: [NSAttributedString.Key: AnyObject] = [ result.append(NSAttributedString(string: " ", attributes: attrReverseLookupSpacer))
.font: NSFont.monospacedDigitSystemFont(ofSize: reverseLookupTextSize, weight: .regular), result.append(NSAttributedString(string: " \(neta) ", attributes: attrReverseLookup))
.paragraphStyle: paragraphStyle, if maxLinesPerPage == 1 { break }
]
for neta in reverseLookupResult {
result.append(NSAttributedString(string: " ", attributes: attrReverseLookupSpacer))
result.append(NSAttributedString(string: " \(neta) ", attributes: attrReverseLookup))
if maxLinesPerPage == 1 { break }
}
} }
result.addAttribute(.paragraphStyle, value: paragraphStyle, range: .init(location: 0, length: result.string.utf16.count))
return result return result
} }
} }

View File

@ -25,9 +25,10 @@ private extension NSUserInterfaceLayoutOrientation {
} }
} }
public class CtlCandidateTDK: CtlCandidate { public class CtlCandidateTDK: CtlCandidate, NSWindowDelegate {
public var maxLinesPerPage: Int = 0 public var maxLinesPerPage: Int = 0
public var isLegacyMode: Bool = false public var useCocoa: Bool = false
public var useMouseScrolling: Bool = true
private static var thePool: CandidatePool = .init(candidates: []) private static var thePool: CandidatePool = .init(candidates: [])
private static var currentView: NSView = .init() private static var currentView: NSView = .init()
@ -38,15 +39,8 @@ public class CtlCandidateTDK: CtlCandidate {
).edgesIgnoringSafeArea(.top) ).edgesIgnoringSafeArea(.top)
} }
private var theViewLegacy: NSView { private var theViewCocoa: NSStackView {
let textField = NSTextField( VwrCandidateTDKCocoa(controller: self, thePool: Self.thePool)
labelWithAttributedString: Self.thePool.attributedDescription
)
textField.isSelectable = false
textField.allowsEditingTextAttributes = false
textField.preferredMaxLayoutWidth = textField.frame.width
textField.backgroundColor = .controlBackgroundColor
return textField
} }
// MARK: - Constructors // MARK: - Constructors
@ -59,12 +53,12 @@ public class CtlCandidateTDK: CtlCandidate {
) )
panel.level = NSWindow.Level(Int(max(CGShieldingWindowLevel(), kCGPopUpMenuWindowLevel)) + 2) panel.level = NSWindow.Level(Int(max(CGShieldingWindowLevel(), kCGPopUpMenuWindowLevel)) + 2)
panel.hasShadow = true panel.hasShadow = true
panel.isOpaque = false
panel.backgroundColor = NSColor.clear panel.backgroundColor = NSColor.clear
contentRect.origin = NSPoint.zero contentRect.origin = NSPoint.zero
super.init(layout) super.init(layout)
window = panel window = panel
window?.delegate = self
currentLayout = layout currentLayout = layout
} }
@ -95,40 +89,36 @@ public class CtlCandidateTDK: CtlCandidate {
Self.thePool.reverseLookupResult = reverseLookupResult Self.thePool.reverseLookupResult = reverseLookupResult
} }
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
if #available(macOS 10.15, *) { window.backgroundColor = .clear
if isLegacyMode { viewCheck: if #available(macOS 10.15, *) {
updateNSWindowLegacy(window) if useCocoa {
return Self.currentView = theViewCocoa
break viewCheck
} }
window.isOpaque = false
window.backgroundColor = NSColor.clear
Self.currentView = NSHostingView(rootView: theView) Self.currentView = NSHostingView(rootView: theView)
let newSize = Self.currentView.fittingSize
window.contentView = Self.currentView
window.setContentSize(newSize)
} else { } else {
updateNSWindowLegacy(window) Self.currentView = theViewCocoa
} }
window.contentView = Self.currentView
window.setContentSize(Self.currentView.fittingSize)
} }
} }
func updateNSWindowLegacy(_ window: NSWindow) { // TODO:
window.isOpaque = true //
window.backgroundColor = NSColor.controlBackgroundColor override public func scrollWheel(with event: NSEvent) {
let viewToDraw = theViewLegacy guard useMouseScrolling else { return }
let coreSize = viewToDraw.fittingSize handleMouseScroll(deltaX: event.deltaX, deltaY: event.deltaY)
let padding: Double = 5 }
let outerSize: NSSize = .init(
width: coreSize.width + 2 * padding, func handleMouseScroll(deltaX: CGFloat, deltaY: CGFloat) {
height: coreSize.height + 2 * padding switch (deltaX, deltaY, Self.thePool.layout) {
) case (1..., 0, .horizontal), (0, 1..., .vertical): highlightNextCandidate()
let innerOrigin: NSPoint = .init(x: padding, y: padding) case (..<0, 0, .horizontal), (0, ..<0, .vertical): highlightPreviousCandidate()
let outerRect: NSRect = .init(origin: .zero, size: outerSize) case (0, 1..., .horizontal), (1..., 0, .vertical): showNextLine()
viewToDraw.setFrameOrigin(innerOrigin) case (0, ..<0, .horizontal), (..<0, 0, .vertical): showPreviousLine()
Self.currentView = NSView(frame: outerRect) case (_, _, _): break
Self.currentView.addSubview(viewToDraw) }
window.contentView = Self.currentView
window.setContentSize(outerSize)
} }
// Already implemented in CandidatePool. // Already implemented in CandidatePool.

View File

@ -0,0 +1,330 @@
// (c) 2022 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Cocoa
import Shared
/// Cocoa SwiftUI
public class VwrCandidateTDKCocoa: NSStackView {
public weak var controller: CtlCandidateTDK?
public var thePool: CandidatePool
// MARK: - Constructors.
public init(controller: CtlCandidateTDK? = nil, thePool pool: CandidatePool) {
self.controller = controller
thePool = pool
super.init(frame: .init(origin: .zero, size: .zero))
refresh()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Interface Renderer.
public extension VwrCandidateTDKCocoa {
func refresh() {
//
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)
}
if thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count > 0 {
thePool.lineRangeForFinalPageBlanked.enumerated().forEach { _ in
var theLine = [thePool.blankCell]
for _ in 1 ..< thePool.maxLineCapacity {
theLine.append(thePool.blankCell)
}
let vwrCurrentLine = generateLineContainer(&theLine)
candidateContainer.addView(vwrCurrentLine, in: isVerticalListing ? .top : .leading)
}
}
//
switch thePool.layout {
case .vertical:
let minHeight = Double(thePool.maxLineCapacity)
* drawCellCocoa(thePool.blankCell).fittingSize.height
candidateContainer.subviews.forEach { vwrCurrentLine in
Self.makeSimpleConstraint(item: vwrCurrentLine, attribute: .height, relation: .equal, value: minHeight)
}
case .horizontal where thePool.maxLinesPerPage > 1:
let containerWidth = candidateContainer.fittingSize.width
candidateContainer.subviews.forEach { vwrCurrentLine in
Self.makeSimpleConstraint(item: vwrCurrentLine, attribute: .width, relation: .equal, value: containerWidth)
}
default: break
}
let vwrPeripherals = Self.makeLabel(thePool.attributedDescriptionBottomPanes)
//
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
}
finalContainer.orientation = finalContainerOrientation
finalContainer.alignment = finalContainerOrientation == .vertical ? .leading : .centerY
finalContainer.addView(candidateContainer, in: .leading)
finalContainer.addView(vwrPeripherals, in: .leading)
//
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 ?? thePool.blankCell
let cellLabel = VwrCandidateCell(cell: theCell)
cellLabel.target = self
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 = thePool.cellWidth(theCell).min ?? wrappedCell.fittingSize.width
let cellHeight = wrappedCell.fittingSize.height
wrappedCell.setHuggingPriority(.fittingSizeCompression, for: .horizontal)
wrappedCell.setHuggingPriority(.fittingSizeCompression, for: .vertical)
wrappedCell.translatesAutoresizingMaskIntoConstraints = false
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)
}
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
let isCurrentLine = theLine.hasHighlightedCell
theLine.forEach { theCell in
vwrCurrentLine.addView(drawCellCocoa(theCell), in: isVerticalListing ? .top : .leading)
}
let lineBg = lineBackground(isCurrentLine: isCurrentLine, isMatrix: isMatrix)
vwrCurrentLine.wantsLayer = isCurrentLine && isMatrix
if vwrCurrentLine.wantsLayer {
vwrCurrentLine.layer?.backgroundColor = lineBg.cgColor
vwrCurrentLine.layer?.cornerRadius = 6
}
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 func makeSimpleConstraint(item: NSView, attribute: NSLayoutConstraint.Attribute, relation: NSLayoutConstraint.Relation, value: CGFloat) {
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: .zero))
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() {
let newMenu = NSMenu()
let boostMenuItem = NSMenuItem(
title: "\(cellData.displayedText)",
action: #selector(menuActionOfBoosting(_:)),
keyEquivalent: ""
)
let nerfMenuItem = NSMenuItem(
title: "\(cellData.displayedText)",
action: #selector(menuActionOfNerfing(_:)),
keyEquivalent: ""
)
let filterMenuItem = NSMenuItem(
title: "✖︎ \(cellData.displayedText)",
action: #selector(menuActionOfFiltering(_:)),
keyEquivalent: ""
)
boostMenuItem.target = self
nerfMenuItem.target = self
filterMenuItem.target = self
newMenu.addItem(boostMenuItem)
newMenu.addItem(nerfMenuItem)
newMenu.addItem(filterMenuItem)
theMenu = 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?.candidatePairSelected(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()
}
}

View File

@ -42,26 +42,11 @@ public struct VwrCandidateTDK: View {
mainViewVertical.background(candidateListBackground) mainViewVertical.background(candidateListBackground)
} }
if thePool.maxLinesPerPage > 1 || thePool.layout == .vertical { if thePool.maxLinesPerPage > 1 || thePool.layout == .vertical {
if controller?.delegate?.showReverseLookupResult ?? true, !tooltip.isEmpty { statusBarContent
ZStack(alignment: .leading) {
bottomPanelBackgroundTDK
.opacity(colorScheme == .dark ? 0 : 0.35)
reverseLookupPane.padding([.top, .horizontal], 4).padding([.bottom], 2)
}
}
statusBar.background(
bottomPanelBackgroundTDK
.opacity(colorScheme == .dark ? 0.35 : 0.5)
)
} }
} }
.frame(minWidth: windowWidth, maxWidth: windowWidth) .fixedSize()
.background(candidateListBackground) .background(candidateListBackground)
.overlay(
RoundedRectangle(cornerRadius: 10).stroke(
absoluteBackgroundColor.opacity(colorScheme == .dark ? 1 : 0.1), lineWidth: 0.5
)
)
.cornerRadius(10) .cornerRadius(10)
} }
} }
@ -70,20 +55,20 @@ public struct VwrCandidateTDK: View {
// MARK: - Main Views. // MARK: - Main Views.
@available(macOS 10.15, *) @available(macOS 10.15, *)
extension VwrCandidateTDK { private extension VwrCandidateTDK {
var mainViewHorizontal: some View { var mainViewHorizontal: some View {
ScrollView(.vertical, showsIndicators: false) { ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: 1.6) { VStack(alignment: .leading, spacing: 1.6) {
ForEach(thePool.lineRangeForCurrentPage, id: \.self) { rowIndex in ForEach(thePool.lineRangeForCurrentPage, id: \.self) { rowIndex in
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
lineBackground(lineID: rowIndex).cornerRadius(6) lineBackground(lineID: rowIndex).cornerRadius(6).frame(minWidth: minLineWidth)
HStack(spacing: horizontalCellSpacing) { HStack(spacing: horizontalCellSpacing) {
ForEach(Array(thePool.candidateLines[rowIndex]), id: \.self) { currentCandidate in ForEach(Array(thePool.candidateLines[rowIndex]), id: \.self) { currentCandidate in
drawCandidate(currentCandidate).fixedSize() drawCandidate(currentCandidate).fixedSize()
} }
.opacity(rowIndex == thePool.currentLineNumber ? 1 : 0.95)
} }
} }
.opacity(rowIndex == thePool.currentLineNumber ? 1 : 0.95)
.id(rowIndex) .id(rowIndex)
} }
if thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count > 0 { if thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count > 0 {
@ -102,9 +87,9 @@ extension VwrCandidateTDK {
} }
} }
} }
.fixedSize(horizontal: thePool.maxLinesPerPage == 1, vertical: true) .fixedSize()
.padding([.horizontal, .top], 5) .padding([.horizontal, .top], 5)
.padding([.bottom], thePool.maxLinesPerPage == 1 ? 5 : 2) .padding([.bottom], thePool.maxLinesPerPage == 1 ? 5 : 0)
} }
var mainViewVertical: some View { var mainViewVertical: some View {
@ -115,6 +100,7 @@ extension VwrCandidateTDK {
ForEach(Array(thePool.candidateLines[columnIndex]), id: \.self) { currentCandidate in ForEach(Array(thePool.candidateLines[columnIndex]), id: \.self) { currentCandidate in
drawCandidate(currentCandidate) drawCandidate(currentCandidate)
} }
.opacity(columnIndex == thePool.currentLineNumber ? 1 : 0.95)
if thePool.candidateLines[columnIndex].count < thePool.maxLineCapacity { if thePool.candidateLines[columnIndex].count < thePool.maxLineCapacity {
ForEach(0 ..< thePool.dummyCellsRequiredForCurrentLine, id: \.self) { _ in ForEach(0 ..< thePool.dummyCellsRequiredForCurrentLine, id: \.self) { _ in
drawCandidate(thePool.blankCell) drawCandidate(thePool.blankCell)
@ -122,11 +108,7 @@ extension VwrCandidateTDK {
} }
} }
.background(lineBackground(lineID: columnIndex)).cornerRadius(6) .background(lineBackground(lineID: columnIndex)).cornerRadius(6)
.opacity(columnIndex == thePool.currentLineNumber ? 1 : 0.95)
.frame( .frame(
minWidth: thePool.maxLinesPerPage == 1
? max(Double(CandidateCellData.unifiedSize * 6), 90)
: nil,
alignment: .topLeading alignment: .topLeading
) )
.id(columnIndex) .id(columnIndex)
@ -140,7 +122,7 @@ extension VwrCandidateTDK {
ForEach(0 ..< thePool.maxLineCapacity, id: \.self) { _ in ForEach(0 ..< thePool.maxLineCapacity, id: \.self) { _ in
attributedStringFor(cell: thePool.blankCell).fixedSize() attributedStringFor(cell: thePool.blankCell).fixedSize()
.frame( .frame(
width: ceil(thePool.blankCell.minWidthToDraw(isMatrix: true)), width: ceil(thePool.blankCell.cellLength(isMatrix: true)),
alignment: .topLeading alignment: .topLeading
) )
.contentShape(Rectangle()) .contentShape(Rectangle())
@ -161,7 +143,7 @@ extension VwrCandidateTDK {
} }
.fixedSize(horizontal: true, vertical: false) .fixedSize(horizontal: true, vertical: false)
.padding([.horizontal, .top], 5) .padding([.horizontal, .top], 5)
.padding([.bottom], thePool.maxLinesPerPage == 1 ? 5 : 2) .padding([.bottom], 0)
} }
} }
@ -171,7 +153,7 @@ extension VwrCandidateTDK {
extension VwrCandidateTDK { extension VwrCandidateTDK {
func drawCandidate(_ cell: CandidateCellData) -> some View { func drawCandidate(_ cell: CandidateCellData) -> some View {
attributedStringFor(cell: cell) attributedStringFor(cell: cell)
.frame(minWidth: cellWidth(cell).min, maxWidth: cellWidth(cell).max, alignment: .topLeading) .frame(minWidth: thePool.cellWidth(cell).min, maxWidth: thePool.cellWidth(cell).max, alignment: .topLeading)
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { didSelectCandidateAt(cell.index) } .onTapGesture { didSelectCandidateAt(cell.index) }
.contextMenu { .contextMenu {
@ -199,31 +181,20 @@ extension VwrCandidateTDK {
let isCurrentLineInMatrix = lineID == thePool.currentLineNumber && thePool.maxLinesPerPage != 1 let isCurrentLineInMatrix = lineID == thePool.currentLineNumber && thePool.maxLinesPerPage != 1
switch thePool.layout { switch thePool.layout {
case .horizontal where isCurrentLineInMatrix: case .horizontal where isCurrentLineInMatrix:
return Color.primary.opacity(0.05) return colorScheme == .dark ? Color.primary.opacity(0.05) : .white
case .vertical where isCurrentLineInMatrix: case .vertical where isCurrentLineInMatrix:
return absoluteBackgroundColor.opacity(0.15) return absoluteBackgroundColor.opacity(0.13)
default: default:
return Color.clear return Color.clear
} }
} }
func cellWidth(_ cell: CandidateCellData) -> (min: CGFloat?, max: CGFloat?) { var minLineWidth: CGFloat? {
let minAccepted = ceil(thePool.blankCell.minWidthToDraw(isMatrix: false) * 1.1)
let defaultMin: CGFloat = cell.minWidthToDraw(isMatrix: thePool.maxLinesPerPage != 1)
var min: CGFloat = defaultMin
if thePool.layout != .vertical, thePool.maxLinesPerPage == 1 {
min = max(minAccepted, cell.minWidthToDraw(isMatrix: false))
}
return (min, nil)
}
var windowWidth: CGFloat? {
let paddings: CGFloat = 10.0
let spacings: CGFloat = horizontalCellSpacing * Double(thePool.maxLineCapacity - 1) let spacings: CGFloat = horizontalCellSpacing * Double(thePool.maxLineCapacity - 1)
let maxWindowWith: CGFloat let maxWindowWith: CGFloat
= ceil( = ceil(
Double(thePool.maxLineCapacity) * (thePool.blankCell.minWidthToDraw()) Double(thePool.maxLineCapacity) * (thePool.blankCell.cellLength())
+ paddings + spacings + spacings
) )
return thePool.layout == .horizontal && thePool.maxLinesPerPage > 1 ? maxWindowWith : nil return thePool.layout == .horizontal && thePool.maxLinesPerPage > 1 ? maxWindowWith : nil
} }
@ -264,7 +235,7 @@ extension VwrCandidateTDK {
) )
) )
.padding([.horizontal], 2) .padding([.horizontal], 2)
.foregroundColor(.primary.opacity(0.8)) .foregroundColor(.primary.opacity(0.9))
}.fixedSize() }.fixedSize()
} }
@ -272,7 +243,7 @@ extension VwrCandidateTDK {
HStack { HStack {
if !tooltip.isEmpty { if !tooltip.isEmpty {
ZStack(alignment: .center) { ZStack(alignment: .center) {
Circle().fill(highlightBackgroundTDK.opacity(0.8)) Circle().fill(thePool.blankCell.themeColor.opacity(0.8))
Text(tooltip.first?.description ?? "").padding(2).font(.system(size: CandidateCellData.unifiedSize)) Text(tooltip.first?.description ?? "").padding(2).font(.system(size: CandidateCellData.unifiedSize))
}.frame(width: ceil(CandidateCellData.unifiedSize * 1.7), height: ceil(CandidateCellData.unifiedSize * 1.7)) }.frame(width: ceil(CandidateCellData.unifiedSize * 1.7), height: ceil(CandidateCellData.unifiedSize * 1.7))
} }
@ -281,7 +252,6 @@ extension VwrCandidateTDK {
if controller?.delegate?.showReverseLookupResult ?? true { if controller?.delegate?.showReverseLookupResult ?? true {
if !firstReverseLookupResult.isEmpty { if !firstReverseLookupResult.isEmpty {
ZStack(alignment: .center) { ZStack(alignment: .center) {
Color(white: colorScheme == .dark ? 0.2 : 0.9).cornerRadius(4)
Text(firstReverseLookupResult) Text(firstReverseLookupResult)
.font(.system(size: max(ceil(CandidateCellData.unifiedSize * 0.6), 9))) .font(.system(size: max(ceil(CandidateCellData.unifiedSize * 0.6), 9)))
.frame( .frame(
@ -307,16 +277,13 @@ extension VwrCandidateTDK {
if thePool.maxLinesPerPage == 1 { if thePool.maxLinesPerPage == 1 {
if !firstReverseLookupResult.isEmpty { if !firstReverseLookupResult.isEmpty {
ZStack(alignment: .center) { ZStack(alignment: .center) {
Color(white: colorScheme == .dark ? 0.3 : 0.9).cornerRadius(3)
Text("\(firstReverseLookupResult.trimmingCharacters(in: .newlines))") Text("\(firstReverseLookupResult.trimmingCharacters(in: .newlines))")
.lineLimit(1).padding([.horizontal], 2) .lineLimit(1).padding([.horizontal], 2)
}.fixedSize() }.fixedSize()
} }
} else { } else {
Text("").opacity(0.8)
ForEach(reverseLookupResult, id: \.self) { currentResult in ForEach(reverseLookupResult, id: \.self) { currentResult in
ZStack(alignment: .center) { ZStack(alignment: .center) {
Color(white: colorScheme == .dark ? 0.3 : 0.9).cornerRadius(3)
Text("\(currentResult.trimmingCharacters(in: .newlines))") Text("\(currentResult.trimmingCharacters(in: .newlines))")
.lineLimit(1).padding([.horizontal], 2) .lineLimit(1).padding([.horizontal], 2)
}.fixedSize() }.fixedSize()
@ -327,27 +294,22 @@ extension VwrCandidateTDK {
.foregroundColor(colorScheme == .light ? Color(white: 0.1) : Color(white: 0.9)) .foregroundColor(colorScheme == .light ? Color(white: 0.1) : Color(white: 0.9))
} }
var statusBar: some View { var statusBarContent: some View {
HStack(alignment: .center) { HStack(alignment: .center) {
positionLabelView
if !tooltip.isEmpty { if !tooltip.isEmpty {
Text(tooltip).lineLimit(1) Text(tooltip).lineLimit(1)
} else {
if controller?.delegate?.showReverseLookupResult ?? true, tooltip.isEmpty {
reverseLookupPane.padding(0)
}
} }
Spacer() if controller?.delegate?.showReverseLookupResult ?? true, !tooltip.isEmpty {
positionLabelView reverseLookupPane.padding(0)
}
Spacer(minLength: 0)
} }
.font(.system(size: max(ceil(CandidateCellData.unifiedSize * 0.7), 11), weight: .bold)) .font(.system(size: max(ceil(CandidateCellData.unifiedSize * 0.7), 11), weight: .bold))
.padding([.bottom, .horizontal], 7).padding([.top], 2) .padding([.bottom, .horizontal], 7).padding([.top], 2)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
} }
var highlightBackgroundTDK: Color {
tooltip.isEmpty ? Color(white: colorScheme == .dark ? 0.2 : 0.9) : thePool.blankCell.themeColor
}
var candidateListBackground: some View { var candidateListBackground: some View {
Group { Group {
absoluteBackgroundColor absoluteBackgroundColor
@ -367,10 +329,6 @@ extension VwrCandidateTDK {
} }
} }
var bottomPanelBackgroundTDK: Color {
Color(white: colorScheme == .dark ? 0.145 : 0.95)
}
func attributedStringFor(cell theCell: CandidateCellData) -> some View { func attributedStringFor(cell theCell: CandidateCellData) -> some View {
let defaultResult = theCell.attributedStringForSwiftUIBackports let defaultResult = theCell.attributedStringForSwiftUIBackports
if forceCatalinaCompatibility { if forceCatalinaCompatibility {
@ -386,7 +344,7 @@ extension VwrCandidateTDK {
// MARK: - Delegate Methods // MARK: - Delegate Methods
@available(macOS 10.15, *) @available(macOS 10.15, *)
extension VwrCandidateTDK { private extension VwrCandidateTDK {
func didSelectCandidateAt(_ pos: Int) { func didSelectCandidateAt(_ pos: Int) {
controller?.delegate?.candidatePairSelected(at: pos) controller?.delegate?.candidatePairSelected(at: pos)
} }
@ -401,7 +359,7 @@ extension VwrCandidateTDK {
import SwiftExtension import SwiftExtension
@available(macOS 10.15, *) @available(macOS 10.15, *)
struct AttributedLabel_Previews: PreviewProvider { struct VwrCandidateTDK_Previews: PreviewProvider {
@State static var testCandidates: [String] = [ @State static var testCandidates: [String] = [
"二十四歲是學生", "二十四歲", "昏睡紅茶", "食雪漢", "意味深", "學生", "便乗", "二十四歲是學生", "二十四歲", "昏睡紅茶", "食雪漢", "意味深", "學生", "便乗",
"🐂🍺🐂🍺", "🐃🍺", "🐂🍺", "🐃🐂🍺🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺", "🐂🍺🐂🍺", "🐃🍺", "🐂🍺", "🐃🐂🍺🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺",
@ -504,5 +462,30 @@ struct AttributedLabel_Previews: PreviewProvider {
} }
} }
} }
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()
}
}
}
}
} }
} }

View File

@ -136,7 +136,7 @@ public extension SessionCtl {
if #available(macOS 10.15, *) { if #available(macOS 10.15, *) {
if let ctlCandidateCurrent = candidateUI as? CtlCandidateTDK { if let ctlCandidateCurrent = candidateUI as? CtlCandidateTDK {
ctlCandidateCurrent.isLegacyMode = PrefMgr.shared.legacyCandidateViewTypesettingMethodEnabled ctlCandidateCurrent.useCocoa = PrefMgr.shared.legacyCandidateViewTypesettingMethodEnabled
} }
} }