TDKCandidates // Make CandidatePool into a class, etc.
This commit is contained in:
parent
fc29b52f8f
commit
682aaf5053
|
@ -39,6 +39,23 @@ public class CandidateCellData: Hashable {
|
||||||
: .init(red: 142 / 255, green: 142 / 255, blue: 147 / 255, alpha: 1)
|
: .init(red: 142 / 255, green: 142 / 255, blue: 147 / 255, alpha: 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var hardCopy: CandidateCellData {
|
||||||
|
let result = CandidateCellData(key: selectionKey, displayedText: displayedText, spanLength: spanLength, isSelected: isHighlighted)
|
||||||
|
result.visualDimension = visualDimension
|
||||||
|
result.locale = locale
|
||||||
|
result.whichLine = whichLine
|
||||||
|
result.index = index
|
||||||
|
result.subIndex = subIndex
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
public var cleanCopy: CandidateCellData {
|
||||||
|
let result = hardCopy
|
||||||
|
result.isHighlighted = false
|
||||||
|
result.selectionKey = " "
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
key: String, displayedText: String,
|
key: String, displayedText: String,
|
||||||
spanLength spanningLength: Int? = nil, isSelected: Bool = false
|
spanLength spanningLength: Int? = nil, isSelected: Bool = false
|
||||||
|
|
|
@ -39,7 +39,7 @@ public extension CandidateCellData {
|
||||||
Text(verbatim: displayedText)
|
Text(verbatim: displayedText)
|
||||||
.font(.init(CTFontCreateUIFontForLanguage(.system, fontSizeCandidate, locale as CFString)!))
|
.font(.init(CTFontCreateUIFontForLanguage(.system, fontSizeCandidate, locale as CFString)!))
|
||||||
.foregroundColor(Color(white: 1)).lineLimit(1)
|
.foregroundColor(Color(white: 1)).lineLimit(1)
|
||||||
}.padding(3).foregroundColor(Color(white: 0.9))
|
}.padding(.vertical, 3).padding(.horizontal, 5)
|
||||||
}.frame(alignment: .leading)
|
}.frame(alignment: .leading)
|
||||||
} else {
|
} else {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
@ -49,7 +49,7 @@ public extension CandidateCellData {
|
||||||
Text(verbatim: displayedText)
|
Text(verbatim: displayedText)
|
||||||
.font(.init(CTFontCreateUIFontForLanguage(.system, fontSizeCandidate, locale as CFString)!))
|
.font(.init(CTFontCreateUIFontForLanguage(.system, fontSizeCandidate, locale as CFString)!))
|
||||||
.foregroundColor(Color.primary).lineLimit(1)
|
.foregroundColor(Color.primary).lineLimit(1)
|
||||||
}.padding(3).foregroundColor(Color(white: 0.9))
|
}.padding(.vertical, 3).padding(.horizontal, 5)
|
||||||
}.frame(alignment: .leading)
|
}.frame(alignment: .leading)
|
||||||
}
|
}
|
||||||
}.fixedSize(horizontal: false, vertical: true)
|
}.fixedSize(horizontal: false, vertical: true)
|
||||||
|
@ -71,7 +71,7 @@ public extension CandidateCellData {
|
||||||
Text(verbatim: displayedText)
|
Text(verbatim: displayedText)
|
||||||
.font(.init(CTFontCreateUIFontForLanguage(.system, fontSizeCandidate, locale as CFString)!))
|
.font(.init(CTFontCreateUIFontForLanguage(.system, fontSizeCandidate, locale as CFString)!))
|
||||||
.foregroundColor(.init(nsColor: fontColorCandidate)).lineLimit(1)
|
.foregroundColor(.init(nsColor: fontColorCandidate)).lineLimit(1)
|
||||||
}.padding(3)
|
}.padding(.vertical, 3).padding(.horizontal, 5)
|
||||||
}.frame(alignment: .leading)
|
}.frame(alignment: .leading)
|
||||||
}.fixedSize(horizontal: false, vertical: true)
|
}.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,13 +10,14 @@ import Foundation
|
||||||
import Shared
|
import Shared
|
||||||
|
|
||||||
/// 候選字窗會用到的資料池單位,即用即拋。
|
/// 候選字窗會用到的資料池單位,即用即拋。
|
||||||
public struct CandidatePool {
|
public class CandidatePool {
|
||||||
public let blankCell: CandidateCellData
|
// 只用來測量單漢字候選字 cell 的最大可能寬度。
|
||||||
public let shitCell: CandidateCellData // 只用來測量單漢字候選字 cell 的最大可能寬度。
|
public static let shitCell = CandidateCellData(key: " ", displayedText: "💩", isSelected: false)
|
||||||
public let maxLinesPerPage: Int
|
public static let blankCell = CandidateCellData(key: " ", displayedText: " ", isSelected: false)
|
||||||
public let layout: LayoutOrientation
|
public private(set) var maxLinesPerPage: Int
|
||||||
public let selectionKeys: String
|
public private(set) var layout: LayoutOrientation
|
||||||
public let candidateDataAll: [CandidateCellData]
|
public private(set) var selectionKeys: String
|
||||||
|
public private(set) var candidateDataAll: [CandidateCellData]
|
||||||
public var candidateLines: [[CandidateCellData]] = []
|
public var candidateLines: [[CandidateCellData]] = []
|
||||||
public var tooltip: String = ""
|
public var tooltip: String = ""
|
||||||
public var reverseLookupResult: [String] = []
|
public var reverseLookupResult: [String] = []
|
||||||
|
@ -30,7 +31,7 @@ public struct CandidatePool {
|
||||||
|
|
||||||
/// 用來在初期化一個候選字詞資料池的時候研判「橫版多行選字窗每行最大應該塞多少個候選字詞」。
|
/// 用來在初期化一個候選字詞資料池的時候研判「橫版多行選字窗每行最大應該塞多少個候選字詞」。
|
||||||
/// 注意:該參數不用來計算視窗寬度,所以無須算上候選字詞間距。
|
/// 注意:該參數不用來計算視窗寬度,所以無須算上候選字詞間距。
|
||||||
public var maxRowWidth: Double { ceil(Double(maxLineCapacity) * blankCell.cellLength()) }
|
public var maxRowWidth: Double { ceil(Double(maxLineCapacity) * Self.blankCell.cellLength()) }
|
||||||
|
|
||||||
/// 當前高亮的候選字詞的順序標籤(同時顯示資料池內已有的全部的候選字詞的數量)
|
/// 當前高亮的候選字詞的順序標籤(同時顯示資料池內已有的全部的候選字詞的數量)
|
||||||
public var currentPositionLabelText: String {
|
public var currentPositionLabelText: String {
|
||||||
|
@ -79,18 +80,35 @@ public struct CandidatePool {
|
||||||
public init(
|
public init(
|
||||||
candidates: [(keyArray: [String], value: String)], lines: Int = 3, selectionKeys: String = "123456789",
|
candidates: [(keyArray: [String], value: String)], lines: Int = 3, selectionKeys: String = "123456789",
|
||||||
layout: LayoutOrientation = .vertical, locale: String = ""
|
layout: LayoutOrientation = .vertical, locale: String = ""
|
||||||
|
) {
|
||||||
|
maxLinesPerPage = 1
|
||||||
|
self.layout = .horizontal
|
||||||
|
self.selectionKeys = "123456789"
|
||||||
|
candidateDataAll = []
|
||||||
|
// 以上只是為了糊弄 compiler。接下來才是正式的初期化。
|
||||||
|
construct(candidates: candidates, lines: lines, selectionKeys: selectionKeys, layout: layout, locale: locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 初期化(或者自我重新初期化)一個候選字窗專用資料池。
|
||||||
|
/// - Parameters:
|
||||||
|
/// - candidates: 要塞入的候選字詞陣列。
|
||||||
|
/// - selectionKeys: 選字鍵。
|
||||||
|
/// - direction: 橫向排列還是縱向排列(預設情況下是縱向)。
|
||||||
|
/// - locale: 區域編碼。例:「zh-Hans」或「zh-Hant」。
|
||||||
|
private func construct(
|
||||||
|
candidates: [(keyArray: [String], value: String)], lines: Int = 3, selectionKeys: String = "123456789",
|
||||||
|
layout: LayoutOrientation = .vertical, locale: String = ""
|
||||||
) {
|
) {
|
||||||
self.layout = layout
|
self.layout = layout
|
||||||
maxLinesPerPage = max(1, lines)
|
maxLinesPerPage = max(1, lines)
|
||||||
blankCell = CandidateCellData(key: " ", displayedText: " ", isSelected: false)
|
Self.blankCell.locale = locale
|
||||||
shitCell = CandidateCellData(key: " ", displayedText: "💩", isSelected: false)
|
|
||||||
blankCell.locale = locale
|
|
||||||
self.selectionKeys = selectionKeys.isEmpty ? "123456789" : selectionKeys
|
self.selectionKeys = selectionKeys.isEmpty ? "123456789" : selectionKeys
|
||||||
var allCandidates = candidates.map {
|
var allCandidates = candidates.map {
|
||||||
CandidateCellData(key: " ", displayedText: $0.value, spanLength: $0.keyArray.count)
|
CandidateCellData(key: " ", displayedText: $0.value, spanLength: $0.keyArray.count)
|
||||||
}
|
}
|
||||||
if allCandidates.isEmpty { allCandidates.append(blankCell) }
|
if allCandidates.isEmpty { allCandidates.append(Self.blankCell) }
|
||||||
candidateDataAll = allCandidates
|
candidateDataAll = allCandidates
|
||||||
|
candidateLines.removeAll()
|
||||||
var currentColumn: [CandidateCellData] = []
|
var currentColumn: [CandidateCellData] = []
|
||||||
for (i, candidate) in candidateDataAll.enumerated() {
|
for (i, candidate) in candidateDataAll.enumerated() {
|
||||||
candidate.index = i
|
candidate.index = i
|
||||||
|
@ -127,7 +145,7 @@ public extension CandidatePool {
|
||||||
/// 往指定的方向翻頁。
|
/// 往指定的方向翻頁。
|
||||||
/// - Parameter isBackward: 是否逆向翻頁。
|
/// - Parameter isBackward: 是否逆向翻頁。
|
||||||
/// - Returns: 操作是否順利。
|
/// - Returns: 操作是否順利。
|
||||||
@discardableResult mutating func flipPage(isBackward: Bool) -> Bool {
|
@discardableResult func flipPage(isBackward: Bool) -> Bool {
|
||||||
backupLineRangeForCurrentPage()
|
backupLineRangeForCurrentPage()
|
||||||
defer { flipLineRangeToNeighborPage(isBackward: isBackward) }
|
defer { flipLineRangeToNeighborPage(isBackward: isBackward) }
|
||||||
return consecutivelyFlipLines(isBackward: isBackward, count: maxLinesPerPage)
|
return consecutivelyFlipLines(isBackward: isBackward, count: maxLinesPerPage)
|
||||||
|
@ -147,7 +165,7 @@ public extension CandidatePool {
|
||||||
/// - isBackward: 是否逆向翻行。
|
/// - isBackward: 是否逆向翻行。
|
||||||
/// - count: 翻幾行。
|
/// - count: 翻幾行。
|
||||||
/// - Returns: 操作是否順利。
|
/// - Returns: 操作是否順利。
|
||||||
@discardableResult mutating func consecutivelyFlipLines(isBackward: Bool, count: Int) -> Bool {
|
@discardableResult func consecutivelyFlipLines(isBackward: Bool, count: Int) -> Bool {
|
||||||
switch isBackward {
|
switch isBackward {
|
||||||
case false where currentLineNumber == candidateLines.count - 1:
|
case false where currentLineNumber == candidateLines.count - 1:
|
||||||
return highlightNeighborCandidate(isBackward: false)
|
return highlightNeighborCandidate(isBackward: false)
|
||||||
|
@ -165,7 +183,7 @@ public extension CandidatePool {
|
||||||
/// 嘗試高亮前方或者後方的鄰近候選字詞。
|
/// 嘗試高亮前方或者後方的鄰近候選字詞。
|
||||||
/// - Parameter isBackward: 是否是後方的鄰近候選字詞。
|
/// - Parameter isBackward: 是否是後方的鄰近候選字詞。
|
||||||
/// - Returns: 是否成功。
|
/// - Returns: 是否成功。
|
||||||
@discardableResult mutating func highlightNeighborCandidate(isBackward: Bool) -> Bool {
|
@discardableResult func highlightNeighborCandidate(isBackward: Bool) -> Bool {
|
||||||
switch isBackward {
|
switch isBackward {
|
||||||
case false where highlightedIndex >= candidateDataAll.count - 1:
|
case false where highlightedIndex >= candidateDataAll.count - 1:
|
||||||
highlight(at: 0)
|
highlight(at: 0)
|
||||||
|
@ -181,7 +199,7 @@ public extension CandidatePool {
|
||||||
|
|
||||||
/// 高亮指定的候選字。
|
/// 高亮指定的候選字。
|
||||||
/// - Parameter indexSpecified: 給定的候選字詞索引編號,得是資料池內的總索引編號。
|
/// - Parameter indexSpecified: 給定的候選字詞索引編號,得是資料池內的總索引編號。
|
||||||
mutating func highlight(at indexSpecified: Int) {
|
func highlight(at indexSpecified: Int) {
|
||||||
var indexSpecified = indexSpecified
|
var indexSpecified = indexSpecified
|
||||||
let isBackward: Bool = indexSpecified > highlightedIndex
|
let isBackward: Bool = indexSpecified > highlightedIndex
|
||||||
highlightedIndex = indexSpecified
|
highlightedIndex = indexSpecified
|
||||||
|
@ -222,7 +240,7 @@ public extension CandidatePool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func cellWidth(_ cell: CandidateCellData) -> (min: CGFloat?, max: CGFloat?) {
|
func cellWidth(_ cell: CandidateCellData) -> (min: CGFloat?, max: CGFloat?) {
|
||||||
let minAccepted = ceil(shitCell.cellLength(isMatrix: false))
|
let minAccepted = ceil(Self.shitCell.cellLength(isMatrix: false))
|
||||||
let defaultMin: CGFloat = cell.cellLength(isMatrix: maxLinesPerPage != 1)
|
let defaultMin: CGFloat = cell.cellLength(isMatrix: maxLinesPerPage != 1)
|
||||||
var min: CGFloat = defaultMin
|
var min: CGFloat = defaultMin
|
||||||
if layout != .vertical, maxLinesPerPage == 1 {
|
if layout != .vertical, maxLinesPerPage == 1 {
|
||||||
|
@ -267,14 +285,14 @@ private extension CandidatePool {
|
||||||
max(0, candidateLines.count - maxLinesPerPage) ..< candidateLines.count
|
max(0, candidateLines.count - maxLinesPerPage) ..< candidateLines.count
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func selectNewNeighborLine(isBackward: Bool) {
|
func selectNewNeighborLine(isBackward: Bool) {
|
||||||
switch layout {
|
switch layout {
|
||||||
case .horizontal: selectNewNeighborRow(direction: isBackward ? .up : .down)
|
case .horizontal: selectNewNeighborRow(direction: isBackward ? .up : .down)
|
||||||
case .vertical: selectNewNeighborColumn(direction: isBackward ? .left : .right)
|
case .vertical: selectNewNeighborColumn(direction: isBackward ? .left : .right)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func fixLineRange(isBackward: Bool = false) {
|
func fixLineRange(isBackward: Bool = false) {
|
||||||
if !lineRangeForCurrentPage.contains(currentLineNumber) {
|
if !lineRangeForCurrentPage.contains(currentLineNumber) {
|
||||||
switch isBackward {
|
switch isBackward {
|
||||||
case false:
|
case false:
|
||||||
|
@ -289,11 +307,11 @@ private extension CandidatePool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func backupLineRangeForCurrentPage() {
|
func backupLineRangeForCurrentPage() {
|
||||||
previouslyRecordedLineRangeForPreviousPage = lineRangeForCurrentPage
|
previouslyRecordedLineRangeForPreviousPage = lineRangeForCurrentPage
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func flipLineRangeToNeighborPage(isBackward: Bool = false) {
|
func flipLineRangeToNeighborPage(isBackward: Bool = false) {
|
||||||
guard let prevRange = previouslyRecordedLineRangeForPreviousPage else { return }
|
guard let prevRange = previouslyRecordedLineRangeForPreviousPage else { return }
|
||||||
var lowerBound = prevRange.lowerBound
|
var lowerBound = prevRange.lowerBound
|
||||||
var upperBound = prevRange.upperBound
|
var upperBound = prevRange.upperBound
|
||||||
|
@ -323,7 +341,7 @@ private extension CandidatePool {
|
||||||
// 應該不會有漏檢的情形了。
|
// 應該不會有漏檢的情形了。
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func selectNewNeighborRow(direction: VerticalDirection) {
|
func selectNewNeighborRow(direction: VerticalDirection) {
|
||||||
let currentSubIndex = candidateDataAll[highlightedIndex].subIndex
|
let currentSubIndex = candidateDataAll[highlightedIndex].subIndex
|
||||||
var result = currentSubIndex
|
var result = currentSubIndex
|
||||||
branch: switch direction {
|
branch: switch direction {
|
||||||
|
@ -369,7 +387,7 @@ private extension CandidatePool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func selectNewNeighborColumn(direction: HorizontalDirection) {
|
func selectNewNeighborColumn(direction: HorizontalDirection) {
|
||||||
let currentSubIndex = candidateDataAll[highlightedIndex].subIndex
|
let currentSubIndex = candidateDataAll[highlightedIndex].subIndex
|
||||||
switch direction {
|
switch direction {
|
||||||
case .left:
|
case .left:
|
||||||
|
|
|
@ -25,7 +25,7 @@ extension CandidatePool {
|
||||||
private var attributedDescriptionHorizontal: NSAttributedString {
|
private var attributedDescriptionHorizontal: NSAttributedString {
|
||||||
let paragraphStyle = sharedParagraphStyle
|
let paragraphStyle = sharedParagraphStyle
|
||||||
let attrCandidate: [NSAttributedString.Key: AnyObject] = [
|
let attrCandidate: [NSAttributedString.Key: AnyObject] = [
|
||||||
.font: blankCell.phraseFont(size: blankCell.size),
|
.font: Self.blankCell.phraseFont(size: Self.blankCell.size),
|
||||||
.paragraphStyle: paragraphStyle,
|
.paragraphStyle: paragraphStyle,
|
||||||
]
|
]
|
||||||
let result = NSMutableAttributedString(string: "", attributes: attrCandidate)
|
let result = NSMutableAttributedString(string: "", attributes: attrCandidate)
|
||||||
|
@ -64,7 +64,7 @@ extension CandidatePool {
|
||||||
private var attributedDescriptionVertical: NSAttributedString {
|
private var attributedDescriptionVertical: NSAttributedString {
|
||||||
let paragraphStyle = sharedParagraphStyle
|
let paragraphStyle = sharedParagraphStyle
|
||||||
let attrCandidate: [NSAttributedString.Key: AnyObject] = [
|
let attrCandidate: [NSAttributedString.Key: AnyObject] = [
|
||||||
.font: blankCell.phraseFont(size: blankCell.size),
|
.font: Self.blankCell.phraseFont(size: Self.blankCell.size),
|
||||||
.paragraphStyle: paragraphStyle,
|
.paragraphStyle: paragraphStyle,
|
||||||
]
|
]
|
||||||
let result = NSMutableAttributedString(string: "", attributes: attrCandidate)
|
let result = NSMutableAttributedString(string: "", attributes: attrCandidate)
|
||||||
|
@ -129,7 +129,7 @@ extension CandidatePool {
|
||||||
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: blankCell.phraseFontEmphasized(size: positionCounterTextSize),
|
.font: Self.blankCell.phraseFontEmphasized(size: positionCounterTextSize),
|
||||||
.backgroundColor: positionCounterColorBG,
|
.backgroundColor: positionCounterColorBG,
|
||||||
.foregroundColor: positionCounterColorText,
|
.foregroundColor: positionCounterColorText,
|
||||||
]
|
]
|
||||||
|
@ -142,7 +142,7 @@ extension CandidatePool {
|
||||||
private var attributedDescriptionTooltip: NSAttributedString {
|
private var attributedDescriptionTooltip: NSAttributedString {
|
||||||
let positionCounterTextSize = max(ceil(CandidateCellData.unifiedSize * 0.7), 11)
|
let positionCounterTextSize = max(ceil(CandidateCellData.unifiedSize * 0.7), 11)
|
||||||
let attrTooltip: [NSAttributedString.Key: AnyObject] = [
|
let attrTooltip: [NSAttributedString.Key: AnyObject] = [
|
||||||
.font: blankCell.phraseFontEmphasized(size: positionCounterTextSize),
|
.font: Self.blankCell.phraseFontEmphasized(size: positionCounterTextSize),
|
||||||
]
|
]
|
||||||
let tooltipText = NSAttributedString(
|
let tooltipText = NSAttributedString(
|
||||||
string: " \(tooltip) ", attributes: attrTooltip
|
string: " \(tooltip) ", attributes: attrTooltip
|
||||||
|
@ -153,10 +153,10 @@ extension CandidatePool {
|
||||||
private var attributedDescriptionReverseLookp: NSAttributedString {
|
private var attributedDescriptionReverseLookp: NSAttributedString {
|
||||||
let reverseLookupTextSize = max(ceil(CandidateCellData.unifiedSize * 0.6), 9)
|
let reverseLookupTextSize = max(ceil(CandidateCellData.unifiedSize * 0.6), 9)
|
||||||
let attrReverseLookup: [NSAttributedString.Key: AnyObject] = [
|
let attrReverseLookup: [NSAttributedString.Key: AnyObject] = [
|
||||||
.font: blankCell.phraseFont(size: reverseLookupTextSize),
|
.font: Self.blankCell.phraseFont(size: reverseLookupTextSize),
|
||||||
]
|
]
|
||||||
let attrReverseLookupSpacer: [NSAttributedString.Key: AnyObject] = [
|
let attrReverseLookupSpacer: [NSAttributedString.Key: AnyObject] = [
|
||||||
.font: blankCell.phraseFont(size: reverseLookupTextSize),
|
.font: Self.blankCell.phraseFont(size: reverseLookupTextSize),
|
||||||
]
|
]
|
||||||
let result = NSMutableAttributedString(string: "", attributes: attrReverseLookupSpacer)
|
let result = NSMutableAttributedString(string: "", attributes: attrReverseLookupSpacer)
|
||||||
for neta in reverseLookupResult {
|
for neta in reverseLookupResult {
|
||||||
|
|
|
@ -31,6 +31,8 @@ public class CtlCandidateTDK: CtlCandidate, NSWindowDelegate {
|
||||||
public var useMouseScrolling: Bool = true
|
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()
|
||||||
|
public static var currentWindow: NSWindow?
|
||||||
|
public static var currentMenu: NSMenu?
|
||||||
|
|
||||||
@available(macOS 10.15, *)
|
@available(macOS 10.15, *)
|
||||||
private var theView: some View {
|
private var theView: some View {
|
||||||
|
|
|
@ -66,9 +66,10 @@ public extension VwrCandidateTDKCocoa {
|
||||||
}
|
}
|
||||||
if thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count > 0 {
|
if thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count > 0 {
|
||||||
thePool.lineRangeForFinalPageBlanked.enumerated().forEach { _ in
|
thePool.lineRangeForFinalPageBlanked.enumerated().forEach { _ in
|
||||||
var theLine = [thePool.blankCell]
|
var theLine = [CandidateCellData]()
|
||||||
for _ in 1 ..< thePool.maxLineCapacity {
|
let copied = CandidatePool.blankCell.cleanCopy
|
||||||
theLine.append(thePool.blankCell)
|
for _ in 0 ..< thePool.maxLineCapacity {
|
||||||
|
theLine.append(copied)
|
||||||
}
|
}
|
||||||
let vwrCurrentLine = generateLineContainer(&theLine)
|
let vwrCurrentLine = generateLineContainer(&theLine)
|
||||||
candidateContainer.addView(vwrCurrentLine, in: isVerticalListing ? .top : .leading)
|
candidateContainer.addView(vwrCurrentLine, in: isVerticalListing ? .top : .leading)
|
||||||
|
@ -88,7 +89,7 @@ public extension VwrCandidateTDKCocoa {
|
||||||
let line = Array(lines[viewLineID])
|
let line = Array(lines[viewLineID])
|
||||||
columnWidth = line.map(\.visualDimension.width).max() ?? lineDimension.width
|
columnWidth = line.map(\.visualDimension.width).max() ?? lineDimension.width
|
||||||
} else {
|
} else {
|
||||||
columnWidth = thePool.blankCell.visualDimension.width
|
columnWidth = CandidatePool.blankCell.visualDimension.width
|
||||||
}
|
}
|
||||||
accumulatedWidth += columnWidth
|
accumulatedWidth += columnWidth
|
||||||
Self.makeSimpleConstraint(item: vwrCurrentLine, attribute: .width, relation: .equal, value: columnWidth)
|
Self.makeSimpleConstraint(item: vwrCurrentLine, attribute: .width, relation: .equal, value: columnWidth)
|
||||||
|
@ -167,7 +168,7 @@ private extension VwrCandidateTDKCocoa {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func drawCellCocoa(_ theCell: CandidateCellData? = nil) -> NSView {
|
private func drawCellCocoa(_ theCell: CandidateCellData? = nil) -> NSView {
|
||||||
let theCell = theCell ?? thePool.blankCell
|
let theCell = theCell ?? CandidatePool.blankCell.cleanCopy
|
||||||
let cellLabel = VwrCandidateCell(cell: theCell)
|
let cellLabel = VwrCandidateCell(cell: theCell)
|
||||||
cellLabel.target = self
|
cellLabel.target = self
|
||||||
Self.makeSimpleConstraint(item: cellLabel, attribute: .width, relation: .equal, value: cellLabel.fittingSize.width)
|
Self.makeSimpleConstraint(item: cellLabel, attribute: .width, relation: .equal, value: cellLabel.fittingSize.width)
|
||||||
|
@ -362,6 +363,7 @@ private extension VwrCandidateTDKCocoa {
|
||||||
}
|
}
|
||||||
|
|
||||||
theMenu = newMenu
|
theMenu = newMenu
|
||||||
|
CtlCandidateTDK.currentMenu = newMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func menuActionOfBoosting(_: Any? = nil) {
|
@objc func menuActionOfBoosting(_: Any? = nil) {
|
||||||
|
|
|
@ -72,9 +72,10 @@ private extension VwrCandidateTDK {
|
||||||
.id(rowIndex)
|
.id(rowIndex)
|
||||||
}
|
}
|
||||||
if thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count > 0 {
|
if thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count > 0 {
|
||||||
|
let copied = CandidatePool.blankCell.cleanCopy
|
||||||
ForEach(thePool.lineRangeForFinalPageBlanked, id: \.self) { _ in
|
ForEach(thePool.lineRangeForFinalPageBlanked, id: \.self) { _ in
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
attributedStringFor(cell: thePool.blankCell)
|
attributedStringFor(cell: copied)
|
||||||
.frame(alignment: .topLeading)
|
.frame(alignment: .topLeading)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
Spacer()
|
Spacer()
|
||||||
|
@ -102,8 +103,9 @@ private extension VwrCandidateTDK {
|
||||||
}
|
}
|
||||||
.opacity(columnIndex == thePool.currentLineNumber ? 1 : 0.85)
|
.opacity(columnIndex == thePool.currentLineNumber ? 1 : 0.85)
|
||||||
if thePool.candidateLines[columnIndex].count < thePool.maxLineCapacity {
|
if thePool.candidateLines[columnIndex].count < thePool.maxLineCapacity {
|
||||||
|
let copied = CandidatePool.blankCell.cleanCopy
|
||||||
ForEach(0 ..< thePool.dummyCellsRequiredForCurrentLine, id: \.self) { _ in
|
ForEach(0 ..< thePool.dummyCellsRequiredForCurrentLine, id: \.self) { _ in
|
||||||
drawCandidate(thePool.blankCell)
|
drawCandidate(copied)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,17 +114,15 @@ private extension VwrCandidateTDK {
|
||||||
alignment: .topLeading
|
alignment: .topLeading
|
||||||
)
|
)
|
||||||
.id(columnIndex)
|
.id(columnIndex)
|
||||||
if thePool.maxLinesPerPage > 1, thePool.maxLinesPerPage <= loopIndex + 1 {
|
|
||||||
Spacer(minLength: 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count > 0 {
|
if thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count > 0 {
|
||||||
ForEach(Array(thePool.lineRangeForFinalPageBlanked.enumerated()), id: \.offset) { loopIndex, _ in
|
ForEach(Array(thePool.lineRangeForFinalPageBlanked.enumerated()), id: \.offset) { loopIndex, _ in
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
let copied = CandidatePool.blankCell.cleanCopy
|
||||||
ForEach(0 ..< thePool.maxLineCapacity, id: \.self) { _ in
|
ForEach(0 ..< thePool.maxLineCapacity, id: \.self) { _ in
|
||||||
attributedStringFor(cell: thePool.blankCell).fixedSize()
|
attributedStringFor(cell: copied).fixedSize()
|
||||||
.frame(
|
.frame(
|
||||||
width: ceil(thePool.blankCell.cellLength(isMatrix: true)),
|
width: ceil(CandidatePool.blankCell.cellLength(isMatrix: true)),
|
||||||
alignment: .topLeading
|
alignment: .topLeading
|
||||||
)
|
)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
|
@ -132,11 +132,6 @@ private extension VwrCandidateTDK {
|
||||||
maxWidth: .infinity,
|
maxWidth: .infinity,
|
||||||
alignment: .topLeading
|
alignment: .topLeading
|
||||||
)
|
)
|
||||||
if thePool.maxLinesPerPage > 1,
|
|
||||||
loopIndex >= thePool.maxLinesPerPage - thePool.lineRangeForCurrentPage.count - 1
|
|
||||||
{
|
|
||||||
Spacer(minLength: 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,7 +190,7 @@ extension VwrCandidateTDK {
|
||||||
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.cellLength())
|
Double(thePool.maxLineCapacity) * (CandidatePool.blankCell.cellLength())
|
||||||
+ spacings
|
+ spacings
|
||||||
)
|
)
|
||||||
return thePool.layout == .horizontal && thePool.maxLinesPerPage > 1 ? maxWindowWith : nil
|
return thePool.layout == .horizontal && thePool.maxLinesPerPage > 1 ? maxWindowWith : nil
|
||||||
|
@ -245,7 +240,7 @@ extension VwrCandidateTDK {
|
||||||
HStack {
|
HStack {
|
||||||
if !tooltip.isEmpty {
|
if !tooltip.isEmpty {
|
||||||
ZStack(alignment: .center) {
|
ZStack(alignment: .center) {
|
||||||
Circle().fill(thePool.blankCell.themeColor.opacity(0.8))
|
Circle().fill(CandidatePool.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))
|
||||||
}
|
}
|
||||||
|
@ -371,7 +366,7 @@ struct VwrCandidateTDK_Previews: PreviewProvider {
|
||||||
]
|
]
|
||||||
@State static var reverseLookupResult = ["mmmmm", "dddd"]
|
@State static var reverseLookupResult = ["mmmmm", "dddd"]
|
||||||
@State static var tooltip = "📼"
|
@State static var tooltip = "📼"
|
||||||
@State static var oldOS: Bool = true
|
@State static var oldOS: Bool = false
|
||||||
|
|
||||||
static var testCandidatesConverted: [(keyArray: [String], value: String)] {
|
static var testCandidatesConverted: [(keyArray: [String], value: String)] {
|
||||||
testCandidates.map { candidate in
|
testCandidates.map { candidate in
|
||||||
|
@ -381,7 +376,7 @@ struct VwrCandidateTDK_Previews: PreviewProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
static var thePoolX: CandidatePool {
|
static var thePoolX: CandidatePool {
|
||||||
var result = CandidatePool(
|
let result = CandidatePool(
|
||||||
candidates: testCandidatesConverted, lines: 4,
|
candidates: testCandidatesConverted, lines: 4,
|
||||||
selectionKeys: "123456", layout: .horizontal
|
selectionKeys: "123456", layout: .horizontal
|
||||||
)
|
)
|
||||||
|
@ -392,7 +387,7 @@ struct VwrCandidateTDK_Previews: PreviewProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
static var thePoolXS: CandidatePool {
|
static var thePoolXS: CandidatePool {
|
||||||
var result = CandidatePool(
|
let result = CandidatePool(
|
||||||
candidates: testCandidatesConverted, lines: 1,
|
candidates: testCandidatesConverted, lines: 1,
|
||||||
selectionKeys: "123456", layout: .horizontal
|
selectionKeys: "123456", layout: .horizontal
|
||||||
)
|
)
|
||||||
|
@ -403,7 +398,7 @@ struct VwrCandidateTDK_Previews: PreviewProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
static var thePoolY: CandidatePool {
|
static var thePoolY: CandidatePool {
|
||||||
var result = CandidatePool(
|
let result = CandidatePool(
|
||||||
candidates: testCandidatesConverted, lines: 4,
|
candidates: testCandidatesConverted, lines: 4,
|
||||||
selectionKeys: "123456", layout: .vertical
|
selectionKeys: "123456", layout: .vertical
|
||||||
)
|
)
|
||||||
|
@ -411,11 +406,12 @@ struct VwrCandidateTDK_Previews: PreviewProvider {
|
||||||
result.tooltip = Self.tooltip
|
result.tooltip = Self.tooltip
|
||||||
result.flipPage(isBackward: false)
|
result.flipPage(isBackward: false)
|
||||||
result.highlight(at: 2)
|
result.highlight(at: 2)
|
||||||
|
result.highlight(at: 21)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
static var thePoolYS: CandidatePool {
|
static var thePoolYS: CandidatePool {
|
||||||
var result = CandidatePool(
|
let result = CandidatePool(
|
||||||
candidates: testCandidatesConverted, lines: 1,
|
candidates: testCandidatesConverted, lines: 1,
|
||||||
selectionKeys: "123456", layout: .vertical
|
selectionKeys: "123456", layout: .vertical
|
||||||
)
|
)
|
||||||
|
|
|
@ -273,6 +273,14 @@ public extension SessionCtl {
|
||||||
inputMode = IMEApp.currentInputMode
|
inputMode = IMEApp.currentInputMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
// 清理掉上一個會話的選字窗及其選單。
|
||||||
|
self.candidateUI = nil
|
||||||
|
CtlCandidateTDK.currentMenu?.cancelTracking()
|
||||||
|
CtlCandidateTDK.currentMenu = nil
|
||||||
|
CtlCandidateTDK.currentWindow?.orderOut(nil)
|
||||||
|
CtlCandidateTDK.currentWindow = nil
|
||||||
|
}
|
||||||
DispatchQueue.main.async { [self] in
|
DispatchQueue.main.async { [self] in
|
||||||
if isActivated { return }
|
if isActivated { return }
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue