From 4eacbf6f8c56c6d5683eba29aa4e45d802854d4a Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Wed, 20 Sep 2023 18:07:03 +0800 Subject: [PATCH] TDKCandidates // Removing SwiftUI support for now. - SwiftUI is not suitable for necessities like writing a performance-critical candidate window. --- .../TDKCandidates/CtlCandidateTDK.swift | 48 +- .../TDKCandidates/VwrCandidateTDK_Cocoa.swift | 421 -------------- .../VwrCandidateTDK_SwiftUI.swift | 512 ------------------ 3 files changed, 2 insertions(+), 979 deletions(-) delete mode 100644 Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_Cocoa.swift delete mode 100644 Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_SwiftUI.swift diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/CtlCandidateTDK.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/CtlCandidateTDK.swift index 67930a46..db667128 100644 --- a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/CtlCandidateTDK.swift +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/CtlCandidateTDK.swift @@ -9,8 +9,6 @@ import AppKit import CocoaExtension import Shared -import SwiftExtension -import SwiftUI private extension NSUserInterfaceLayoutOrientation { var layoutTDK: CandidatePool.LayoutOrientation { @@ -38,20 +36,6 @@ public class CtlCandidateTDK: CtlCandidate, NSWindowDelegate { } } - @available(macOS 10.15, *) - private var theView: some View { - VwrCandidateTDK( - controller: self, thePool: Self.thePool - ).edgesIgnoringSafeArea(.top) - } - - #if USING_STACK_VIEW_IN_TDK_COCOA - /// 該視圖模式因算法陳舊而不再維護。 - private var theViewCocoa: NSStackView { - VwrCandidateTDKCocoa(controller: self, thePool: Self.thePool) - } - #endif - private var theViewAppKit: NSView { VwrCandidateTDKAppKit(controller: self, thePool: Self.thePool) } @@ -121,47 +105,19 @@ public class CtlCandidateTDK: CtlCandidate, NSWindowDelegate { delegate?.candidatePairHighlightChanged(at: highlightedIndex) DispatchQueue.main.async { [weak self] in guard let self = self else { return } - updateNSWindowModern(window) + self.updateNSWindowModern(window) } } func updateNSWindowModern(_ window: NSWindow) { + Self.currentView = theViewAppKit window.isOpaque = false window.backgroundColor = .clear - viewCheck: do { - viewCheckCatalina: if #available(macOS 10.15, *) { - if useCocoa { break viewCheckCatalina } - Self.thePool.update() - Self.currentView = NSHostingView(rootView: theView) - break viewCheck - } - Self.currentView = theViewAppKit - } window.contentView = Self.currentView window.setContentSize(Self.currentView.fittingSize) delegate?.resetCandidateWindowOrigin() } - func updateNSWindowLegacy(_ window: NSWindow) { - window.isOpaque = true - window.backgroundColor = NSColor.controlBackgroundColor - let viewToDraw = theViewLegacy - let coreSize = viewToDraw.fittingSize - let padding: Double = 5 - let outerSize: NSSize = .init( - width: coreSize.width + 2 * padding, - height: coreSize.height + 2 * padding - ) - let innerOrigin: NSPoint = .init(x: padding, y: padding) - let outerRect: NSRect = .init(origin: .zero, size: outerSize) - viewToDraw.setFrameOrigin(innerOrigin) - Self.currentView = NSView(frame: outerRect) - Self.currentView.addSubview(viewToDraw) - window.contentView = Self.currentView - window.setContentSize(outerSize) - delegate?.resetCandidateWindowOrigin() - } - override public func scrollWheel(with event: NSEvent) { guard useMouseScrolling else { return } handleMouseScroll(deltaX: event.deltaX, deltaY: event.deltaY) diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_Cocoa.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_Cocoa.swift deleted file mode 100644 index f03d17d4..00000000 --- a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_Cocoa.swift +++ /dev/null @@ -1,421 +0,0 @@ -// (c) 2022 and onwards The vChewing Project (MIT-NTL License). -// ==================== -// This code is released under the MIT license (SPDX-License-Identifier: MIT) -// ... with NTL restriction stating that: -// No trademark license is granted to use the trade names, trademarks, service -// marks, or product names of Contributor, except as required to fulfill notice -// requirements defined in MIT License. - -#if USING_STACK_VIEW_IN_TDK_COCOA - - import AppKit - import Shared - - /// 田所選字窗的 Cocoa 版本,繪製效率不受 SwiftUI 的限制。 - public class VwrCandidateTDKCocoa: NSStackView { - public weak var controller: CtlCandidateTDK? - public var thePool: CandidatePool - private var lineDimension: CGSize = .zero - private var candidateAreaDimension: CGSize = .zero - - // MARK: - Constructors. - - public init(controller: CtlCandidateTDK? = nil, thePool pool: CandidatePool) { - self.controller = controller - thePool = pool - super.init(frame: .init(origin: .zero, size: .init(width: 114_514, height: 114_514))) - refresh() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - } - - // MARK: - Interface Renderer. - - public extension VwrCandidateTDKCocoa { - func refresh() { - defer { - vCLog(Self.strForConstraintStatistics.description) - Self.strForConstraintStatistics = .init() - } - // 用來登記全部的行容器,方便在收尾階段統一設定 constraints。 - var arrStackViewsOfLines = [NSStackView]() - // 清理兩個計數器。 - lineDimension = .zero - candidateAreaDimension = .zero - // 容器自身美化。 - edgeInsets = .init(top: 5, left: 5, bottom: 5, right: 5) - wantsLayer = true - layer?.backgroundColor = candidateListBackground.cgColor - layer?.cornerRadius = 10 - // 現在開始準備容器內容。 - let isVerticalListing: Bool = thePool.layout == .vertical - let candidateContainer = NSStackView() - // 這是行陳列方向,不是候選字詞陳列方向。 - candidateContainer.orientation = isVerticalListing ? .horizontal : .vertical - candidateContainer.alignment = isVerticalListing ? .top : .leading - candidateContainer.spacing = 0 - candidateContainer.setHuggingPriority(.fittingSizeCompression, for: .horizontal) - candidateContainer.setHuggingPriority(.fittingSizeCompression, for: .vertical) - for lineID in thePool.lineRangeForCurrentPage { - var theLine = thePool.candidateLines[lineID] - let vwrCurrentLine = generateLineContainer(&theLine) - candidateContainer.addView(vwrCurrentLine, in: isVerticalListing ? .top : .leading) - arrStackViewsOfLines.append(vwrCurrentLine) - } - if thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count > 0 { - thePool.lineRangeForFinalPageBlanked.enumerated().forEach { _ in - var theLine = [CandidateCellData]() - let copied = CandidatePool.blankCell.cleanCopy - for _ in 0 ..< thePool.maxLineCapacity { - theLine.append(copied) - } - let vwrCurrentLine = generateLineContainer(&theLine) - candidateContainer.addView(vwrCurrentLine, in: isVerticalListing ? .top : .leading) - arrStackViewsOfLines.append(vwrCurrentLine) - } - } - - // 處理行寬或列高。 - switch thePool.layout { - case .vertical: - var accumulatedWidth: CGFloat = 0 - var lines = [[CandidateCellData]]() - thePool.lineRangeForCurrentPage.forEach { lines.append(thePool.candidateLines[$0]) } - arrStackViewsOfLines.enumerated().forEach { viewLineID, vwrCurrentLine in - var columnWidth: CGFloat = 0 - if (0 ..< lines.count).contains(viewLineID), !lines.isEmpty { - let line = Array(lines[viewLineID]) - columnWidth = line.map(\.visualDimension.width).max() ?? lineDimension.width - } else { - columnWidth = CandidatePool.blankCell.visualDimension.width - } - accumulatedWidth += columnWidth - Self.makeSimpleConstraint(item: vwrCurrentLine, attribute: .width, relation: .equal, value: columnWidth) - Self.makeSimpleConstraint(item: vwrCurrentLine, attribute: .height, relation: .equal, value: lineDimension.height) - Self.addStatistics(vwrCurrentLine, memo: "vwrCurrentLine") - } - candidateAreaDimension.width = accumulatedWidth - candidateAreaDimension.height = lineDimension.height - case .horizontal: - arrStackViewsOfLines.forEach { vwrCurrentLine in - Self.makeSimpleConstraint(item: vwrCurrentLine, attribute: .width, relation: .equal, value: lineDimension.width) - Self.makeSimpleConstraint(item: vwrCurrentLine, attribute: .height, relation: .equal, value: lineDimension.height) - Self.addStatistics(vwrCurrentLine, memo: "vwrCurrentLine") - } - candidateAreaDimension.width = lineDimension.width - candidateAreaDimension.height = lineDimension.height * Double(thePool.maxLinesPerPage) - } - - Self.makeSimpleConstraint(item: candidateContainer, attribute: .width, relation: .equal, value: candidateAreaDimension.width) - Self.makeSimpleConstraint(item: candidateContainer, attribute: .height, relation: .equal, value: candidateAreaDimension.height) - Self.addStatistics(candidateContainer, memo: "candidateContainer") - - let vwrPeripherals = Self.makeLabel(thePool.attributedDescriptionBottomPanes) - Self.makeSimpleConstraint( - item: vwrPeripherals, attribute: .height, relation: .greaterThanOrEqual, - value: vwrPeripherals.fittingSize.height - ) - Self.makeSimpleConstraint( - item: vwrPeripherals, attribute: .width, relation: .greaterThanOrEqual, - value: vwrPeripherals.fittingSize.width - ) - - // 組裝。 - let finalContainer = NSStackView() - let finalContainerOrientation: NSUserInterfaceLayoutOrientation = { - if thePool.maxLinesPerPage == 1, thePool.layout == .horizontal { return .horizontal } - return .vertical - }() - - if finalContainerOrientation == .horizontal { - let vwrPeripheralMinWidth = vwrPeripherals.fittingSize.width + 3 - Self.makeSimpleConstraint(item: vwrPeripherals, attribute: .width, relation: .greaterThanOrEqual, value: vwrPeripheralMinWidth) - finalContainer.spacing = 5 - } else { - finalContainer.spacing = 2 - Self.makeSimpleConstraint(item: vwrPeripherals, attribute: .width, relation: .greaterThanOrEqual, value: vwrPeripherals.fittingSize.width) - } - Self.addStatistics(vwrPeripherals, memo: "vwrPeripherals") - - finalContainer.orientation = finalContainerOrientation - finalContainer.alignment = finalContainerOrientation == .vertical ? .leading : .centerY - finalContainer.addView(candidateContainer, in: .leading) - finalContainer.addView(vwrPeripherals, in: .leading) - Self.makeSimpleConstraint( - item: finalContainer, attribute: .width, - relation: .equal, value: finalContainer.fittingSize.width - ) - Self.makeSimpleConstraint( - item: finalContainer, attribute: .height, - relation: .equal, value: finalContainer.fittingSize.height - ) - Self.addStatistics(finalContainer, memo: "finalContainer") - - // 更換容器內容為上文生成的新內容。 - subviews.forEach { removeView($0) } - addView(finalContainer, in: .top) - } - } - - // MARK: - Interface Components. - - private extension VwrCandidateTDKCocoa { - private var candidateListBackground: NSColor { - let delta = NSApplication.isDarkMode ? 0.05 : 0.99 - return .init(white: delta, alpha: 1) - } - - private func drawCellCocoa(_ theCell: CandidateCellData? = nil) -> NSView { - let theCell = theCell ?? CandidatePool.blankCell.cleanCopy - let cellLabel = VwrCandidateCell(cell: theCell) - cellLabel.target = self - Self.makeSimpleConstraint(item: cellLabel, attribute: .width, relation: .equal, value: cellLabel.fittingSize.width) - Self.makeSimpleConstraint(item: cellLabel, attribute: .height, relation: .equal, value: cellLabel.fittingSize.height) - Self.addStatistics(cellLabel, memo: "cellLabel") - let wrappedCell = NSStackView() - let padding: CGFloat = 3 - wrappedCell.edgeInsets = .init(top: padding, left: padding, bottom: padding, right: padding) - wrappedCell.addView(cellLabel, in: .leading) - if theCell.isHighlighted { - wrappedCell.wantsLayer = true - wrappedCell.layer?.backgroundColor = theCell.themeColorCocoa.cgColor - wrappedCell.layer?.cornerRadius = padding * 2 - } - let cellWidth = max(thePool.cellWidth(theCell).min ?? wrappedCell.fittingSize.width, wrappedCell.fittingSize.width) - let cellHeight = wrappedCell.fittingSize.height - wrappedCell.setHuggingPriority(.fittingSizeCompression, for: .horizontal) - wrappedCell.setHuggingPriority(.fittingSizeCompression, for: .vertical) - Self.makeSimpleConstraint(item: wrappedCell, attribute: .height, relation: .equal, value: cellHeight) - switch thePool.layout { - case .horizontal where thePool.isMatrix: - Self.makeSimpleConstraint(item: wrappedCell, attribute: .width, relation: .equal, value: cellWidth) - default: - Self.makeSimpleConstraint(item: wrappedCell, attribute: .width, relation: .greaterThanOrEqual, value: cellWidth) - } - Self.addStatistics(wrappedCell, memo: "wrappedCell") - theCell.visualDimension = .init(width: cellWidth, height: cellHeight) - return wrappedCell - } - - private func lineBackground(isCurrentLine: Bool, isMatrix: Bool) -> NSColor { - if !isCurrentLine { return .clear } - let absBg: NSColor = NSApplication.isDarkMode ? .black : .white - switch thePool.layout { - case .horizontal where isMatrix: - return NSApplication.isDarkMode ? .controlTextColor.withAlphaComponent(0.05) : .white - case .vertical where isMatrix: - return absBg.withAlphaComponent(0.9) - default: - return .clear - } - } - - private func generateLineContainer(_ theLine: inout [CandidateCellData]) -> NSStackView { - let isVerticalListing: Bool = thePool.layout == .vertical - let isMatrix = thePool.isMatrix - let vwrCurrentLine = NSStackView() - vwrCurrentLine.spacing = 0 - vwrCurrentLine.orientation = isVerticalListing ? .vertical : .horizontal - var cellHeight = 0.0 - var lineSize: CGSize = .zero - let isCurrentLine = theLine.hasHighlightedCell - theLine.forEach { theCell in - vwrCurrentLine.addView(drawCellCocoa(theCell), in: isVerticalListing ? .top : .leading) - switch thePool.layout { - case .horizontal: - lineSize.width += theCell.visualDimension.width - lineSize.height = max(lineSize.height, theCell.visualDimension.height) - case .vertical: - lineSize.width = max(lineSize.width, theCell.visualDimension.width) - lineSize.height += theCell.visualDimension.height - } - cellHeight = max(theCell.visualDimension.height, cellHeight) - } - let lineBg = lineBackground(isCurrentLine: isCurrentLine, isMatrix: isMatrix) - vwrCurrentLine.wantsLayer = isCurrentLine && isMatrix - if vwrCurrentLine.wantsLayer { - vwrCurrentLine.layer?.backgroundColor = lineBg.cgColor - vwrCurrentLine.layer?.cornerRadius = 6 - } - vwrCurrentLine.alphaValue = isCurrentLine ? 1 : 0.85 - lineDimension.width = max(lineSize.width, lineDimension.width) - switch thePool.layout { - case .horizontal: lineDimension.height = max(lineSize.height, lineDimension.height) - case .vertical: lineDimension.height = cellHeight * Double(thePool.maxLineCapacity) - } - return vwrCurrentLine - } - - private static func makeLabel(_ attrStr: NSAttributedString) -> NSTextField { - let textField = NSTextField() - textField.isSelectable = false - textField.isEditable = false - textField.isBordered = false - textField.backgroundColor = .clear - textField.allowsEditingTextAttributes = false - textField.preferredMaxLayoutWidth = textField.frame.width - textField.attributedStringValue = attrStr - textField.sizeToFit() - return textField - } - } - - // MARK: - Constraint Utilities - - private extension VwrCandidateTDKCocoa { - static var strForConstraintStatistics = NSMutableString(string: "TDKCandidates Dimensions (Debug):\n") - - static func addStatistics(_ target: NSView, memo: String = "") { - if Self.strForConstraintStatistics.length == 0 { - Self.strForConstraintStatistics.append("TDKCandidates Dimensions (Debug):\n") - } - Self.strForConstraintStatistics.append("\(target.fittingSize) \(memo)\n") - } - - static func makeSimpleConstraint(item: NSView, attribute: NSLayoutConstraint.Attribute, relation: NSLayoutConstraint.Relation, value: CGFloat) { - item.translatesAutoresizingMaskIntoConstraints = false - let widthConstraint = NSLayoutConstraint( - item: item, attribute: attribute, relatedBy: relation, toItem: nil, - attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: value - ) - item.addConstraint(widthConstraint) - } - } - - // MARK: - Candidate Cell View - - private extension VwrCandidateTDKCocoa { - class VwrCandidateCell: NSTextField { - public var cellData: CandidateCellData - public init(cell: CandidateCellData) { - cellData = cell - super.init(frame: .init(origin: .zero, size: .init(width: 114_514, height: 114_514))) - isSelectable = false - isEditable = false - isBordered = false - backgroundColor = .clear - allowsEditingTextAttributes = false - preferredMaxLayoutWidth = frame.width - attributedStringValue = cellData.attributedString() - sizeToFit() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - // TODO: This doesn't work at all. (#TDKError_NSMenuDeconstruction) - theMenu?.cancelTrackingWithoutAnimation() - } - - // MARK: Mouse Actions. - - override func mouseUp(with _: NSEvent) { - guard let target = target as? VwrCandidateTDKCocoa else { return } - target.didSelectCandidateAt(cellData.index) - } - - override func rightMouseUp(with event: NSEvent) { - guard let target = target as? VwrCandidateTDKCocoa else { return } - let index = cellData.index - let candidateText = cellData.displayedText - let isEnabled: Bool = target.controller?.delegate?.isCandidateContextMenuEnabled ?? false - guard isEnabled, !candidateText.isEmpty, index >= 0 else { return } - prepareMenu() - theMenu?.popUp(positioning: nil, at: event.locationInWindow, in: target) - } - - // MARK: Menu. - - var theMenu: NSMenu? - - private func prepareMenu() { - guard let thePool = (target as? VwrCandidateTDKCocoa)?.thePool else { return } - let newMenu = NSMenu() - let boostMenuItem = NSMenuItem( - title: "↑ \(cellData.displayedText)", - action: #selector(menuActionOfBoosting(_:)), - keyEquivalent: "" - ) - boostMenuItem.target = self - newMenu.addItem(boostMenuItem) - - let nerfMenuItem = NSMenuItem( - title: "↓ \(cellData.displayedText)", - action: #selector(menuActionOfNerfing(_:)), - keyEquivalent: "" - ) - nerfMenuItem.target = self - newMenu.addItem(nerfMenuItem) - - if thePool.isFilterable(target: cellData.index) { - let filterMenuItem = NSMenuItem( - title: "✖︎ \(cellData.displayedText)", - action: #selector(menuActionOfFiltering(_:)), - keyEquivalent: "" - ) - filterMenuItem.target = self - newMenu.addItem(filterMenuItem) - } - - theMenu = newMenu - CtlCandidateTDK.currentMenu = newMenu - } - - @objc func menuActionOfBoosting(_: Any? = nil) { - guard let target = target as? VwrCandidateTDKCocoa else { return } - target.didRightClickCandidateAt(cellData.index, action: .toBoost) - } - - @objc func menuActionOfNerfing(_: Any? = nil) { - guard let target = target as? VwrCandidateTDKCocoa else { return } - target.didRightClickCandidateAt(cellData.index, action: .toNerf) - } - - @objc func menuActionOfFiltering(_: Any? = nil) { - guard let target = target as? VwrCandidateTDKCocoa else { return } - target.didRightClickCandidateAt(cellData.index, action: .toFilter) - } - } - } - - // MARK: - Delegate Methods - - private extension VwrCandidateTDKCocoa { - func didSelectCandidateAt(_ pos: Int) { - controller?.delegate?.candidatePairSelectionConfirmed(at: pos) - } - - func didRightClickCandidateAt(_ pos: Int, action: CandidateContextMenuAction) { - controller?.delegate?.candidatePairRightClicked(at: pos, action: action) - } - } - - // MARK: - Debug Module Using Swift UI. - - import SwiftUI - - @available(macOS 10.15, *) - public struct VwrCandidateTDKCocoaForSwiftUI: NSViewRepresentable { - public weak var controller: CtlCandidateTDK? - public var thePool: CandidatePool - - public func makeNSView(context _: Context) -> VwrCandidateTDKCocoa { - let nsView = VwrCandidateTDKCocoa(thePool: thePool) - nsView.controller = controller - return nsView - } - - public func updateNSView(_ nsView: VwrCandidateTDKCocoa, context _: Context) { - nsView.thePool = thePool - nsView.refresh() - } - } - -#endif diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_SwiftUI.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_SwiftUI.swift deleted file mode 100644 index 9df4374e..00000000 --- a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_SwiftUI.swift +++ /dev/null @@ -1,512 +0,0 @@ -// (c) 2022 and onwards The vChewing Project (MIT-NTL License). -// ==================== -// This code is released under the MIT license (SPDX-License-Identifier: MIT) -// ... with NTL restriction stating that: -// No trademark license is granted to use the trade names, trademarks, service -// marks, or product names of Contributor, except as required to fulfill notice -// requirements defined in MIT License. - -import AppKit -import Shared -import SwiftUI -import SwiftUIBackports - -// MARK: - Main View - -@available(macOS 10.15, *) -public struct VwrCandidateTDK: View { - public weak var controller: CtlCandidateTDK? - @Environment(\.colorScheme) var colorScheme - @Backport.StateObject public var thePool: CandidatePool - @State public var forceCatalinaCompatibility: Bool = false - var tooltip: String { thePool.tooltip } - var reverseLookupResult: [String] { thePool.reverseLookupResult } - - let horizontalCellSpacing: CGFloat = 0 - - public var body: some View { - Group { - VStack(alignment: .leading, spacing: 0) { - switch thePool.layout { - case .horizontal: - ZStack { - candidateListBackground - HStack(spacing: 0) { - mainViewHorizontal - if thePool.maxLinesPerPage == 1 { - rightPanes - } - } - } - default: - mainViewVertical.background(candidateListBackground) - } - if thePool.isMatrix || thePool.layout == .vertical { - statusBarContent - } - } - .fixedSize() - .background(candidateListBackground) - .cornerRadius(10) - } - } -} - -// MARK: - Main Views. - -@available(macOS 10.15, *) -private extension VwrCandidateTDK { - var mainViewHorizontal: some View { - Group { - VStack(alignment: .leading, spacing: 1.6) { - ForEach(thePool.lineRangeForCurrentPage, id: \.self) { rowIndex in - ZStack(alignment: .leading) { - lineBackground(lineID: rowIndex).cornerRadius(6).frame(minWidth: minLineWidth) - HStack(spacing: horizontalCellSpacing) { - ForEach(Array(thePool.candidateLines[rowIndex]), id: \.self) { currentCandidate in - drawCandidate(currentCandidate).fixedSize() - } - .opacity(rowIndex == thePool.currentLineNumber ? 1 : 0.85) - } - } - .id(rowIndex) - } - if thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count > 0 { - let copied = CandidatePool.blankCell.cleanCopy - ForEach(thePool.lineRangeForFinalPageBlanked, id: \.self) { _ in - HStack(spacing: 0) { - attributedStringFor(cell: copied) - .frame(alignment: .topLeading) - .contentShape(Rectangle()) - Spacer() - }.frame( - minWidth: 0, - maxWidth: thePool.maxLinesPerPage != 1 ? .infinity : nil, - alignment: .topLeading - ) - } - } - } - } - .fixedSize() - .padding([.horizontal, .top], 5) - .padding([.bottom], thePool.maxLinesPerPage == 1 ? 5 : 0) - } - - var mainViewVertical: some View { - Group { - HStack(alignment: .top, spacing: 4) { - ForEach(Array(thePool.lineRangeForCurrentPage.enumerated()), id: \.offset) { _, columnIndex in - VStack(alignment: .leading, spacing: 0) { - ForEach(Array(thePool.candidateLines[columnIndex]), id: \.self) { currentCandidate in - drawCandidate(currentCandidate) - } - .opacity(columnIndex == thePool.currentLineNumber ? 1 : 0.85) - if thePool.candidateLines[columnIndex].count < thePool.maxLineCapacity { - let copied = CandidatePool.blankCell.cleanCopy - ForEach(0 ..< thePool.dummyCellsRequiredForCurrentLine, id: \.self) { _ in - drawCandidate(copied) - } - } - } - .background(lineBackground(lineID: columnIndex)).cornerRadius(6) - .frame( - alignment: .topLeading - ) - .id(columnIndex) - } - if thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count > 0 { - ForEach(Array(thePool.lineRangeForFinalPageBlanked.enumerated()), id: \.offset) { _, _ in - VStack(alignment: .leading, spacing: 0) { - let copied = CandidatePool.blankCell.cleanCopy - ForEach(0 ..< thePool.maxLineCapacity, id: \.self) { _ in - attributedStringFor(cell: copied).fixedSize() - .frame( - width: ceil(CandidatePool.blankCell.cellLength(isMatrix: true)), - alignment: .topLeading - ) - .contentShape(Rectangle()) - } - }.frame( - minWidth: 0, - maxWidth: .infinity, - alignment: .topLeading - ) - } - } - } - } - .fixedSize(horizontal: true, vertical: false) - .padding([.horizontal, .top], 5) - .padding([.bottom], 0) - } -} - -// MARK: - Common Components. - -@available(macOS 10.15, *) -extension VwrCandidateTDK { - func drawCandidate(_ cell: CandidateCellData) -> some View { - attributedStringFor(cell: cell) - .frame(minWidth: thePool.cellWidth(cell).min, maxWidth: thePool.cellWidth(cell).max, alignment: .topLeading) - .contentShape(Rectangle()) - .onTapGesture { didSelectCandidateAt(cell.index) } - .contextMenu { - if controller?.delegate?.isCandidateContextMenuEnabled ?? false { - Button { - didRightClickCandidateAt(cell.index, action: .toBoost) - } label: { - Text("↑ " + cell.displayedText) - } - Button { - didRightClickCandidateAt(cell.index, action: .toNerf) - } label: { - Text("↓ " + cell.displayedText) - } - if thePool.isFilterable(target: cell.index) { - Button { - didRightClickCandidateAt(cell.index, action: .toFilter) - } label: { - Text("✖︎ " + cell.displayedText) - } - } - } - } - } - - func lineBackground(lineID: Int) -> Color { - let isCurrentLineInMatrix = lineID == thePool.currentLineNumber && thePool.maxLinesPerPage != 1 - switch thePool.layout { - case .horizontal where isCurrentLineInMatrix: - return colorScheme == .dark ? Color.primary.opacity(0.05) : .white - case .vertical where isCurrentLineInMatrix: - return absoluteBackgroundColor.opacity(0.9) - default: - return Color.clear - } - } - - var minLineWidth: CGFloat? { - let spacings: CGFloat = horizontalCellSpacing * Double(thePool.maxLineCapacity - 1) - let maxWindowWith: CGFloat - = ceil( - Double(thePool.maxLineCapacity) * (CandidatePool.blankCell.cellLength()) - + spacings - ) - return thePool.layout == .horizontal && thePool.isMatrix ? maxWindowWith : nil - } - - var firstReverseLookupResult: String { - reverseLookupResult.first?.trimmingCharacters(in: .newlines) ?? "" - } - - /// 以系統字型就給定的粗細狀態與字號來測量給定的字串的渲染寬度,且給出其「向上取整值」。 - /// - Remark: 所有 SwiftUI Text 元件必須手動在介面元素尺寸處理這方面加上向上取整的步驟, - /// 否則的話:當元素尺寸不是整數、且整個視窗內部的 View 都在 .fixedSize() 的時候, - /// 視窗內整個 View 的橫向或縱向起始座標可能就不是 0 而是 -0.5。 - /// - Parameters: - /// - text: 給定的字串。 - /// - fontSize: 給定的字號。 - /// - isBold: 給定的粗細狀態。 - /// - Returns: 測量出來的字串渲染寬度,經過向上取整之處理。 - func getTextWidth(text: String, fontSize: CGFloat, isBold: Bool) -> CGFloat? { - guard !text.isEmpty else { return nil } - let attributes: [NSAttributedString.Key: Any] = [ - .kern: 0, - .font: NSFont.systemFont(ofSize: fontSize, weight: isBold ? .bold : .regular), - .paragraphStyle: CandidateCellData.sharedParagraphStyle, - ] - let attrString = NSAttributedString(string: text, attributes: attributes) - return ceil(attrString.boundingDimension.width) - } - - var positionLabelView: some View { - ZStack { - Color(white: colorScheme == .dark ? 0.215 : 0.9).cornerRadius(4) - Text(thePool.currentPositionLabelText).lineLimit(1) - .font(.system(size: max(ceil(CandidateCellData.unifiedSize * 0.7), 11), weight: .bold)) - .frame( - width: getTextWidth( - text: thePool.currentPositionLabelText, - fontSize: max(ceil(CandidateCellData.unifiedSize * 0.7), 11), - isBold: true - ) - ) - .padding([.horizontal], 2) - .foregroundColor(.primary.opacity(0.9)) - }.fixedSize() - } - - var rightPanes: some View { - HStack { - if !tooltip.isEmpty { - ZStack(alignment: .center) { - Circle().fill(CandidatePool.blankCell.themeColor.opacity(0.8)) - Text(tooltip.first?.description ?? "").padding(2).font(.system(size: CandidateCellData.unifiedSize)) - }.frame(width: ceil(CandidateCellData.unifiedSize * 1.7), height: ceil(CandidateCellData.unifiedSize * 1.7)) - } - HStack(alignment: .center, spacing: 0) { - positionLabelView - if controller?.delegate?.showReverseLookupResult ?? true { - if !firstReverseLookupResult.isEmpty { - Text(firstReverseLookupResult) - .font(.system(size: max(ceil(CandidateCellData.unifiedSize * 0.6), 9))) - .frame( - width: getTextWidth( - text: firstReverseLookupResult, - fontSize: max(ceil(CandidateCellData.unifiedSize * 0.6), 9), - isBold: false - ) - ) - .opacity(0.8).padding([.leading], 9) - } - } - } - .opacity(0.9) - .fixedSize() - .padding([.trailing], 12) - } - } - - var reverseLookupPane: some View { - HStack(alignment: .center, spacing: 2) { - let text = (thePool.maxLinesPerPage == 1) ? firstReverseLookupResult : reverseLookupResult.joined(separator: " ") - if !reverseLookupResult.joined().trimmingCharacters(in: .newlines).isEmpty { - Text(verbatim: "\(text.trimmingCharacters(in: .newlines))") - .lineLimit(1).padding([.horizontal], 2).fixedSize() - } - } - .font(.system(size: max(ceil(CandidateCellData.unifiedSize * 0.6), 9))) - .foregroundColor(colorScheme == .light ? Color(white: 0.1) : Color(white: 0.9)) - } - - var statusBarContent: some View { - HStack(alignment: .center) { - positionLabelView - if !tooltip.isEmpty { - Text(tooltip).lineLimit(1) - } - if controller?.delegate?.showReverseLookupResult ?? true, !reverseLookupResult.joined().isEmpty { - reverseLookupPane.padding(0) - } - Spacer(minLength: 0) - } - .font(.system(size: max(ceil(CandidateCellData.unifiedSize * 0.7), 11), weight: .bold)) - .padding([.bottom, .horizontal], 7).padding([.top], 2) - .fixedSize(horizontal: false, vertical: true) - } - - var candidateListBackground: some View { - Group { - absoluteBackgroundColor - if colorScheme == .dark { - Color.primary.opacity(0.05) - } else { - Color.primary.opacity(0.01) - } - } - } - - var absoluteBackgroundColor: Color { - if colorScheme == .dark { - return Color.black - } else { - return Color.white - } - } - - func attributedStringFor(cell theCell: CandidateCellData) -> some View { - let defaultResult = theCell.attributedStringForSwiftUIBackports - if forceCatalinaCompatibility { - return defaultResult - } - if #available(macOS 12, *) { - return theCell.attributedStringForSwiftUI - } - return defaultResult - } -} - -// MARK: - Delegate Methods - -@available(macOS 10.15, *) -private extension VwrCandidateTDK { - func didSelectCandidateAt(_ pos: Int) { - controller?.delegate?.candidatePairSelectionConfirmed(at: pos) - } - - func didRightClickCandidateAt(_ pos: Int, action: CandidateContextMenuAction) { - controller?.delegate?.candidatePairRightClicked(at: pos, action: action) - } -} - -// MARK: - Preview - -import SwiftExtension - -@available(macOS 10.15, *) -struct VwrCandidateTDK_Previews: PreviewProvider { - @State static var testCandidates: [String] = [ - "二十四歲是學生", "二十四歲", "昏睡紅茶", "食雪漢", "意味深", "學生", "便乗", - "🐂🍺🐂🍺", "🐃🍺", "🐂🍺", "🐃🐂🍺🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺", - "迫真", "驚愕", "論證", "正論", "惱", "悲", "屑", "食", "雪", "漢", "意", "味", - "深", "二", "十", "四", "歲", "是", "學", "生", "昏", "睡", "紅", "茶", "便", "乗", - "嗯", "哼", "啊", - ] - @State static var reverseLookupResult = ["mmmmm", "dddd"] - @State static var tooltip = "📼" - @State static var oldOS: Bool = false - - static var testCandidatesConverted: [(keyArray: [String], value: String)] { - testCandidates.map { candidate in - let firstValue: [String] = .init(repeating: "", count: candidate.count) - return (firstValue, candidate) - } - } - - static var thePoolX: CandidatePool { - let result = CandidatePool( - candidates: testCandidatesConverted, lines: 4, - selectionKeys: "123456", layout: .horizontal - ) - result.reverseLookupResult = Self.reverseLookupResult - result.tooltip = Self.tooltip - result.highlight(at: 0) - return result - } - - static var thePoolXS: CandidatePool { - let result = CandidatePool( - candidates: testCandidatesConverted, lines: 1, - selectionKeys: "123456", layout: .horizontal - ) - result.reverseLookupResult = Self.reverseLookupResult - result.tooltip = Self.tooltip - result.highlight(at: 1) - return result - } - - static var thePoolY: CandidatePool { - let result = CandidatePool( - candidates: testCandidatesConverted, lines: 4, - selectionKeys: "123456", layout: .vertical - ) - result.reverseLookupResult = Self.reverseLookupResult - result.tooltip = Self.tooltip - result.flipPage(isBackward: false) - result.highlight(at: 2) - result.highlight(at: 21) - return result - } - - static var thePoolYS: CandidatePool { - let result = CandidatePool( - candidates: testCandidatesConverted, lines: 1, - selectionKeys: "123456", layout: .vertical - ) - result.reverseLookupResult = Self.reverseLookupResult - result.tooltip = Self.tooltip - result.highlight(at: 1) - return result - } - - static var candidateListBackground: Color { - if NSApplication.isDarkMode { - return Color(white: 0.05) - } else { - return Color(white: 0.99) - } - } - - static var previews: some View { - VStack { - HStack(alignment: .top) { - Text("田所選字窗 效能模式").bold().font(Font.system(.title)) - VStack { - AttributedLabel(attributedString: Self.thePoolX.attributedDescription) - .padding(5) - .background(candidateListBackground) - .cornerRadius(10).fixedSize() - AttributedLabel(attributedString: Self.thePoolXS.attributedDescription) - .padding(5) - .background(candidateListBackground) - .cornerRadius(10).fixedSize() - HStack { - AttributedLabel(attributedString: Self.thePoolY.attributedDescription) - .padding(5) - .background(candidateListBackground) - .cornerRadius(10).fixedSize() - AttributedLabel(attributedString: Self.thePoolYS.attributedDescription) - .padding(5) - .background(candidateListBackground) - .cornerRadius(10).fixedSize() - } - } - } - Divider() - HStack(alignment: .top) { - Text("田所選字窗 SwiftUI 模式").bold().font(Font.system(.title)) - VStack { - VwrCandidateTDK(controller: nil, thePool: thePoolX, forceCatalinaCompatibility: oldOS).fixedSize() - VwrCandidateTDK(controller: nil, thePool: thePoolXS, forceCatalinaCompatibility: oldOS).fixedSize() - HStack { - VwrCandidateTDK(controller: nil, thePool: thePoolY, forceCatalinaCompatibility: oldOS).fixedSize() - VwrCandidateTDK(controller: nil, thePool: thePoolYS, forceCatalinaCompatibility: oldOS).fixedSize() - } - } - } - } - VStack { - HStack(alignment: .top) { - Text("田所選字窗 CG 模式").bold().font(Font.system(.title)) - VStack { - VwrCandidateTDKAppKitForSwiftUI(controller: nil, thePool: thePoolX).fixedSize() - VwrCandidateTDKAppKitForSwiftUI(controller: nil, thePool: thePoolXS).fixedSize() - HStack { - VwrCandidateTDKAppKitForSwiftUI(controller: nil, thePool: thePoolY).fixedSize() - VwrCandidateTDKAppKitForSwiftUI(controller: nil, thePool: thePoolYS).fixedSize() - } - } - } - Divider() - HStack(alignment: .top) { - Text("田所選字窗 SwiftUI 模式").bold().font(Font.system(.title)) - VStack { - VwrCandidateTDK(controller: nil, thePool: thePoolX, forceCatalinaCompatibility: oldOS).fixedSize() - VwrCandidateTDK(controller: nil, thePool: thePoolXS, forceCatalinaCompatibility: oldOS).fixedSize() - HStack { - VwrCandidateTDK(controller: nil, thePool: thePoolY, forceCatalinaCompatibility: oldOS).fixedSize() - VwrCandidateTDK(controller: nil, thePool: thePoolYS, forceCatalinaCompatibility: oldOS).fixedSize() - } - } - } - } - #if USING_STACK_VIEW_IN_TDK_COCOA - VStack { - HStack(alignment: .top) { - Text("田所選字窗 Cocoa 模式").bold().font(Font.system(.title)) - VStack { - VwrCandidateTDKCocoaForSwiftUI(controller: nil, thePool: thePoolX).fixedSize() - VwrCandidateTDKCocoaForSwiftUI(controller: nil, thePool: thePoolXS).fixedSize() - HStack { - VwrCandidateTDKCocoaForSwiftUI(controller: nil, thePool: thePoolY).fixedSize() - VwrCandidateTDKCocoaForSwiftUI(controller: nil, thePool: thePoolYS).fixedSize() - } - } - } - Divider() - HStack(alignment: .top) { - Text("田所選字窗 SwiftUI 模式").bold().font(Font.system(.title)) - VStack { - VwrCandidateTDK(controller: nil, thePool: thePoolX, forceCatalinaCompatibility: oldOS).fixedSize() - VwrCandidateTDK(controller: nil, thePool: thePoolXS, forceCatalinaCompatibility: oldOS).fixedSize() - HStack { - VwrCandidateTDK(controller: nil, thePool: thePoolY, forceCatalinaCompatibility: oldOS).fixedSize() - VwrCandidateTDK(controller: nil, thePool: thePoolYS, forceCatalinaCompatibility: oldOS).fixedSize() - } - } - } - } - #endif - } -}