TDKCandidates // Implement page-expansion feature.
This commit is contained in:
parent
2abb86f4b8
commit
b2ee0e3972
|
@ -14,16 +14,17 @@ public class CandidatePool {
|
||||||
// 只用來測量單漢字候選字 cell 的最大可能寬度。
|
// 只用來測量單漢字候選字 cell 的最大可能寬度。
|
||||||
public static let shitCell = CandidateCellData(key: " ", displayedText: "💩", isSelected: false)
|
public static let shitCell = CandidateCellData(key: " ", displayedText: "💩", isSelected: false)
|
||||||
public static let blankCell = CandidateCellData(key: " ", displayedText: " ", isSelected: false)
|
public static let blankCell = CandidateCellData(key: " ", displayedText: " ", isSelected: false)
|
||||||
public private(set) var maxLinesPerPage: Int
|
public private(set) var _maxLinesPerPage: Int
|
||||||
public private(set) var layout: LayoutOrientation
|
public private(set) var layout: LayoutOrientation
|
||||||
public private(set) var selectionKeys: String
|
public private(set) var selectionKeys: String
|
||||||
public private(set) var candidateDataAll: [CandidateCellData]
|
public private(set) var candidateDataAll: [CandidateCellData]
|
||||||
public var candidateLines: [[CandidateCellData]] = []
|
public private(set) var candidateLines: [[CandidateCellData]] = []
|
||||||
public var tooltip: String = ""
|
|
||||||
public var reverseLookupResult: [String] = []
|
|
||||||
public private(set) var highlightedIndex: Int = 0
|
public private(set) var highlightedIndex: Int = 0
|
||||||
public private(set) var currentLineNumber = 0
|
public private(set) var currentLineNumber = 0
|
||||||
|
public private(set) var isExpanded: Bool = false
|
||||||
public var metrics: UIMetrics = .allZeroed
|
public var metrics: UIMetrics = .allZeroed
|
||||||
|
public var tooltip: String = ""
|
||||||
|
public var reverseLookupResult: [String] = []
|
||||||
|
|
||||||
private var recordedLineRangeForCurrentPage: Range<Int>?
|
private var recordedLineRangeForCurrentPage: Range<Int>?
|
||||||
private var previouslyRecordedLineRangeForPreviousPage: Range<Int>?
|
private var previouslyRecordedLineRangeForPreviousPage: Range<Int>?
|
||||||
|
@ -47,9 +48,15 @@ public class CandidatePool {
|
||||||
public let cellRadius: CGFloat = 4
|
public let cellRadius: CGFloat = 4
|
||||||
public var windowRadius: CGFloat { originDelta + cellRadius }
|
public var windowRadius: CGFloat { originDelta + cellRadius }
|
||||||
|
|
||||||
/// 當前資料池是否存在多列/多行候選字詞呈現。
|
/// 當前資料池每頁顯示的最大行/列數。
|
||||||
|
public var maxLinesPerPage: Int { isExpanded ? _maxLinesPerPage : 1 }
|
||||||
|
|
||||||
|
/// 當前資料池是否正在以多列/多行的形式呈現候選字詞。
|
||||||
public var isMatrix: Bool { maxLinesPerPage > 1 }
|
public var isMatrix: Bool { maxLinesPerPage > 1 }
|
||||||
|
|
||||||
|
/// 當前資料池是否能夠以多列/多行的形式呈現候選字詞。
|
||||||
|
public var isExpandable: Bool { _maxLinesPerPage > 1 }
|
||||||
|
|
||||||
/// 用來在初期化一個候選字詞資料池的時候研判「橫版多行選字窗每行最大應該塞多少個候選字詞」。
|
/// 用來在初期化一個候選字詞資料池的時候研判「橫版多行選字窗每行最大應該塞多少個候選字詞」。
|
||||||
/// 注意:該參數不用來計算視窗寬度,所以無須算上候選字詞間距。
|
/// 注意:該參數不用來計算視窗寬度,所以無須算上候選字詞間距。
|
||||||
public var maxRowWidth: Double { ceil(Double(maxLineCapacity) * Self.blankCell.cellLength()) }
|
public var maxRowWidth: Double { ceil(Double(maxLineCapacity) * Self.blankCell.cellLength()) }
|
||||||
|
@ -99,15 +106,16 @@ public class CandidatePool {
|
||||||
/// - direction: 橫向排列還是縱向排列(預設情況下是縱向)。
|
/// - direction: 橫向排列還是縱向排列(預設情況下是縱向)。
|
||||||
/// - locale: 區域編碼。例:「zh-Hans」或「zh-Hant」。
|
/// - locale: 區域編碼。例:「zh-Hans」或「zh-Hant」。
|
||||||
public init(
|
public init(
|
||||||
candidates: [(keyArray: [String], value: String)], lines: Int = 3, selectionKeys: String = "123456789",
|
candidates: [(keyArray: [String], value: String)], lines: Int = 3, isExpanded expanded: Bool = true, selectionKeys: String = "123456789",
|
||||||
layout: LayoutOrientation = .vertical, locale: String = ""
|
layout: LayoutOrientation = .vertical, locale: String = ""
|
||||||
) {
|
) {
|
||||||
maxLinesPerPage = 1
|
_maxLinesPerPage = max(1, lines)
|
||||||
|
isExpanded = expanded
|
||||||
self.layout = .horizontal
|
self.layout = .horizontal
|
||||||
self.selectionKeys = "123456789"
|
self.selectionKeys = "123456789"
|
||||||
candidateDataAll = []
|
candidateDataAll = []
|
||||||
// 以上只是為了糊弄 compiler。接下來才是正式的初期化。
|
// 以上只是為了糊弄 compiler。接下來才是正式的初期化。
|
||||||
construct(candidates: candidates, lines: lines, selectionKeys: selectionKeys, layout: layout, locale: locale)
|
construct(candidates: candidates, selectionKeys: selectionKeys, layout: layout, locale: locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 初期化(或者自我重新初期化)一個候選字窗專用資料池。
|
/// 初期化(或者自我重新初期化)一個候選字窗專用資料池。
|
||||||
|
@ -117,11 +125,10 @@ public class CandidatePool {
|
||||||
/// - direction: 橫向排列還是縱向排列(預設情況下是縱向)。
|
/// - direction: 橫向排列還是縱向排列(預設情況下是縱向)。
|
||||||
/// - locale: 區域編碼。例:「zh-Hans」或「zh-Hant」。
|
/// - locale: 區域編碼。例:「zh-Hans」或「zh-Hant」。
|
||||||
private func construct(
|
private func construct(
|
||||||
candidates: [(keyArray: [String], value: String)], lines: Int = 3, selectionKeys: String = "123456789",
|
candidates: [(keyArray: [String], value: String)], selectionKeys: String = "123456789",
|
||||||
layout: LayoutOrientation = .vertical, locale: String = ""
|
layout: LayoutOrientation = .vertical, locale: String = ""
|
||||||
) {
|
) {
|
||||||
self.layout = layout
|
self.layout = layout
|
||||||
maxLinesPerPage = max(1, lines)
|
|
||||||
Self.blankCell.locale = locale
|
Self.blankCell.locale = locale
|
||||||
self.selectionKeys = selectionKeys.isEmpty ? "123456789" : selectionKeys
|
self.selectionKeys = selectionKeys.isEmpty ? "123456789" : selectionKeys
|
||||||
var allCandidates = candidates.map {
|
var allCandidates = candidates.map {
|
||||||
|
@ -172,13 +179,47 @@ public extension CandidatePool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func expandIfNeeded(isBackward: Bool) {
|
||||||
|
guard !candidateLines.isEmpty, !isExpanded, isExpandable else { return }
|
||||||
|
let candidatesShown: [CandidateCellData] = candidateLines[lineRangeForCurrentPage].flatMap { $0 }
|
||||||
|
guard !candidatesShown.filter(\.isHighlighted).isEmpty else { return }
|
||||||
|
isExpanded = true
|
||||||
|
if candidateLines.count <= _maxLinesPerPage {
|
||||||
|
recordedLineRangeForCurrentPage = max(0, currentLineNumber - _maxLinesPerPage + 1) ..< currentLineNumber + 1
|
||||||
|
} else {
|
||||||
|
switch isBackward {
|
||||||
|
case true:
|
||||||
|
if lineRangeForFirstPage.contains(currentLineNumber) {
|
||||||
|
recordedLineRangeForCurrentPage = lineRangeForFirstPage
|
||||||
|
} else {
|
||||||
|
recordedLineRangeForCurrentPage = max(0, currentLineNumber - _maxLinesPerPage + 1) ..< currentLineNumber + 1
|
||||||
|
}
|
||||||
|
case false:
|
||||||
|
if lineRangeForFinalPage.contains(currentLineNumber) {
|
||||||
|
recordedLineRangeForCurrentPage = lineRangeForFinalPage
|
||||||
|
} else {
|
||||||
|
recordedLineRangeForCurrentPage = currentLineNumber ..< min(candidateLines.count, currentLineNumber + _maxLinesPerPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateMetrics()
|
||||||
|
}
|
||||||
|
|
||||||
/// 往指定的方向翻頁。
|
/// 往指定的方向翻頁。
|
||||||
/// - Parameter isBackward: 是否逆向翻頁。
|
/// - Parameter isBackward: 是否逆向翻頁。
|
||||||
/// - Returns: 操作是否順利。
|
/// - Returns: 操作是否順利。
|
||||||
@discardableResult func flipPage(isBackward: Bool) -> Bool {
|
@discardableResult func flipPage(isBackward: Bool) -> Bool {
|
||||||
|
if !isExpanded, isExpandable {
|
||||||
|
expandIfNeeded(isBackward: isBackward)
|
||||||
|
return true
|
||||||
|
}
|
||||||
backupLineRangeForCurrentPage()
|
backupLineRangeForCurrentPage()
|
||||||
defer { flipLineRangeToNeighborPage(isBackward: isBackward) }
|
defer { flipLineRangeToNeighborPage(isBackward: isBackward) }
|
||||||
return consecutivelyFlipLines(isBackward: isBackward, count: maxLinesPerPage)
|
var theCount = maxLinesPerPage
|
||||||
|
let rareConditionA: Bool = isBackward && currentLineNumber == 0
|
||||||
|
let rareConditionB: Bool = !isBackward && currentLineNumber == candidateLines.count - 1
|
||||||
|
if rareConditionA || rareConditionB { theCount = 1 }
|
||||||
|
return consecutivelyFlipLines(isBackward: isBackward, count: theCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 嘗試用給定的行內編號推算該候選字在資料池內的總編號。
|
/// 嘗試用給定的行內編號推算該候選字在資料池內的總編號。
|
||||||
|
@ -196,6 +237,7 @@ public extension CandidatePool {
|
||||||
/// - count: 翻幾行。
|
/// - count: 翻幾行。
|
||||||
/// - Returns: 操作是否順利。
|
/// - Returns: 操作是否順利。
|
||||||
@discardableResult func consecutivelyFlipLines(isBackward: Bool, count: Int) -> Bool {
|
@discardableResult func consecutivelyFlipLines(isBackward: Bool, count: Int) -> Bool {
|
||||||
|
expandIfNeeded(isBackward: isBackward)
|
||||||
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)
|
||||||
|
|
|
@ -170,7 +170,7 @@ extension CandidatePool {
|
||||||
arrLine.enumerated().forEach { cellID, currentCell in
|
arrLine.enumerated().forEach { cellID, currentCell in
|
||||||
let cellString = NSMutableAttributedString(
|
let cellString = NSMutableAttributedString(
|
||||||
attributedString: currentCell.attributedString(
|
attributedString: currentCell.attributedString(
|
||||||
noSpacePadding: false, withHighlight: true, isMatrix: maxLinesPerPage > 1
|
noSpacePadding: false, withHighlight: true, isMatrix: isMatrix
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if lineID != currentLineNumber {
|
if lineID != currentLineNumber {
|
||||||
|
@ -184,7 +184,7 @@ extension CandidatePool {
|
||||||
result.append(spacer)
|
result.append(spacer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if lineID < lineRangeForCurrentPage.upperBound - 1 || maxLinesPerPage > 1 {
|
if lineID < lineRangeForCurrentPage.upperBound - 1 || isMatrix {
|
||||||
result.append(lineFeed)
|
result.append(lineFeed)
|
||||||
} else {
|
} else {
|
||||||
result.append(spacer)
|
result.append(spacer)
|
||||||
|
@ -211,7 +211,7 @@ extension CandidatePool {
|
||||||
let currentCell = lineData[inlineIndex]
|
let currentCell = lineData[inlineIndex]
|
||||||
let cellString = NSMutableAttributedString(
|
let cellString = NSMutableAttributedString(
|
||||||
attributedString: currentCell.attributedString(
|
attributedString: currentCell.attributedString(
|
||||||
noSpacePadding: false, withHighlight: true, isMatrix: maxLinesPerPage > 1
|
noSpacePadding: false, withHighlight: true, isMatrix: isMatrix
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if lineID != currentLineNumber {
|
if lineID != currentLineNumber {
|
||||||
|
@ -221,7 +221,7 @@ extension CandidatePool {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
result.append(cellString)
|
result.append(cellString)
|
||||||
if maxLinesPerPage > 1, currentCell.displayedText.count > 1 {
|
if isMatrix, currentCell.displayedText.count > 1 {
|
||||||
if currentCell.isHighlighted {
|
if currentCell.isHighlighted {
|
||||||
spacer.addAttribute(
|
spacer.addAttribute(
|
||||||
.backgroundColor,
|
.backgroundColor,
|
||||||
|
|
|
@ -96,7 +96,7 @@ public class CtlCandidateTDK: CtlCandidate, NSWindowDelegate {
|
||||||
CandidateCellData.unifiedSize = candidateFont.pointSize
|
CandidateCellData.unifiedSize = candidateFont.pointSize
|
||||||
guard let delegate = delegate else { return }
|
guard let delegate = delegate else { return }
|
||||||
Self.thePool = .init(
|
Self.thePool = .init(
|
||||||
candidates: delegate.candidatePairs(conv: true), lines: maxLinesPerPage,
|
candidates: delegate.candidatePairs(conv: true), lines: maxLinesPerPage, isExpanded: false,
|
||||||
selectionKeys: delegate.selectionKeys, layout: currentLayout.layoutTDK, locale: locale
|
selectionKeys: delegate.selectionKeys, layout: currentLayout.layoutTDK, locale: locale
|
||||||
)
|
)
|
||||||
Self.thePool.tooltip = tooltip
|
Self.thePool.tooltip = tooltip
|
||||||
|
|
|
@ -191,7 +191,7 @@
|
||||||
wrappedCell.setHuggingPriority(.fittingSizeCompression, for: .vertical)
|
wrappedCell.setHuggingPriority(.fittingSizeCompression, for: .vertical)
|
||||||
Self.makeSimpleConstraint(item: wrappedCell, attribute: .height, relation: .equal, value: cellHeight)
|
Self.makeSimpleConstraint(item: wrappedCell, attribute: .height, relation: .equal, value: cellHeight)
|
||||||
switch thePool.layout {
|
switch thePool.layout {
|
||||||
case .horizontal where thePool.maxLinesPerPage > 1:
|
case .horizontal where thePool.isMatrix:
|
||||||
Self.makeSimpleConstraint(item: wrappedCell, attribute: .width, relation: .equal, value: cellWidth)
|
Self.makeSimpleConstraint(item: wrappedCell, attribute: .width, relation: .equal, value: cellWidth)
|
||||||
default:
|
default:
|
||||||
Self.makeSimpleConstraint(item: wrappedCell, attribute: .width, relation: .greaterThanOrEqual, value: cellWidth)
|
Self.makeSimpleConstraint(item: wrappedCell, attribute: .width, relation: .greaterThanOrEqual, value: cellWidth)
|
||||||
|
@ -216,7 +216,7 @@
|
||||||
|
|
||||||
private func generateLineContainer(_ theLine: inout [CandidateCellData]) -> NSStackView {
|
private func generateLineContainer(_ theLine: inout [CandidateCellData]) -> NSStackView {
|
||||||
let isVerticalListing: Bool = thePool.layout == .vertical
|
let isVerticalListing: Bool = thePool.layout == .vertical
|
||||||
let isMatrix = thePool.maxLinesPerPage > 1
|
let isMatrix = thePool.isMatrix
|
||||||
let vwrCurrentLine = NSStackView()
|
let vwrCurrentLine = NSStackView()
|
||||||
vwrCurrentLine.spacing = 0
|
vwrCurrentLine.spacing = 0
|
||||||
vwrCurrentLine.orientation = isVerticalListing ? .vertical : .horizontal
|
vwrCurrentLine.orientation = isVerticalListing ? .vertical : .horizontal
|
||||||
|
|
|
@ -41,7 +41,7 @@ public struct VwrCandidateTDK: View {
|
||||||
default:
|
default:
|
||||||
mainViewVertical.background(candidateListBackground)
|
mainViewVertical.background(candidateListBackground)
|
||||||
}
|
}
|
||||||
if thePool.maxLinesPerPage > 1 || thePool.layout == .vertical {
|
if thePool.isMatrix || thePool.layout == .vertical {
|
||||||
statusBarContent
|
statusBarContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,7 +193,7 @@ extension VwrCandidateTDK {
|
||||||
Double(thePool.maxLineCapacity) * (CandidatePool.blankCell.cellLength())
|
Double(thePool.maxLineCapacity) * (CandidatePool.blankCell.cellLength())
|
||||||
+ spacings
|
+ spacings
|
||||||
)
|
)
|
||||||
return thePool.layout == .horizontal && thePool.maxLinesPerPage > 1 ? maxWindowWith : nil
|
return thePool.layout == .horizontal && thePool.isMatrix ? maxWindowWith : nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstReverseLookupResult: String {
|
var firstReverseLookupResult: String {
|
||||||
|
|
Loading…
Reference in New Issue