CtlCandidateTDK // Vertical candidate layout support, etc.

- SP2: Fix a color scheme mistake in bright mode.
This commit is contained in:
ShikiSuen 2022-09-29 21:22:51 +08:00
parent 8fd236a065
commit 030a8cb776
18 changed files with 573 additions and 258 deletions

View File

@ -0,0 +1,37 @@
// (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 Shared
import SwiftUI
@available(macOS 12, *)
extension CandidateCellData {
public var attributedStringForSwiftUI: some View {
var result: some View {
ZStack(alignment: .leading) {
if isSelected {
Color(nsColor: CandidateCellData.highlightBackground).ignoresSafeArea().cornerRadius(6)
}
VStack(spacing: 0) {
HStack(spacing: 4) {
if UserDefaults.standard.bool(forKey: UserDef.kHandleDefaultCandidateFontsByLangIdentifier.rawValue) {
Text(AttributedString(attributedStringHeader)).frame(width: CandidateCellData.unifiedSize / 2)
Text(AttributedString(attributedString))
} else {
Text(key).font(.system(size: fontSizeKey).monospaced())
.foregroundColor(.init(nsColor: fontColorKey)).lineLimit(1)
Text(displayedText).font(.system(size: fontSizeCandidate))
.foregroundColor(.init(nsColor: fontColorCandidate)).lineLimit(1)
}
}.padding(4)
}
}.fixedSize(horizontal: false, vertical: true)
}
return result
}
}

View File

@ -12,53 +12,105 @@ import Shared
///
public class CandidatePool {
public let blankCell = CandidateCellData(key: " ", displayedText: " ", isSelected: false)
public var currentRowNumber = 0
public var maximumLinesPerPage = 3
public private(set) var candidateDataAll: [CandidateCellData] = []
public private(set) var selectionKeys: String
public private(set) var highlightedIndex: Int = 0
public private(set) var maxColumnCapacity: Int = 6
public private(set) var candidateDataAll: [CandidateCellData] = []
//
public var currentRowNumber = 0
public var maximumRowsPerPage = 3
public private(set) var maxRowCapacity: Int = 6
public private(set) var candidateRows: [[CandidateCellData]] = []
public var isVerticalLayout: Bool { maxColumnCapacity == 1 }
public var maxColumnWidth: Int { Int(Double(maxColumnCapacity + 3) * 2) * Int(ceil(CandidateCellData.unifiedSize)) }
//
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(maxColumnCapacity + 3) * 2.7 * ceil(CandidateCellData.unifiedSize) * 1.2)
ceil(Double(maxRowCapacity + 3) * 2.7 * ceil(CandidateCellData.unifiedSize) * 1.2)
}
public var rangeForCurrentPage: Range<Int> {
currentRowNumber..<min(candidateRows.count, currentRowNumber + maximumLinesPerPage)
public var rangeForCurrentHorizontalPage: Range<Int> {
currentRowNumber..<min(candidateRows.count, currentRowNumber + maximumRowsPerPage)
}
public var rangeForLastPageBlanked: Range<Int> { 0..<(maximumLinesPerPage - rangeForCurrentPage.count) }
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: (, )
public init(candidates: [String], columnCapacity: Int = 6, selectionKeys: String = "123456789", locale: String = "") {
/// - 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.whichRow = candidateRows.count
let isOverflown: Bool = currentColumn.map(\.cellLength).reduce(0, +) + candidate.cellLength > maxColumnWidth
if isOverflown || currentColumn.count == maxColumnCapacity, !currentColumn.isEmpty {
candidateRows.append(currentColumn)
candidate.whichColumn = candidateColumns.count
if currentColumn.count == maxColumnCapacity, !currentColumn.isEmpty {
candidateColumns.append(currentColumn)
currentColumn.removeAll()
candidate.whichRow += 1
candidate.whichColumn += 1
}
candidate.subIndex = currentColumn.count
candidate.locale = locale
currentColumn.append(candidate)
}
candidateRows.append(currentColumn)
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) {
@ -70,7 +122,7 @@ public class CandidatePool {
if candidateRows.isEmpty { break }
let firstRow = candidateRows[0]
let newSubIndex = min(currentSubIndex, firstRow.count - 1)
highlight(at: firstRow[newSubIndex].index)
highlightHorizontal(at: firstRow[newSubIndex].index)
break
}
if currentRowNumber >= candidateRows.count - 1 { currentRowNumber = candidateRows.count - 1 }
@ -80,13 +132,13 @@ public class CandidatePool {
}
let targetRow = candidateRows[currentRowNumber - 1]
let newSubIndex = min(result, targetRow.count - 1)
highlight(at: targetRow[newSubIndex].index)
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)
highlight(at: finalRow[newSubIndex].index)
highlightHorizontal(at: finalRow[newSubIndex].index)
break
}
if candidateRows[currentRowNumber].count != candidateRows[currentRowNumber + 1].count {
@ -95,11 +147,40 @@ public class CandidatePool {
}
let targetRow = candidateRows[currentRowNumber + 1]
let newSubIndex = min(result, targetRow.count - 1)
highlight(at: targetRow[newSubIndex].index)
highlightHorizontal(at: targetRow[newSubIndex].index)
}
}
public func highlight(at indexSpecified: Int) {
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) {
@ -120,13 +201,48 @@ public class CandidatePool {
candidate.isSelected = (indexSpecified == i)
if candidate.isSelected { currentRowNumber = candidate.whichRow }
}
for (i, candidateColumn) in candidateRows.enumerated() {
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]
}
}

