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:
parent
7180bb2798
commit
949f140671
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue