From 949f140671beea89f21314552107442999f7f712 Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Sat, 25 Feb 2023 15:38:22 +0800 Subject: [PATCH] TDKCandidates // Rewrite the UI using NSStackView. * The mouse wheel scrolling operation currently gives a disastrous experience with TrackPad, hence getting disabled for now. --- .../CandidateCellData_Core.swift | 142 ++++---- .../CandidateWindow/CandidatePool.swift | 20 +- .../CandidatePool_CocoaImpl.swift | 102 +++--- .../TDKCandidates/CtlCandidateTDK.swift | 68 ++-- .../TDKCandidates/VwrCandidateTDK_Cocoa.swift | 330 ++++++++++++++++++ ...DK.swift => VwrCandidateTDK_SwiftUI.swift} | 119 +++---- Source/Modules/SessionCtl_HandleDisplay.swift | 2 +- 7 files changed, 552 insertions(+), 231 deletions(-) create mode 100644 Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_Cocoa.swift rename Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/{VwrCandidateTDK.swift => VwrCandidateTDK_SwiftUI.swift} (83%) diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidateCellData_Core.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidateCellData_Core.swift index 77fc823a..ee368585 100644 --- a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidateCellData_Core.swift +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidateCellData_Core.swift @@ -30,12 +30,13 @@ public class CandidateCellData: Hashable { 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 } 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) { self.key = key self.displayedText = displayedText @@ -52,11 +53,60 @@ public class CandidateCellData: Hashable { } 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 } - 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 = { let paraStyle = NSMutableParagraphStyle() paraStyle.setParagraphStyle(NSParagraphStyle.default) @@ -65,65 +115,28 @@ public class CandidateCellData: Hashable { return paraStyle }() - var phraseFont: NSFont { - CTFontCreateUIFontForLanguage(.system, size, locale as CFString) ?? NSFont.systemFont(ofSize: size) - } - - var highlightedNSColor: NSColor { - var result = NSColor.alternateSelectedControlColor - var colorBlendAmount: Double = NSApplication.isDarkMode ? 0.3 : 0.0 - if #available(macOS 10.14, *), !NSApplication.isDarkMode, locale == "zh-Hant" { - colorBlendAmount = 0.15 - } - // 設定當前高亮候選字的背景顏色。 - switch locale { - case "zh-Hans": - result = NSColor.systemRed - case "zh-Hant": - result = NSColor.systemBlue - case "ja": - result = NSColor.systemBrown - default: break - } - var blendingAgainstTarget: NSColor = NSApplication.isDarkMode ? NSColor.black : NSColor.white - if #unavailable(macOS 10.14) { - colorBlendAmount = 0.3 - blendingAgainstTarget = NSColor.white - } - return result.blended(withFraction: colorBlendAmount, of: blendingAgainstTarget)! - } - - public var attributedStringForLengthCalculation: NSAttributedString { - let attrCandidate: [NSAttributedString.Key: AnyObject] = [ - .font: NSFont.monospacedDigitSystemFont(ofSize: size, weight: .regular), - .paragraphStyle: Self.sharedParagraphStyle, - ] - let attrStrCandidate = NSAttributedString(string: displayedText + " ", attributes: attrCandidate) - return attrStrCandidate - } - public func attributedString( noSpacePadding: Bool = true, withHighlight: Bool = false, isMatrix: Bool = false ) -> NSAttributedString { - let attrCandidate: [NSAttributedString.Key: AnyObject] = [ - .font: NSFont.monospacedDigitSystemFont(ofSize: size, weight: .regular), + let attrSpace: [NSAttributedString.Key: AnyObject] = [ + .font: phraseFont(size: size), .paragraphStyle: Self.sharedParagraphStyle, ] let result: NSMutableAttributedString = { if noSpacePadding { - let resultNeo = NSMutableAttributedString(string: " ", attributes: attrCandidate) + let resultNeo = NSMutableAttributedString(string: " ", attributes: attrSpace) resultNeo.insert(attributedStringPhrase(isMatrix: isMatrix), at: 1) resultNeo.insert(attributedStringHeader, at: 0) return resultNeo } - let resultNeo = NSMutableAttributedString(string: " ", attributes: attrCandidate) + let resultNeo = NSMutableAttributedString(string: " ", attributes: attrSpace) resultNeo.insert(attributedStringPhrase(isMatrix: isMatrix), at: 2) resultNeo.insert(attributedStringHeader, at: 1) return resultNeo }() if withHighlight, isHighlighted { result.addAttribute( - .backgroundColor, value: highlightedNSColor, + .backgroundColor, value: themeColorCocoa, range: NSRange(location: 0, length: result.string.utf16.count) ) } @@ -131,35 +144,21 @@ public class CandidateCellData: Hashable { } public var attributedStringHeader: NSAttributedString { - let theFontForCandidateKey: NSFont = { - if #available(macOS 10.15, *) { - return NSFont.monospacedSystemFont(ofSize: fontSizeKey, weight: .regular) - } - return NSFont.monospacedDigitSystemFont(ofSize: fontSizeKey, weight: .regular) - }() - var attrKey: [NSAttributedString.Key: AnyObject] = [ - .font: theFontForCandidateKey, + let attrKey: [NSAttributedString.Key: AnyObject] = [ + .font: selectionKeyFont(size: fontSizeKey), .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) return attrStrKey } public func attributedStringPhrase(isMatrix: Bool = false) -> NSAttributedString { var attrCandidate: [NSAttributedString.Key: AnyObject] = [ - .font: phraseFont, + .font: phraseFont(size: size), .paragraphStyle: Self.sharedParagraphStyle, + .foregroundColor: fontColorCandidate, ] - if isHighlighted { - attrCandidate[.foregroundColor] = NSColor.white - } else { - attrCandidate[.foregroundColor] = NSColor.labelColor - } if #available(macOS 12, *) { if UserDefaults.standard.bool( forKey: UserDef.kLegacyCandidateViewTypesettingMethodEnabled.rawValue @@ -184,8 +183,15 @@ public class CandidateCellData: Hashable { return String(format: "U+%02X %@", $0.value, theName) } } +} - public func minWidthToDraw(isMatrix: Bool = true) -> Double { - cellLength(isMatrix: isMatrix) + ceil(fontSizeKey * 0.1) +// MARK: - Array Container Extension. + +public extension Array where Element == CandidateCellData { + var hasHighlightedCell: Bool { + for neta in self { + if neta.isHighlighted { return true } + } + return false } } diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool.swift index c7101ea4..b00f731c 100644 --- a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool.swift +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool.swift @@ -12,6 +12,7 @@ import Shared /// 候選字窗會用到的資料池單位,即用即拋。 public struct CandidatePool { public let blankCell: CandidateCellData + public let shitCell: CandidateCellData // 只用來測量單漢字候選字 cell 的最大可能寬度。 public let maxLinesPerPage: Int public let layout: LayoutOrientation 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 { @@ -82,6 +83,7 @@ public struct CandidatePool { self.layout = layout maxLinesPerPage = max(1, lines) blankCell = CandidateCellData(key: " ", displayedText: " ", isSelected: false) + shitCell = CandidateCellData(key: " ", displayedText: "💩", isSelected: false) blankCell.locale = locale self.selectionKeys = selectionKeys.isEmpty ? "123456789" : selectionKeys var allCandidates = candidates.map { CandidateCellData(key: " ", displayedText: $0) } @@ -94,7 +96,7 @@ public struct CandidatePool { var isOverflown: Bool = (currentColumn.count == maxLineCapacity) && !currentColumn.isEmpty if layout == .horizontal { isOverflown = isOverflown - || currentColumn.map { $0.cellLength() }.reduce(0, +) >= maxRowWidth - candidate.cellLength() + || currentColumn.map { $0.cellLength() }.reduce(0, +) > maxRowWidth - candidate.cellLength() } if isOverflown { candidateLines.append(currentColumn) @@ -216,9 +218,21 @@ public extension CandidatePool { 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 { enum VerticalDirection { diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool_CocoaImpl.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool_CocoaImpl.swift index b9effd43..1cc15734 100644 --- a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool_CocoaImpl.swift +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool_CocoaImpl.swift @@ -8,7 +8,11 @@ import Cocoa +// MARK: - Using One Single NSAttributedString. + extension CandidatePool { + // MARK: Candidate List with Peripherals. + public var attributedDescription: NSAttributedString { switch layout { case .horizontal: return attributedDescriptionHorizontal @@ -16,13 +20,12 @@ extension CandidatePool { } } - /// 將當前資料池以橫版的形式列印成 NSAttributedString。 + private var sharedParagraphStyle: NSParagraphStyle { CandidateCellData.sharedParagraphStyle } + private var attributedDescriptionHorizontal: NSAttributedString { - let paragraphStyle = CandidateCellData.sharedParagraphStyle as! NSMutableParagraphStyle - paragraphStyle.lineSpacing = ceil(blankCell.size * 0.3) - paragraphStyle.lineBreakStrategy = .pushOut + let paragraphStyle = sharedParagraphStyle let attrCandidate: [NSAttributedString.Key: AnyObject] = [ - .font: NSFont.monospacedDigitSystemFont(ofSize: blankCell.size, weight: .regular), + .font: blankCell.phraseFont(size: blankCell.size), .paragraphStyle: paragraphStyle, ] let result = NSMutableAttributedString(string: "", attributes: attrCandidate) @@ -59,11 +62,9 @@ extension CandidatePool { } private var attributedDescriptionVertical: NSAttributedString { - let paragraphStyle = CandidateCellData.sharedParagraphStyle as! NSMutableParagraphStyle - paragraphStyle.lineSpacing = ceil(blankCell.size * 0.3) - paragraphStyle.lineBreakStrategy = .pushOut + let paragraphStyle = sharedParagraphStyle let attrCandidate: [NSAttributedString.Key: AnyObject] = [ - .font: NSFont.monospacedDigitSystemFont(ofSize: blankCell.size, weight: .regular), + .font: blankCell.phraseFont(size: blankCell.size), .paragraphStyle: paragraphStyle, ] let result = NSMutableAttributedString(string: "", attributes: attrCandidate) @@ -90,7 +91,7 @@ extension CandidatePool { if currentCell.isHighlighted { spacer.addAttribute( .backgroundColor, - value: currentCell.highlightedNSColor, + value: currentCell.themeColorCocoa, range: .init(location: 0, length: spacer.string.utf16.count) ) } else { @@ -109,63 +110,60 @@ extension CandidatePool { return result } - private var attributedDescriptionBottomPanes: NSAttributedString { - let paragraphStyle = CandidateCellData.sharedParagraphStyle as! NSMutableParagraphStyle - paragraphStyle.lineSpacing = ceil(blankCell.size * 0.3) - paragraphStyle.lineBreakStrategy = .pushOut - let attrCandidate: [NSAttributedString.Key: AnyObject] = [ - .font: NSFont.monospacedDigitSystemFont(ofSize: blankCell.size, weight: .regular), - .paragraphStyle: paragraphStyle, - ] - let result = NSMutableAttributedString(string: "", attributes: attrCandidate) + // MARK: Peripherals + + public var attributedDescriptionBottomPanes: NSAttributedString { + let paragraphStyle = sharedParagraphStyle + let result = NSMutableAttributedString(string: "") + result.append(attributedDescriptionPositionCounter) + if !tooltip.isEmpty { result.append(attributedDescriptionTooltip) } + if !reverseLookupResult.isEmpty { result.append(attributedDescriptionReverseLookp) } + result.addAttribute(.paragraphStyle, value: paragraphStyle, range: .init(location: 0, length: result.string.utf16.count)) + return result + } + + private var attributedDescriptionPositionCounter: NSAttributedString { let positionCounterColorBG = NSApplication.isDarkMode ? NSColor(white: 0.215, alpha: 0.7) : NSColor(white: 0.9, alpha: 0.7) let positionCounterColorText = NSColor.controlTextColor let positionCounterTextSize = max(ceil(CandidateCellData.unifiedSize * 0.7), 11) let attrPositionCounter: [NSAttributedString.Key: AnyObject] = [ - .font: NSFont.monospacedDigitSystemFont(ofSize: positionCounterTextSize, weight: .bold), - .paragraphStyle: paragraphStyle, + .font: blankCell.phraseFontEmphasized(size: positionCounterTextSize), .backgroundColor: positionCounterColorBG, .foregroundColor: positionCounterColorText, ] let positionCounter = NSAttributedString( string: " \(currentPositionLabelText) ", attributes: attrPositionCounter ) - result.append(positionCounter) + return positionCounter + } - if !tooltip.isEmpty { - let attrTooltip: [NSAttributedString.Key: AnyObject] = [ - .font: NSFont.monospacedDigitSystemFont(ofSize: positionCounterTextSize, weight: .regular), - .paragraphStyle: paragraphStyle, - ] - let tooltipText = NSAttributedString( - string: " \(tooltip) ", attributes: attrTooltip - ) - result.append(tooltipText) - } + private var attributedDescriptionTooltip: NSAttributedString { + let positionCounterTextSize = max(ceil(CandidateCellData.unifiedSize * 0.7), 11) + let attrTooltip: [NSAttributedString.Key: AnyObject] = [ + .font: blankCell.phraseFontEmphasized(size: positionCounterTextSize), + ] + let tooltipText = NSAttributedString( + string: " \(tooltip) ", attributes: attrTooltip + ) + return tooltipText + } - if !reverseLookupResult.isEmpty { - let reverseLookupTextSize = max(ceil(CandidateCellData.unifiedSize * 0.6), 9) - let reverseLookupColorBG = NSApplication.isDarkMode - ? NSColor(white: 0.1, alpha: 1) - : NSColor(white: 0.9, alpha: 1) - let attrReverseLookup: [NSAttributedString.Key: AnyObject] = [ - .font: NSFont.monospacedDigitSystemFont(ofSize: reverseLookupTextSize, weight: .regular), - .paragraphStyle: paragraphStyle, - .backgroundColor: reverseLookupColorBG, - ] - let attrReverseLookupSpacer: [NSAttributedString.Key: AnyObject] = [ - .font: NSFont.monospacedDigitSystemFont(ofSize: reverseLookupTextSize, weight: .regular), - .paragraphStyle: paragraphStyle, - ] - for neta in reverseLookupResult { - result.append(NSAttributedString(string: " ", attributes: attrReverseLookupSpacer)) - result.append(NSAttributedString(string: " \(neta) ", attributes: attrReverseLookup)) - if maxLinesPerPage == 1 { break } - } + private var attributedDescriptionReverseLookp: NSAttributedString { + let reverseLookupTextSize = max(ceil(CandidateCellData.unifiedSize * 0.6), 9) + let attrReverseLookup: [NSAttributedString.Key: AnyObject] = [ + .font: blankCell.phraseFont(size: reverseLookupTextSize), + ] + let attrReverseLookupSpacer: [NSAttributedString.Key: AnyObject] = [ + .font: blankCell.phraseFont(size: reverseLookupTextSize), + ] + let result = NSMutableAttributedString(string: "", attributes: attrReverseLookupSpacer) + 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 } } diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/CtlCandidateTDK.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/CtlCandidateTDK.swift index 8dad9851..5eb476da 100644 --- a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/CtlCandidateTDK.swift +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/CtlCandidateTDK.swift @@ -25,9 +25,10 @@ private extension NSUserInterfaceLayoutOrientation { } } -public class CtlCandidateTDK: CtlCandidate { +public class CtlCandidateTDK: CtlCandidate, NSWindowDelegate { 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 currentView: NSView = .init() @@ -38,15 +39,8 @@ public class CtlCandidateTDK: CtlCandidate { ).edgesIgnoringSafeArea(.top) } - private var theViewLegacy: NSView { - let textField = NSTextField( - labelWithAttributedString: Self.thePool.attributedDescription - ) - textField.isSelectable = false - textField.allowsEditingTextAttributes = false - textField.preferredMaxLayoutWidth = textField.frame.width - textField.backgroundColor = .controlBackgroundColor - return textField + private var theViewCocoa: NSStackView { + VwrCandidateTDKCocoa(controller: self, thePool: Self.thePool) } // MARK: - Constructors @@ -59,12 +53,12 @@ public class CtlCandidateTDK: CtlCandidate { ) panel.level = NSWindow.Level(Int(max(CGShieldingWindowLevel(), kCGPopUpMenuWindowLevel)) + 2) panel.hasShadow = true - panel.isOpaque = false panel.backgroundColor = NSColor.clear contentRect.origin = NSPoint.zero super.init(layout) window = panel + window?.delegate = self currentLayout = layout } @@ -95,40 +89,36 @@ public class CtlCandidateTDK: CtlCandidate { Self.thePool.reverseLookupResult = reverseLookupResult } DispatchQueue.main.async { [self] in - if #available(macOS 10.15, *) { - if isLegacyMode { - updateNSWindowLegacy(window) - return + window.backgroundColor = .clear + viewCheck: if #available(macOS 10.15, *) { + if useCocoa { + Self.currentView = theViewCocoa + break viewCheck } - window.isOpaque = false - window.backgroundColor = NSColor.clear Self.currentView = NSHostingView(rootView: theView) - let newSize = Self.currentView.fittingSize - window.contentView = Self.currentView - window.setContentSize(newSize) } else { - updateNSWindowLegacy(window) + Self.currentView = theViewCocoa } + window.contentView = Self.currentView + window.setContentSize(Self.currentView.fittingSize) } } - func updateNSWindowLegacy(_ window: NSWindow) { - window.isOpaque = true - window.backgroundColor = NSColor.controlBackgroundColor - let viewToDraw = theViewLegacy - let coreSize = viewToDraw.fittingSize - let padding: Double = 5 - let outerSize: NSSize = .init( - width: coreSize.width + 2 * padding, - height: coreSize.height + 2 * padding - ) - let innerOrigin: NSPoint = .init(x: padding, y: padding) - let outerRect: NSRect = .init(origin: .zero, size: outerSize) - viewToDraw.setFrameOrigin(innerOrigin) - Self.currentView = NSView(frame: outerRect) - Self.currentView.addSubview(viewToDraw) - window.contentView = Self.currentView - window.setContentSize(outerSize) + // TODO: 滑鼠滾輪操作的體驗不該是這個鬼樣子,今後還得再重新設計。 + // 這裡暫時先僅實裝給單行單列模式 + override public func scrollWheel(with event: NSEvent) { + guard useMouseScrolling else { return } + handleMouseScroll(deltaX: event.deltaX, deltaY: event.deltaY) + } + + func handleMouseScroll(deltaX: CGFloat, deltaY: CGFloat) { + switch (deltaX, deltaY, Self.thePool.layout) { + case (1..., 0, .horizontal), (0, 1..., .vertical): highlightNextCandidate() + case (..<0, 0, .horizontal), (0, ..<0, .vertical): highlightPreviousCandidate() + case (0, 1..., .horizontal), (1..., 0, .vertical): showNextLine() + case (0, ..<0, .horizontal), (..<0, 0, .vertical): showPreviousLine() + case (_, _, _): break + } } // Already implemented in CandidatePool. diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_Cocoa.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_Cocoa.swift new file mode 100644 index 00000000..540dac92 --- /dev/null +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_Cocoa.swift @@ -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() + } +} diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_SwiftUI.swift similarity index 83% rename from Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK.swift rename to Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_SwiftUI.swift index 88f779f0..88922dc2 100644 --- a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK.swift +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_SwiftUI.swift @@ -42,26 +42,11 @@ public struct VwrCandidateTDK: View { mainViewVertical.background(candidateListBackground) } if thePool.maxLinesPerPage > 1 || thePool.layout == .vertical { - if controller?.delegate?.showReverseLookupResult ?? true, !tooltip.isEmpty { - ZStack(alignment: .leading) { - bottomPanelBackgroundTDK - .opacity(colorScheme == .dark ? 0 : 0.35) - reverseLookupPane.padding([.top, .horizontal], 4).padding([.bottom], 2) - } - } - statusBar.background( - bottomPanelBackgroundTDK - .opacity(colorScheme == .dark ? 0.35 : 0.5) - ) + statusBarContent } } - .frame(minWidth: windowWidth, maxWidth: windowWidth) + .fixedSize() .background(candidateListBackground) - .overlay( - RoundedRectangle(cornerRadius: 10).stroke( - absoluteBackgroundColor.opacity(colorScheme == .dark ? 1 : 0.1), lineWidth: 0.5 - ) - ) .cornerRadius(10) } } @@ -70,20 +55,20 @@ public struct VwrCandidateTDK: View { // MARK: - Main Views. @available(macOS 10.15, *) -extension VwrCandidateTDK { +private extension VwrCandidateTDK { var mainViewHorizontal: some View { ScrollView(.vertical, showsIndicators: false) { VStack(alignment: .leading, spacing: 1.6) { ForEach(thePool.lineRangeForCurrentPage, id: \.self) { rowIndex in ZStack(alignment: .leading) { - lineBackground(lineID: rowIndex).cornerRadius(6) + lineBackground(lineID: rowIndex).cornerRadius(6).frame(minWidth: minLineWidth) HStack(spacing: horizontalCellSpacing) { ForEach(Array(thePool.candidateLines[rowIndex]), id: \.self) { currentCandidate in drawCandidate(currentCandidate).fixedSize() } + .opacity(rowIndex == thePool.currentLineNumber ? 1 : 0.95) } } - .opacity(rowIndex == thePool.currentLineNumber ? 1 : 0.95) .id(rowIndex) } 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([.bottom], thePool.maxLinesPerPage == 1 ? 5 : 2) + .padding([.bottom], thePool.maxLinesPerPage == 1 ? 5 : 0) } var mainViewVertical: some View { @@ -115,6 +100,7 @@ extension VwrCandidateTDK { ForEach(Array(thePool.candidateLines[columnIndex]), id: \.self) { currentCandidate in drawCandidate(currentCandidate) } + .opacity(columnIndex == thePool.currentLineNumber ? 1 : 0.95) if thePool.candidateLines[columnIndex].count < thePool.maxLineCapacity { ForEach(0 ..< thePool.dummyCellsRequiredForCurrentLine, id: \.self) { _ in drawCandidate(thePool.blankCell) @@ -122,11 +108,7 @@ extension VwrCandidateTDK { } } .background(lineBackground(lineID: columnIndex)).cornerRadius(6) - .opacity(columnIndex == thePool.currentLineNumber ? 1 : 0.95) .frame( - minWidth: thePool.maxLinesPerPage == 1 - ? max(Double(CandidateCellData.unifiedSize * 6), 90) - : nil, alignment: .topLeading ) .id(columnIndex) @@ -140,7 +122,7 @@ extension VwrCandidateTDK { ForEach(0 ..< thePool.maxLineCapacity, id: \.self) { _ in attributedStringFor(cell: thePool.blankCell).fixedSize() .frame( - width: ceil(thePool.blankCell.minWidthToDraw(isMatrix: true)), + width: ceil(thePool.blankCell.cellLength(isMatrix: true)), alignment: .topLeading ) .contentShape(Rectangle()) @@ -161,7 +143,7 @@ extension VwrCandidateTDK { } .fixedSize(horizontal: true, vertical: false) .padding([.horizontal, .top], 5) - .padding([.bottom], thePool.maxLinesPerPage == 1 ? 5 : 2) + .padding([.bottom], 0) } } @@ -171,7 +153,7 @@ extension VwrCandidateTDK { extension VwrCandidateTDK { func drawCandidate(_ cell: CandidateCellData) -> some View { 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()) .onTapGesture { didSelectCandidateAt(cell.index) } .contextMenu { @@ -199,31 +181,20 @@ extension VwrCandidateTDK { let isCurrentLineInMatrix = lineID == thePool.currentLineNumber && thePool.maxLinesPerPage != 1 switch thePool.layout { case .horizontal where isCurrentLineInMatrix: - return Color.primary.opacity(0.05) + return colorScheme == .dark ? Color.primary.opacity(0.05) : .white case .vertical where isCurrentLineInMatrix: - return absoluteBackgroundColor.opacity(0.15) + return absoluteBackgroundColor.opacity(0.13) default: return Color.clear } } - func cellWidth(_ cell: CandidateCellData) -> (min: CGFloat?, max: CGFloat?) { - let minAccepted = ceil(thePool.blankCell.minWidthToDraw(isMatrix: false) * 1.1) - let defaultMin: CGFloat = cell.minWidthToDraw(isMatrix: thePool.maxLinesPerPage != 1) - var min: CGFloat = defaultMin - if thePool.layout != .vertical, thePool.maxLinesPerPage == 1 { - min = max(minAccepted, cell.minWidthToDraw(isMatrix: false)) - } - return (min, nil) - } - - var windowWidth: CGFloat? { - let paddings: CGFloat = 10.0 + var minLineWidth: CGFloat? { let spacings: CGFloat = horizontalCellSpacing * Double(thePool.maxLineCapacity - 1) let maxWindowWith: CGFloat = ceil( - Double(thePool.maxLineCapacity) * (thePool.blankCell.minWidthToDraw()) - + paddings + spacings + Double(thePool.maxLineCapacity) * (thePool.blankCell.cellLength()) + + spacings ) return thePool.layout == .horizontal && thePool.maxLinesPerPage > 1 ? maxWindowWith : nil } @@ -264,7 +235,7 @@ extension VwrCandidateTDK { ) ) .padding([.horizontal], 2) - .foregroundColor(.primary.opacity(0.8)) + .foregroundColor(.primary.opacity(0.9)) }.fixedSize() } @@ -272,7 +243,7 @@ extension VwrCandidateTDK { HStack { if !tooltip.isEmpty { 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)) }.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 !firstReverseLookupResult.isEmpty { ZStack(alignment: .center) { - Color(white: colorScheme == .dark ? 0.2 : 0.9).cornerRadius(4) Text(firstReverseLookupResult) .font(.system(size: max(ceil(CandidateCellData.unifiedSize * 0.6), 9))) .frame( @@ -307,16 +277,13 @@ extension VwrCandidateTDK { if thePool.maxLinesPerPage == 1 { if !firstReverseLookupResult.isEmpty { ZStack(alignment: .center) { - Color(white: colorScheme == .dark ? 0.3 : 0.9).cornerRadius(3) Text("\(firstReverseLookupResult.trimmingCharacters(in: .newlines))") .lineLimit(1).padding([.horizontal], 2) }.fixedSize() } } else { - Text("→").opacity(0.8) ForEach(reverseLookupResult, id: \.self) { currentResult in ZStack(alignment: .center) { - Color(white: colorScheme == .dark ? 0.3 : 0.9).cornerRadius(3) Text("\(currentResult.trimmingCharacters(in: .newlines))") .lineLimit(1).padding([.horizontal], 2) }.fixedSize() @@ -327,27 +294,22 @@ extension VwrCandidateTDK { .foregroundColor(colorScheme == .light ? Color(white: 0.1) : Color(white: 0.9)) } - var statusBar: some View { + var statusBarContent: some View { HStack(alignment: .center) { + positionLabelView if !tooltip.isEmpty { Text(tooltip).lineLimit(1) - } else { - if controller?.delegate?.showReverseLookupResult ?? true, tooltip.isEmpty { - reverseLookupPane.padding(0) - } } - Spacer() - positionLabelView + if controller?.delegate?.showReverseLookupResult ?? true, !tooltip.isEmpty { + reverseLookupPane.padding(0) + } + Spacer(minLength: 0) } .font(.system(size: max(ceil(CandidateCellData.unifiedSize * 0.7), 11), weight: .bold)) .padding([.bottom, .horizontal], 7).padding([.top], 2) .fixedSize(horizontal: false, vertical: true) } - var highlightBackgroundTDK: Color { - tooltip.isEmpty ? Color(white: colorScheme == .dark ? 0.2 : 0.9) : thePool.blankCell.themeColor - } - var candidateListBackground: some View { Group { 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 { let defaultResult = theCell.attributedStringForSwiftUIBackports if forceCatalinaCompatibility { @@ -386,7 +344,7 @@ extension VwrCandidateTDK { // MARK: - Delegate Methods @available(macOS 10.15, *) -extension VwrCandidateTDK { +private extension VwrCandidateTDK { func didSelectCandidateAt(_ pos: Int) { controller?.delegate?.candidatePairSelected(at: pos) } @@ -401,7 +359,7 @@ extension VwrCandidateTDK { import SwiftExtension @available(macOS 10.15, *) -struct AttributedLabel_Previews: PreviewProvider { +struct VwrCandidateTDK_Previews: PreviewProvider { @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() + } + } + } + } } } diff --git a/Source/Modules/SessionCtl_HandleDisplay.swift b/Source/Modules/SessionCtl_HandleDisplay.swift index 9004157a..d22fc284 100644 --- a/Source/Modules/SessionCtl_HandleDisplay.swift +++ b/Source/Modules/SessionCtl_HandleDisplay.swift @@ -136,7 +136,7 @@ public extension SessionCtl { if #available(macOS 10.15, *) { if let ctlCandidateCurrent = candidateUI as? CtlCandidateTDK { - ctlCandidateCurrent.isLegacyMode = PrefMgr.shared.legacyCandidateViewTypesettingMethodEnabled + ctlCandidateCurrent.useCocoa = PrefMgr.shared.legacyCandidateViewTypesettingMethodEnabled } }