View File

@ -120,6 +120,14 @@ open class CtlCandidate: NSWindowController, CtlCandidateProtocol {
open var tooltip: String = ""
@discardableResult open func showNextLine() -> Bool {
false
}
@discardableResult open func showPreviousLine() -> Bool {
false
}
@discardableResult open func highlightNextCandidate() -> Bool {
false
}

View File

@ -1,122 +0,0 @@
// (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 CocoaExtension
import Shared
import SwiftUI
@available(macOS 12, *)
public class CtlCandidateTDK: CtlCandidate {
public var thePool: CandidatePool = .init(candidates: [])
public var theView: VwrCandidateTDK { .init(controller: self, thePool: thePool, hint: hint) }
public required init(_ layout: NSUserInterfaceLayoutOrientation = .horizontal) {
var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0)
let styleMask: NSWindow.StyleMask = [.nonactivatingPanel]
let panel = NSPanel(
contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false
)
panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 2)
panel.hasShadow = true
panel.isOpaque = false
panel.backgroundColor = NSColor.clear
contentRect.origin = NSPoint.zero
super.init(layout)
window = panel
currentLayout = layout
reloadData()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func reloadData() {
CandidateCellData.highlightBackground = highlightedColor()
CandidateCellData.unifiedSize = candidateFont.pointSize
guard let delegate = delegate else { return }
thePool = .init(
candidates: delegate.candidatePairs(conv: true).map(\.1),
selectionKeys: delegate.selectionKeys, locale: locale
)
thePool.highlight(at: 0)
updateDisplay()
}
override open func updateDisplay() {
DispatchQueue.main.async { [self] in
let newView = NSHostingView(rootView: theView.fixedSize())
let newSize = newView.fittingSize
var newFrame = NSRect.zero
if let window = window { newFrame = window.frame }
newFrame.size = newSize
window?.setFrame(newFrame, display: false)
window?.contentView = NSHostingView(rootView: theView.fixedSize())
window?.setContentSize(newSize)
}
}
@discardableResult override public func showNextPage() -> Bool {
for _ in 0..<thePool.maximumLinesPerPage {
thePool.selectNewNeighborRow(direction: .down)
}
updateDisplay()
return true
}
@discardableResult override public func showPreviousPage() -> Bool {
for _ in 0..<thePool.maximumLinesPerPage {
thePool.selectNewNeighborRow(direction: .up)
}
updateDisplay()
return true
}
@discardableResult public func showNextLine() -> Bool {
thePool.selectNewNeighborRow(direction: .down)
updateDisplay()
return true
}
@discardableResult public func showPreviousLine() -> Bool {
thePool.selectNewNeighborRow(direction: .up)
updateDisplay()
return true
}
@discardableResult override public func highlightNextCandidate() -> Bool {
thePool.highlight(at: thePool.highlightedIndex + 1)
updateDisplay()
return true
}
@discardableResult override public func highlightPreviousCandidate() -> Bool {
thePool.highlight(at: thePool.highlightedIndex - 1)
updateDisplay()
return true
}
override public func candidateIndexAtKeyLabelIndex(_ id: Int) -> Int {
let currentRow = thePool.candidateRows[thePool.currentRowNumber]
let actualID = max(0, min(id, currentRow.count - 1))
return thePool.candidateRows[thePool.currentRowNumber][actualID].index
}
override public var selectedCandidateIndex: Int {
get {
thePool.highlightedIndex
}
set {
thePool.highlight(at: newValue)
updateDisplay()
}
}
}

View File

