vChewing-macOS/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool.swift

252 lines
10 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// (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
///
public class CandidatePool {
public let blankCell = CandidateCellData(key: " ", displayedText: " ", isSelected: false)
public private(set) var candidateDataAll: [CandidateCellData] = []
public private(set) var selectionKeys: String
public private(set) var highlightedIndex: Int = 0
//
public var currentRowNumber = 0
public var maximumRowsPerPage = 3
public private(set) var maxRowCapacity: Int = 6
public private(set) var candidateRows: [[CandidateCellData]] = []
//
public var currentColumnNumber = 0
public var maximumColumnsPerPage = 3
public private(set) var maxColumnCapacity: Int = 6
public private(set) var candidateColumns: [[CandidateCellData]] = []
//
public var maxRowWidth: Int { Int(Double(maxRowCapacity + 3) * 2) * Int(ceil(CandidateCellData.unifiedSize)) }
public var maxWindowWidth: Double {
ceil(Double(maxRowCapacity + 3) * 2.7 * ceil(CandidateCellData.unifiedSize) * 1.2)
}
public var rangeForCurrentHorizontalPage: Range<Int> {
currentRowNumber..<min(candidateRows.count, currentRowNumber + maximumRowsPerPage)
}
public var rangeForCurrentVerticalPage: Range<Int> {
currentColumnNumber..<min(candidateColumns.count, currentColumnNumber + maximumColumnsPerPage)
}
public var rangeForLastHorizontalPageBlanked: Range<Int> {
0..<(maximumRowsPerPage - rangeForCurrentHorizontalPage.count)
}
public var rangeForLastVerticalPageBlanked: Range<Int> {
0..<(maximumColumnsPerPage - rangeForCurrentVerticalPage.count)
}
public enum VerticalDirection {
case up
case down
}
public enum HorizontalDirection {
case left
case right
}
///
/// - Parameters:
/// - candidates:
/// - columnCapacity: (, )
/// - selectionKeys:
/// - locale: zh-Hanszh-Hant
public init(candidates: [String], columnCapacity: Int, selectionKeys: String = "123456789", locale: String = "") {
maxColumnCapacity = max(1, columnCapacity)
self.selectionKeys = selectionKeys
candidateDataAll = candidates.map { .init(key: "0", displayedText: $0) }
var currentColumn: [CandidateCellData] = []
for (i, candidate) in candidateDataAll.enumerated() {
candidate.index = i
candidate.whichColumn = candidateColumns.count
if currentColumn.count == maxColumnCapacity, !currentColumn.isEmpty {
candidateColumns.append(currentColumn)
currentColumn.removeAll()
candidate.whichColumn += 1
}
candidate.subIndex = currentColumn.count
candidate.locale = locale
currentColumn.append(candidate)
}
candidateColumns.append(currentColumn)
}
///
/// - Parameters:
/// - candidates:
/// - rowCapacity: (, )
/// - selectionKeys:
/// - locale: zh-Hanszh-Hant
public init(candidates: [String], rowCapacity: Int, selectionKeys: String = "123456789", locale: String = "") {
maxRowCapacity = max(1, rowCapacity)
self.selectionKeys = selectionKeys
candidateDataAll = candidates.map { .init(key: "0", displayedText: $0) }
var currentRow: [CandidateCellData] = []
for (i, candidate) in candidateDataAll.enumerated() {
candidate.index = i
candidate.whichRow = candidateRows.count
let isOverflown: Bool = currentRow.map(\.cellLength).reduce(0, +) + candidate.cellLength > maxRowWidth
if isOverflown || currentRow.count == maxRowCapacity, !currentRow.isEmpty {
candidateRows.append(currentRow)
currentRow.removeAll()
candidate.whichRow += 1
}
candidate.subIndex = currentRow.count
candidate.locale = locale
currentRow.append(candidate)
}
candidateRows.append(currentRow)
}
public func selectNewNeighborRow(direction: VerticalDirection) {
let currentSubIndex = candidateDataAll[highlightedIndex].subIndex
var result = currentSubIndex
switch direction {
case .up:
if currentRowNumber <= 0 {
if candidateRows.isEmpty { break }
let firstRow = candidateRows[0]
let newSubIndex = min(currentSubIndex, firstRow.count - 1)
highlightHorizontal(at: firstRow[newSubIndex].index)
break
}
if currentRowNumber >= candidateRows.count - 1 { currentRowNumber = candidateRows.count - 1 }
if candidateRows[currentRowNumber].count != candidateRows[currentRowNumber - 1].count {
let ratio: Double = min(1, Double(currentSubIndex) / Double(candidateRows[currentRowNumber].count))
result = Int(floor(Double(candidateRows[currentRowNumber - 1].count) * ratio))
}
let targetRow = candidateRows[currentRowNumber - 1]
let newSubIndex = min(result, targetRow.count - 1)
highlightHorizontal(at: targetRow[newSubIndex].index)
case .down:
if currentRowNumber >= candidateRows.count - 1 {
if candidateRows.isEmpty { break }
let finalRow = candidateRows[candidateRows.count - 1]
let newSubIndex = min(currentSubIndex, finalRow.count - 1)
highlightHorizontal(at: finalRow[newSubIndex].index)
break
}
if candidateRows[currentRowNumber].count != candidateRows[currentRowNumber + 1].count {
let ratio: Double = min(1, Double(currentSubIndex) / Double(candidateRows[currentRowNumber].count))
result = Int(floor(Double(candidateRows[currentRowNumber + 1].count) * ratio))
}
let targetRow = candidateRows[currentRowNumber + 1]
let newSubIndex = min(result, targetRow.count - 1)
highlightHorizontal(at: targetRow[newSubIndex].index)
}
}
public func selectNewNeighborColumn(direction: HorizontalDirection) {
let currentSubIndex = candidateDataAll[highlightedIndex].subIndex
switch direction {
case .left:
if currentColumnNumber <= 0 {
if candidateColumns.isEmpty { break }
let firstColumn = candidateColumns[0]
let newSubIndex = min(currentSubIndex, firstColumn.count - 1)
highlightVertical(at: firstColumn[newSubIndex].index)
break
}
if currentColumnNumber >= candidateColumns.count - 1 { currentColumnNumber = candidateColumns.count - 1 }
let targetColumn = candidateColumns[currentColumnNumber - 1]
let newSubIndex = min(currentSubIndex, targetColumn.count - 1)
highlightVertical(at: targetColumn[newSubIndex].index)
case .right:
if currentColumnNumber >= candidateColumns.count - 1 {
if candidateColumns.isEmpty { break }
let finalColumn = candidateColumns[candidateColumns.count - 1]
let newSubIndex = min(currentSubIndex, finalColumn.count - 1)
highlightVertical(at: finalColumn[newSubIndex].index)
break
}
let targetColumn = candidateColumns[currentColumnNumber + 1]
let newSubIndex = min(currentSubIndex, targetColumn.count - 1)
highlightVertical(at: targetColumn[newSubIndex].index)
}
}
public func highlightHorizontal(at indexSpecified: Int) {
var indexSpecified = indexSpecified
highlightedIndex = indexSpecified
if !(0..<candidateDataAll.count).contains(highlightedIndex) {
NSSound.beep()
switch highlightedIndex {
case candidateDataAll.count...:
currentRowNumber = candidateRows.count - 1
highlightedIndex = max(0, candidateDataAll.count - 1)
indexSpecified = highlightedIndex
case ..<0:
highlightedIndex = 0
currentRowNumber = 0
indexSpecified = highlightedIndex
default: break
}
}
for (i, candidate) in candidateDataAll.enumerated() {
candidate.isSelected = (indexSpecified == i)
if candidate.isSelected { currentRowNumber = candidate.whichRow }
}
for (i, candidateRow) in candidateRows.enumerated() {
if i != currentRowNumber {
candidateRow.forEach {
$0.key = " "
}
} else {
for (i, neta) in candidateRow.enumerated() {
neta.key = selectionKeys.map { String($0) }[i]
}
}
}
}
public func highlightVertical(at indexSpecified: Int) {
var indexSpecified = indexSpecified
highlightedIndex = indexSpecified
if !(0..<candidateDataAll.count).contains(highlightedIndex) {
NSSound.beep()
switch highlightedIndex {
case candidateDataAll.count...:
currentColumnNumber = candidateColumns.count - 1
highlightedIndex = max(0, candidateDataAll.count - 1)
indexSpecified = highlightedIndex
case ..<0:
highlightedIndex = 0
currentColumnNumber = 0
indexSpecified = highlightedIndex
default: break
}
}
for (i, candidate) in candidateDataAll.enumerated() {
candidate.isSelected = (indexSpecified == i)
if candidate.isSelected { currentColumnNumber = candidate.whichColumn }
}
for (i, candidateColumn) in candidateColumns.enumerated() {
if i != currentColumnNumber {
candidateColumn.forEach {
$0.key = " "
}
} else {
for (i, neta) in candidateColumn.enumerated() {
if neta.key.isEmpty { continue }
neta.key = selectionKeys.map { String($0) }[i]
}
}
}
}
}