From b2ee0e3972d51d84709b63ef228c65b2b408dd7a Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Wed, 9 Aug 2023 23:53:09 +0800 Subject: [PATCH] TDKCandidates // Implement page-expansion feature. --- .../CandidateWindow/CandidatePool.swift | 64 +++++++++++++++---- .../CandidatePool_CocoaImpl.swift | 8 +-- .../TDKCandidates/CtlCandidateTDK.swift | 2 +- .../TDKCandidates/VwrCandidateTDK_Cocoa.swift | 4 +- .../VwrCandidateTDK_SwiftUI.swift | 4 +- 5 files changed, 62 insertions(+), 20 deletions(-) diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool.swift index 67c0829b..6f1c5bd3 100644 --- a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool.swift +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool.swift @@ -14,16 +14,17 @@ public class CandidatePool { // 只用來測量單漢字候選字 cell 的最大可能寬度。 public static let shitCell = 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 selectionKeys: String public private(set) var candidateDataAll: [CandidateCellData] - public var candidateLines: [[CandidateCellData]] = [] - public var tooltip: String = "" - public var reverseLookupResult: [String] = [] + public private(set) var candidateLines: [[CandidateCellData]] = [] public private(set) var highlightedIndex: Int = 0 public private(set) var currentLineNumber = 0 + public private(set) var isExpanded: Bool = false public var metrics: UIMetrics = .allZeroed + public var tooltip: String = "" + public var reverseLookupResult: [String] = [] private var recordedLineRangeForCurrentPage: Range? private var previouslyRecordedLineRangeForPreviousPage: Range? @@ -47,9 +48,15 @@ public class CandidatePool { public let cellRadius: CGFloat = 4 public var windowRadius: CGFloat { originDelta + cellRadius } - /// 當前資料池是否存在多列/多行候選字詞呈現。 + /// 當前資料池每頁顯示的最大行/列數。 + public var maxLinesPerPage: Int { isExpanded ? _maxLinesPerPage : 1 } + + /// 當前資料池是否正在以多列/多行的形式呈現候選字詞。 public var isMatrix: Bool { maxLinesPerPage > 1 } + /// 當前資料池是否能夠以多列/多行的形式呈現候選字詞。 + public var isExpandable: Bool { _maxLinesPerPage > 1 } + /// 用來在初期化一個候選字詞資料池的時候研判「橫版多行選字窗每行最大應該塞多少個候選字詞」。 /// 注意:該參數不用來計算視窗寬度,所以無須算上候選字詞間距。 public var maxRowWidth: Double { ceil(Double(maxLineCapacity) * Self.blankCell.cellLength()) } @@ -99,15 +106,16 @@ public class CandidatePool { /// - direction: 橫向排列還是縱向排列(預設情況下是縱向)。 /// - locale: 區域編碼。例:「zh-Hans」或「zh-Hant」。 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 = "" ) { - maxLinesPerPage = 1 + _maxLinesPerPage = max(1, lines) + isExpanded = expanded self.layout = .horizontal self.selectionKeys = "123456789" candidateDataAll = [] // 以上只是為了糊弄 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: 橫向排列還是縱向排列(預設情況下是縱向)。 /// - locale: 區域編碼。例:「zh-Hans」或「zh-Hant」。 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 = "" ) { self.layout = layout - maxLinesPerPage = max(1, lines) Self.blankCell.locale = locale self.selectionKeys = selectionKeys.isEmpty ? "123456789" : selectionKeys 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: 是否逆向翻頁。 /// - Returns: 操作是否順利。 @discardableResult func flipPage(isBackward: Bool) -> Bool { + if !isExpanded, isExpandable { + expandIfNeeded(isBackward: isBackward) + return true + } backupLineRangeForCurrentPage() 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: 翻幾行。 /// - Returns: 操作是否順利。 @discardableResult func consecutivelyFlipLines(isBackward: Bool, count: Int) -> Bool { + expandIfNeeded(isBackward: isBackward) switch isBackward { case false where currentLineNumber == candidateLines.count - 1: return highlightNeighborCandidate(isBackward: false) diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool_CocoaImpl.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool_CocoaImpl.swift index e8b972ce..ee28f6be 100644 --- a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool_CocoaImpl.swift +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool_CocoaImpl.swift @@ -170,7 +170,7 @@ extension CandidatePool { arrLine.enumerated().forEach { cellID, currentCell in let cellString = NSMutableAttributedString( attributedString: currentCell.attributedString( - noSpacePadding: false, withHighlight: true, isMatrix: maxLinesPerPage > 1 + noSpacePadding: false, withHighlight: true, isMatrix: isMatrix ) ) if lineID != currentLineNumber { @@ -184,7 +184,7 @@ extension CandidatePool { result.append(spacer) } } - if lineID < lineRangeForCurrentPage.upperBound - 1 || maxLinesPerPage > 1 { + if lineID < lineRangeForCurrentPage.upperBound - 1 || isMatrix { result.append(lineFeed) } else { result.append(spacer) @@ -211,7 +211,7 @@ extension CandidatePool { let currentCell = lineData[inlineIndex] let cellString = NSMutableAttributedString( attributedString: currentCell.attributedString( - noSpacePadding: false, withHighlight: true, isMatrix: maxLinesPerPage > 1 + noSpacePadding: false, withHighlight: true, isMatrix: isMatrix ) ) if lineID != currentLineNumber { @@ -221,7 +221,7 @@ extension CandidatePool { ) } result.append(cellString) - if maxLinesPerPage > 1, currentCell.displayedText.count > 1 { + if isMatrix, currentCell.displayedText.count > 1 { if currentCell.isHighlighted { spacer.addAttribute( .backgroundColor, diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/CtlCandidateTDK.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/CtlCandidateTDK.swift index 545170e3..9d8c0ff5 100644 --- a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/CtlCandidateTDK.swift +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/CtlCandidateTDK.swift @@ -96,7 +96,7 @@ public class CtlCandidateTDK: CtlCandidate, NSWindowDelegate { CandidateCellData.unifiedSize = candidateFont.pointSize guard let delegate = delegate else { return } 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 ) Self.thePool.tooltip = tooltip diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_Cocoa.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_Cocoa.swift index b011f296..f03d17d4 100644 --- a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_Cocoa.swift +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_Cocoa.swift @@ -191,7 +191,7 @@ wrappedCell.setHuggingPriority(.fittingSizeCompression, for: .vertical) Self.makeSimpleConstraint(item: wrappedCell, attribute: .height, relation: .equal, value: cellHeight) switch thePool.layout { - case .horizontal where thePool.maxLinesPerPage > 1: + case .horizontal where thePool.isMatrix: Self.makeSimpleConstraint(item: wrappedCell, attribute: .width, relation: .equal, value: cellWidth) default: Self.makeSimpleConstraint(item: wrappedCell, attribute: .width, relation: .greaterThanOrEqual, value: cellWidth) @@ -216,7 +216,7 @@ private func generateLineContainer(_ theLine: inout [CandidateCellData]) -> NSStackView { let isVerticalListing: Bool = thePool.layout == .vertical - let isMatrix = thePool.maxLinesPerPage > 1 + let isMatrix = thePool.isMatrix let vwrCurrentLine = NSStackView() vwrCurrentLine.spacing = 0 vwrCurrentLine.orientation = isVerticalListing ? .vertical : .horizontal diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_SwiftUI.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_SwiftUI.swift index 3330abbc..2185deb6 100644 --- a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_SwiftUI.swift +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/TDKCandidates/VwrCandidateTDK_SwiftUI.swift @@ -41,7 +41,7 @@ public struct VwrCandidateTDK: View { default: mainViewVertical.background(candidateListBackground) } - if thePool.maxLinesPerPage > 1 || thePool.layout == .vertical { + if thePool.isMatrix || thePool.layout == .vertical { statusBarContent } } @@ -193,7 +193,7 @@ extension VwrCandidateTDK { Double(thePool.maxLineCapacity) * (CandidatePool.blankCell.cellLength()) + spacings ) - return thePool.layout == .horizontal && thePool.maxLinesPerPage > 1 ? maxWindowWith : nil + return thePool.layout == .horizontal && thePool.isMatrix ? maxWindowWith : nil } var firstReverseLookupResult: String {