@ -0,0 +1,209 @@
// (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 CocoaExtension
import Shared
import SwiftUI
@available(macOS 12, *)
public class CtlCandidateTDK: CtlCandidate {
public var thePoolHorizontal: CandidatePool = .init(candidates: [], rowCapacity: 6)
public var theViewHorizontal: VwrCandidateHorizontal {
.init(controller: self, thePool: thePoolHorizontal, hint: hint)
}
public var thePoolVertical: CandidatePool = .init(candidates: [], columnCapacity: 6)
public var theViewVertical: VwrCandidateVertical { .init(controller: self, thePool: thePoolVertical, hint: hint) }
public required init(_ layout: NSUserInterfaceLayoutOrientation = .horizontal) {
var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0)
let styleMask: NSWindow.StyleMask = [.nonactivatingPanel]
let panel = NSPanel(
contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false
)
panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 2)
panel.hasShadow = true
panel.isOpaque = false
panel.backgroundColor = NSColor.clear
contentRect.origin = NSPoint.zero
super.init(layout)
window = panel
currentLayout = layout
reloadData()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func reloadData() {
CandidateCellData.highlightBackground = highlightedColor()
CandidateCellData.unifiedSize = candidateFont.pointSize
guard let delegate = delegate else { return }
switch currentLayout {
case .horizontal:
thePoolHorizontal = .init(
candidates: delegate.candidatePairs(conv: true).map(\.1), rowCapacity: 6,
selectionKeys: delegate.selectionKeys, locale: locale
)
thePoolHorizontal.highlightHorizontal(at: 0)
case .vertical:
thePoolVertical = .init(
candidates: delegate.candidatePairs(conv: true).map(\.1), columnCapacity: 6,
selectionKeys: delegate.selectionKeys, locale: locale
)
thePoolVertical.highlightVertical(at: 0)
@unknown default:
return
}
updateDisplay()
}
override open func updateDisplay() {
switch currentLayout {
case .horizontal:
DispatchQueue.main.async { [self] in
let newView = NSHostingView(rootView: theViewHorizontal)
let newSize = newView.fittingSize
window?.contentView = newView
window?.setContentSize(newSize)
}
case .vertical:
DispatchQueue.main.async { [self] in
let newView = NSHostingView(rootView: theViewVertical)
let newSize = newView.fittingSize
window?.contentView = newView
window?.setContentSize(newSize)
}
@unknown default:
return
}
}
@discardableResult override public func showNextPage() -> Bool {
switch currentLayout {
case .horizontal:
for _ in 0..<thePoolHorizontal.maximumRowsPerPage {
thePoolHorizontal.selectNewNeighborRow(direction: .down)
}
case .vertical:
for _ in 0..<thePoolVertical.maximumColumnsPerPage {
thePoolVertical.selectNewNeighborColumn(direction: .right)
}
@unknown default:
return false
}
updateDisplay()
return true
}
@discardableResult override public func showPreviousPage() -> Bool {
switch currentLayout {
case .horizontal:
for _ in 0..<thePoolHorizontal.maximumRowsPerPage {
thePoolHorizontal.selectNewNeighborRow(direction: .up)
}
case .vertical:
for _ in 0..<thePoolVertical.maximumColumnsPerPage {
thePoolVertical.selectNewNeighborColumn(direction: .left)
}
@unknown default:
return false
}
updateDisplay()
return true
}
@discardableResult override public func showNextLine() -> Bool {
switch currentLayout {
case .horizontal:
thePoolHorizontal.selectNewNeighborRow(direction: .down)
case .vertical:
thePoolVertical.selectNewNeighborColumn(direction: .right)
@unknown default:
return false
}
updateDisplay()
return true
}
@discardableResult override public func showPreviousLine() -> Bool {
switch currentLayout {
case .horizontal:
thePoolHorizontal.selectNewNeighborRow(direction: .up)
case .vertical:
thePoolVertical.selectNewNeighborColumn(direction: .left)
@unknown default:
return false
}
updateDisplay()
return true
}
@discardableResult override public func highlightNextCandidate() -> Bool {
switch currentLayout {
case .horizontal:
thePoolHorizontal.highlightHorizontal(at: thePoolHorizontal.highlightedIndex + 1)
case .vertical:
thePoolVertical.highlightVertical(at: thePoolVertical.highlightedIndex + 1)
@unknown default:
return false
}
updateDisplay()
return true
}
@discardableResult override public func highlightPreviousCandidate() -> Bool {
switch currentLayout {
case .horizontal:
thePoolHorizontal.highlightHorizontal(at: thePoolHorizontal.highlightedIndex - 1)
case .vertical:
thePoolVertical.highlightVertical(at: thePoolVertical.highlightedIndex - 1)
@unknown default:
return false
}
updateDisplay()
return true
}
override public func candidateIndexAtKeyLabelIndex(_ id: Int) -> Int {
switch currentLayout {
case .horizontal:
let currentRow = thePoolHorizontal.candidateRows[thePoolHorizontal.currentRowNumber]
let actualID = max(0, min(id, currentRow.count - 1))
return thePoolHorizontal.candidateRows[thePoolHorizontal.currentRowNumber][actualID].index
case .vertical:
let currentColumn = thePoolVertical.candidateColumns[thePoolVertical.currentColumnNumber]
let actualID = max(0, min(id, currentColumn.count - 1))
return thePoolVertical.candidateColumns[thePoolVertical.currentColumnNumber][actualID].index
@unknown default:
return 0
}
}
override public var selectedCandidateIndex: Int {
get {
switch currentLayout {
case .horizontal: return thePoolHorizontal.highlightedIndex
case .vertical: return thePoolVertical.highlightedIndex
@unknown default: return 0
}
}
set {
switch currentLayout {
case .horizontal: thePoolHorizontal.highlightHorizontal(at: newValue)
case .vertical: thePoolVertical.highlightVertical(at: newValue)
@unknown default: return
}
updateDisplay()
}
}
}

