CtlCandidateTDK // Vertical candidate layout support, etc.
- SP2: Fix a color scheme mistake in bright mode.
This commit is contained in:
parent
8fd236a065
commit
030a8cb776
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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-Hans」或「zh-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-Hans」或「zh-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]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 + ", "
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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" = "送り出す緩衝列内容から未完成な音読みを除く";
|
||||
|
|
|
@ -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" = "在递交时清理未完成拼写的读音";
|
||||
|
|
|
@ -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" = "在遞交時清理未完成拼寫的讀音";
|
||||
|
|
Loading…
Reference in New Issue