View File

@ -13,27 +13,28 @@ import SwiftUI
// MARK: - Some useless tests
@available(macOS 12, *)
struct CandidatePoolViewUI_Previews: PreviewProvider {
struct CandidatePoolViewUIHorizontal_Previews: PreviewProvider {
@State static var testCandidates: [String] = [
"八月中秋山林涼", "八月中秋", "風吹大地", "山林涼", "草枝擺", "八月", "中秋",
"🐂🍺🐂🍺", "🐃🍺", "🐂🍺", "🐃🐂🍺🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺",
"山林", "風吹", "大地", "草枝", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "",
]
static var thePool: CandidatePool {
let result = CandidatePool(candidates: testCandidates, columnCapacity: 6)
let result = CandidatePool(candidates: testCandidates, rowCapacity: 6)
// 使
result.highlight(at: 14)
result.highlightHorizontal(at: 5)
return result
}
static var previews: some View {
VwrCandidateTDK(controller: .init(.horizontal), thePool: thePool).fixedSize()
VwrCandidateHorizontal(controller: .init(.horizontal), thePool: thePool).fixedSize()
}
}
@available(macOS 12, *)
public struct VwrCandidateTDK: View {
public struct VwrCandidateHorizontal: View {
public var controller: CtlCandidateTDK
@State public var thePool: CandidatePool
@State public var hint: String = ""
@ -52,26 +53,28 @@ public struct VwrCandidateTDK: View {
VStack(alignment: .leading, spacing: 0) {
ScrollView(.vertical, showsIndicators: true) {
VStack(alignment: .leading, spacing: 1.6) {
ForEach(thePool.rangeForCurrentPage, id: \.self) { columnIndex in
ForEach(thePool.rangeForCurrentHorizontalPage, id: \.self) { rowIndex in
HStack(spacing: 10) {
ForEach(Array(thePool.candidateRows[columnIndex]), id: \.self) { currentCandidate in
ForEach(Array(thePool.candidateRows[rowIndex]), id: \.self) { currentCandidate in
currentCandidate.attributedStringForSwiftUI.fixedSize()
.frame(maxWidth: .infinity, alignment: .topLeading)
.frame(
maxWidth: .infinity,
alignment: .topLeading
)
.contentShape(Rectangle())
.onTapGesture { didSelectCandidateAt(currentCandidate.index) }
}
Spacer()
}.frame(
minWidth: 0,
maxWidth: .infinity,
alignment: .topLeading
).id(columnIndex)
).id(rowIndex)
Divider()
}
if thePool.maximumLinesPerPage - thePool.rangeForCurrentPage.count > 0 {
ForEach(thePool.rangeForLastPageBlanked, id: \.self) { _ in
if thePool.maximumRowsPerPage - thePool.rangeForCurrentHorizontalPage.count > 0 {
ForEach(thePool.rangeForLastHorizontalPageBlanked, id: \.self) { _ in
HStack(spacing: 0) {
thePool.blankCell.attributedStringForSwiftUI.fixedSize()
thePool.blankCell.attributedStringForSwiftUI
.frame(maxWidth: .infinity, alignment: .topLeading)
.contentShape(Rectangle())
Spacer()
@ -87,41 +90,20 @@ public struct VwrCandidateTDK: View {
}
.fixedSize(horizontal: false, vertical: true).padding(5)
.background(Color(nsColor: NSColor.controlBackgroundColor).ignoresSafeArea())
HStack(alignment: .bottom) {
Text(hint).font(.system(size: max(CandidateCellData.unifiedSize * 0.7, 11), weight: .bold)).lineLimit(1)
Spacer()
Text(positionLabel).font(.system(size: max(CandidateCellData.unifiedSize * 0.7, 11), weight: .bold)).lineLimit(
1)
}.padding(6).foregroundColor(.init(nsColor: .controlTextColor))
.shadow(color: .init(nsColor: .textBackgroundColor), radius: 1)
ZStack(alignment: .leading) {
Color(nsColor: hint.isEmpty ? .windowBackgroundColor : CandidateCellData.highlightBackground).ignoresSafeArea()
HStack(alignment: .bottom) {
Text(hint).font(.system(size: max(CandidateCellData.unifiedSize * 0.7, 11), weight: .bold)).lineLimit(1)
Spacer()
Text(positionLabel).font(.system(size: max(CandidateCellData.unifiedSize * 0.7, 11), weight: .bold))
.lineLimit(
1)
}
.padding(6).foregroundColor(
.init(nsColor: hint.isEmpty ? .controlTextColor : .selectedMenuItemTextColor.withAlphaComponent(0.9))
)
}
}
.frame(minWidth: thePool.maxWindowWidth, maxWidth: thePool.maxWindowWidth)
}
}
@available(macOS 12, *)
extension CandidateCellData {
public var attributedStringForSwiftUI: some View {
var result: some View {
ZStack(alignment: .leading) {
if isSelected {
Color(nsColor: CandidateCellData.highlightBackground).ignoresSafeArea().cornerRadius(6)
}
VStack(spacing: 0) {
HStack(spacing: 4) {
if UserDefaults.standard.bool(forKey: UserDef.kHandleDefaultCandidateFontsByLangIdentifier.rawValue) {
Text(AttributedString(attributedStringHeader)).frame(width: CandidateCellData.unifiedSize / 2)
Text(AttributedString(attributedString))
} else {
Text(key).font(.system(size: fontSizeKey).monospaced())
.foregroundColor(.init(nsColor: fontColorKey)).lineLimit(1)
Text(displayedText).font(.system(size: fontSizeCandidate))
.foregroundColor(.init(nsColor: fontColorCandidate)).lineLimit(1)
}
}.padding(4)
}
}.fixedSize(horizontal: false, vertical: true)
}
return result
}
}

View File

@ -0,0 +1,109 @@
// (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
import SwiftUI
// MARK: - Some useless tests
@available(macOS 12, *)
struct CandidatePoolViewUIVertical_Previews: PreviewProvider {
@State static var testCandidates: [String] = [
"八月中秋山林涼", "八月中秋", "風吹大地", "山林涼", "草枝擺", "🐂🍺", "🐃🍺", "八月", "中秋",
"山林", "風吹", "大地", "草枝", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "",
]
static var thePool: CandidatePool {
let result = CandidatePool(candidates: testCandidates, columnCapacity: 6, selectionKeys: "123456789")
// 使
result.highlightVertical(at: 5)
return result
}
static var previews: some View {
VwrCandidateVertical(controller: .init(.horizontal), thePool: thePool).fixedSize()
}
}
@available(macOS 12, *)
public struct VwrCandidateVertical: View {
public var controller: CtlCandidateTDK
@State public var thePool: CandidatePool
@State public var hint: String = ""
private var positionLabel: String {
(thePool.highlightedIndex + 1).description + "/" + thePool.candidateDataAll.count.description
}
private func didSelectCandidateAt(_ pos: Int) {
if let delegate = controller.delegate {
delegate.candidatePairSelected(at: pos)
}
}
public var body: some View {
VStack(alignment: .leading, spacing: 0) {
ScrollView(.horizontal, showsIndicators: true) {
HStack(alignment: .top, spacing: 10) {
ForEach(thePool.rangeForCurrentVerticalPage, id: \.self) { columnIndex in
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(thePool.candidateColumns[columnIndex]), id: \.self) { currentCandidate in
HStack(spacing: 0) {
currentCandidate.attributedStringForSwiftUI.fixedSize(horizontal: false, vertical: true)
.frame(
maxWidth: .infinity,
alignment: .topLeading
)
.contentShape(Rectangle())
.onTapGesture { didSelectCandidateAt(currentCandidate.index) }
}
}
}.frame(
minWidth: Double(CandidateCellData.unifiedSize * 5),
alignment: .topLeading
).id(columnIndex)
Divider()
}
if thePool.maximumColumnsPerPage - thePool.rangeForCurrentVerticalPage.count > 0 {
ForEach(thePool.rangeForLastVerticalPageBlanked, id: \.self) { _ in
VStack(alignment: .leading, spacing: 0) {
ForEach(0..<thePool.maxColumnCapacity, id: \.self) { _ in
thePool.blankCell.attributedStringForSwiftUI.fixedSize()
.frame(width: Double(CandidateCellData.unifiedSize * 5), alignment: .topLeading)
.contentShape(Rectangle())
}
}.frame(
minWidth: 0,
maxWidth: .infinity,
alignment: .topLeading
)
Divider()
}
}
}
}
.fixedSize(horizontal: true, vertical: false).padding(5)
.background(Color(nsColor: NSColor.controlBackgroundColor).ignoresSafeArea())
ZStack(alignment: .leading) {
Color(nsColor: hint.isEmpty ? .windowBackgroundColor : CandidateCellData.highlightBackground).ignoresSafeArea()
HStack(alignment: .bottom) {
Text(hint).font(.system(size: max(CandidateCellData.unifiedSize * 0.7, 11), weight: .bold)).lineLimit(1)
Spacer()
Text(positionLabel).font(.system(size: max(CandidateCellData.unifiedSize * 0.7, 11), weight: .bold))
.lineLimit(
1)
}
.padding(6).foregroundColor(
.init(nsColor: hint.isEmpty ? .controlTextColor : .selectedMenuItemTextColor.withAlphaComponent(0.9))
)
}
}
}
}

View File

@ -19,7 +19,7 @@ final class CandidatePoolTests: XCTestCase {
]
func testPoolHorizontal() throws {
let pool = CandidatePool(candidates: testCandidates, columnCapacity: 8)
let pool = CandidatePool(candidates: testCandidates, rowCapacity: 6)
var strOutput = ""
pool.candidateRows.forEach {
$0.forEach {
@ -32,9 +32,9 @@ final class CandidatePoolTests: XCTestCase {
}
func testPoolVertical() throws {
let pool = CandidatePool(candidates: testCandidates, columnCapacity: 8)
let pool = CandidatePool(candidates: testCandidates, columnCapacity: 6)
var strOutput = ""
pool.candidateRows.forEach {
pool.candidateColumns.forEach {
$0.forEach {
strOutput += $0.displayedText + ", "
}

View File

@ -25,7 +25,8 @@ public class CandidateCellData: Hashable {
public var displayedText: String
public var size: Double { Self.unifiedSize }
public var isSelected: Bool = false
public var whichRow: Int = 0
public var whichRow: Int = 0 //
public var whichColumn: Int = 0 //
public var index: Int = 0
public var subIndex: Int = 0
@ -48,7 +49,7 @@ public class CandidateCellData: Hashable {
let rect = attributedStringForLengthCalculation.boundingRect(
with: NSSize(width: 1600.0, height: 1600.0), options: [.usesLineFragmentOrigin]
)
let rawResult = ceil(rect.width + size / size)
let rawResult = ceil(rect.width)
return Int(rawResult)
}
@ -73,8 +74,16 @@ public class CandidateCellData: Hashable {
}
public var attributedStringForLengthCalculation: NSAttributedString {
let paraStyleKey = NSMutableParagraphStyle()
paraStyleKey.setParagraphStyle(NSParagraphStyle.default)
paraStyleKey.alignment = .natural
let paraStyle = NSMutableParagraphStyle()
paraStyle.setParagraphStyle(NSParagraphStyle.default)
paraStyle.alignment = .natural
paraStyle.lineBreakMode = .byWordWrapping
let attrCandidate: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: size, weight: .regular)
.font: NSFont.monospacedDigitSystemFont(ofSize: size, weight: .regular),
.paragraphStyle: paraStyle,
]
let attrStrCandidate = NSMutableAttributedString(string: displayedText + " ", attributes: attrCandidate)
return attrStrCandidate
@ -87,6 +96,7 @@ public class CandidateCellData: Hashable {
let paraStyle = NSMutableParagraphStyle()
paraStyle.setParagraphStyle(NSParagraphStyle.default)
paraStyle.alignment = .natural
paraStyle.lineBreakMode = .byWordWrapping
var attrCandidate: [NSAttributedString.Key: AnyObject] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: size, weight: .regular),
.paragraphStyle: paraStyle,

View File

@ -36,6 +36,8 @@ public protocol CtlCandidateProtocol {
func updateDisplay()
func showNextPage() -> Bool
func showPreviousPage() -> Bool
func showNextLine() -> Bool
func showPreviousLine() -> Bool
func highlightNextCandidate() -> Bool
func highlightPreviousCandidate() -> Bool
func candidateIndexAtKeyLabelIndex(_: Int) -> Int

View File

@ -131,7 +131,7 @@ extension KeyHandler {
errorCallback("1145148D")
}
case .vertical:
if !ctlCandidate.showPreviousPage() {
if !ctlCandidate.showPreviousLine() {
errorCallback("1919810D")
}
@unknown default:
@ -149,7 +149,7 @@ extension KeyHandler {
errorCallback("9B65138D")
}
case .vertical:
if !ctlCandidate.showNextPage() {
if !ctlCandidate.showNextLine() {
errorCallback("9244908D")
}
@unknown default:
@ -163,21 +163,8 @@ extension KeyHandler {
if input.isUp {
switch ctlCandidate.currentLayout {
case .horizontal:
if #available(macOS 12, *) {
if let ctlCandidate = ctlCandidate as? CtlCandidateTDK {
ctlCandidate.showPreviousLine()
break
} else {
if !ctlCandidate.showPreviousPage() {
errorCallback("9B614524")
break
}
}
} else {
if !ctlCandidate.showPreviousPage() {
errorCallback("9B614524")
break
}
if !ctlCandidate.showPreviousLine() {
errorCallback("9B614524")
}
case .vertical:
if !ctlCandidate.highlightPreviousCandidate() {
@ -194,21 +181,9 @@ extension KeyHandler {
if input.isDown {
switch ctlCandidate.currentLayout {
case .horizontal:
if #available(macOS 12, *) {
if let ctlCandidate = ctlCandidate as? CtlCandidateTDK {
ctlCandidate.showNextLine()
break
} else {
if !ctlCandidate.showNextPage() {
errorCallback("92B990DD")
break
}
}
} else {
if !ctlCandidate.showNextPage() {
errorCallback("92B990DD")
break
}
if !ctlCandidate.showNextLine() {
errorCallback("92B990DD")
break
}
case .vertical:
if !ctlCandidate.highlightNextCandidate() {
@ -322,15 +297,7 @@ extension KeyHandler {
if input.isSymbolMenuPhysicalKey {
var updated = true
if #available(macOS 12, *) {
if let ctlCandidate = ctlCandidate as? CtlCandidateTDK {
updated = input.isShiftHold ? ctlCandidate.showPreviousLine() : ctlCandidate.showNextLine()
} else {
updated = input.isShiftHold ? ctlCandidate.showPreviousPage() : ctlCandidate.showNextPage()
}
} else {
updated = input.isShiftHold ? ctlCandidate.showPreviousPage() : ctlCandidate.showNextPage()
}
updated = input.isShiftHold ? ctlCandidate.showPreviousLine() : ctlCandidate.showNextLine()
if !updated {
errorCallback("66F3477B")
}

View File

@ -148,6 +148,18 @@ public class CtlCandidateIMK: IMKCandidates, CtlCandidateProtocol {
return true
}
// IMK
public func showNextLine() -> Bool {
do { currentLayout == .vertical ? moveRight(self) : moveDown(self) }
return true
}
// IMK
public func showPreviousLine() -> Bool {
do { currentLayout == .vertical ? moveLeft(self) : moveUp(self) }
return true
}
public func candidateIndexAtKeyLabelIndex(_ index: Int) -> Int {
guard let delegate = delegate else { return Int.max }
let result = currentPageIndex * keyLabels.count + index

View File

@ -127,18 +127,8 @@ struct suiPrefPaneGeneral: View {
.labelsHidden()
.horizontalRadioGroupLayout()
.pickerStyle(RadioGroupPickerStyle())
.disabled(!PrefMgr.shared.useIMKCandidateWindow)
if PrefMgr.shared.useIMKCandidateWindow {
Text(LocalizedStringKey("Choose your preferred layout of the candidate window."))
.preferenceDescription()
} else {
Text(
LocalizedStringKey(
"Tadokoro candidate window only supports horizontal grid view. Enable IMK candidate window in DevZone page first if you want to choose vertical candidate window."
)
)
Text(LocalizedStringKey("Choose your preferred layout of the candidate window."))
.preferenceDescription()
}
}
Preferences.Section(label: { Text(LocalizedStringKey("Output Settings:")) }) {
Toggle(

View File

@ -216,7 +216,6 @@
"Specify the behavior of intonation key when syllable composer is empty." = "Specify the behavior of intonation key when syllable composer is empty.";
"Starlight" = "Starlight";
"Stop farting (when typed phonetic combination is invalid, etc.)" = "Stop farting (when typed phonetic combination is invalid, etc.)";
"Tadokoro candidate window only supports horizontal grid view. Enable IMK candidate window in DevZone page first if you want to choose vertical candidate window." = "Tadokoro candidate window only supports horizontal grid view. Enable IMK candidate window in DevZone page first if you want to choose vertical candidate window.";
"This only works with Tadokoro candidate window." = "This only works with Tadokoro candidate window.";
"Traditional Chinese" = "Traditional Chinese";
"Trim unfinished readings on commit" = "Trim unfinished readings on commit";

View File

@ -216,7 +216,6 @@
"Specify the behavior of intonation key when syllable composer is empty." = "Specify the behavior of intonation key when syllable composer is empty.";
"Starlight" = "Starlight";
"Stop farting (when typed phonetic combination is invalid, etc.)" = "Stop farting (when typed phonetic combination is invalid, etc.)";
"Tadokoro candidate window only supports horizontal grid view. Enable IMK candidate window in DevZone page first if you want to choose vertical candidate window." = "Tadokoro candidate window only supports horizontal grid view. Enable IMK candidate window in DevZone page first if you want to choose vertical candidate window.";
"This only works with Tadokoro candidate window." = "This only works with Tadokoro candidate window.";
"Traditional Chinese" = "Traditional Chinese";
"Trim unfinished readings on commit" = "Trim unfinished readings on commit";

View File

@ -81,7 +81,7 @@
"Optimize Memorized Phrases" = "臨時記憶資料を整う";
"Clear Memorized Phrases" = "臨時記憶資料を削除";
"Currency Numeral Output" = "数字大字変換";
"Hold ⇧ to choose associates." = "⇧を押しながら連想候補を選択ください。";
"Hold ⇧ to choose associates." = "⇧を押しながら連想候補を選択。";
// The followings are the category names used in the Symbol menu.
"catCommonSymbols" = "常用";
@ -216,7 +216,6 @@
"Specify the behavior of intonation key when syllable composer is empty." = "音読組立緩衝列が空かされた時の音調キーの行為をご指定ください。";
"Starlight" = "星光配列";
"Stop farting (when typed phonetic combination is invalid, etc.)" = "マナーモード // 外すと入力間違った時に変な声が出る";
"Tadokoro candidate window only supports horizontal grid view. Enable IMK candidate window in DevZone page first if you want to choose vertical candidate window." = "田所候補陳列ウィンドウは格子状横型陳列しかできません。開発道場のページで IMK 候補陳列ウィンドウを起用してから、今のこのページで縦型・横型陳列の選択はできます。";
"This only works with Tadokoro candidate window." = "これは田所候補陳列ウィンドウだけに効ける機能である。";
"Traditional Chinese" = "繁体中国語";
"Trim unfinished readings on commit" = "送り出す緩衝列内容から未完成な音読みを除く";

View File

@ -216,7 +216,6 @@
"Specify the behavior of intonation key when syllable composer is empty." = "指定声调键(在注拼槽为「空」状态时)的行为。";
"Starlight" = "星光排列";
"Stop farting (when typed phonetic combination is invalid, etc.)" = "廉耻模式 // 取消勾选的话,敲错字时会有异音";
"Tadokoro candidate window only supports horizontal grid view. Enable IMK candidate window in DevZone page first if you want to choose vertical candidate window." = "田所选字窗仅支援横排矩阵布局模式。如欲使用纵排布局模式者,请在开发道场内先启用 IMK 选字窗。";
"This only works with Tadokoro candidate window." = "该方法仅对田所选字窗起作用。";
"Traditional Chinese" = "繁体中文";
"Trim unfinished readings on commit" = "在递交时清理未完成拼写的读音";

View File

@ -216,7 +216,6 @@
"Specify the behavior of intonation key when syllable composer is empty." = "指定聲調鍵(在注拼槽為「空」狀態時)的行為。";
"Starlight" = "星光排列";
"Stop farting (when typed phonetic combination is invalid, etc.)" = "廉恥模式 // 取消勾選的話,敲錯字時會有異音";
"Tadokoro candidate window only supports horizontal grid view. Enable IMK candidate window in DevZone page first if you want to choose vertical candidate window." = "田所選字窗僅支援橫排矩陣佈局模式。如欲使用縱排佈局模式者,請在開發道場內先啟用 IMK 選字窗。";
"This only works with Tadokoro candidate window." = "該方法僅對田所選字窗起作用。";
"Traditional Chinese" = "繁體中文";
"Trim unfinished readings on commit" = "在遞交時清理未完成拼寫的讀音";