Repo // Introducing CtlCandidateTDK (horizontal).
This commit is contained in:
parent
72b1099e63
commit
eadae22dbb
|
@ -301,17 +301,18 @@ public class CtlCandidateUniversal: CtlCandidate {
|
||||||
private var nextPageButton: NSButton
|
private var nextPageButton: NSButton
|
||||||
private var pageCounterLabel: NSTextField
|
private var pageCounterLabel: NSTextField
|
||||||
private var currentPageIndex: Int = 0
|
private var currentPageIndex: Int = 0
|
||||||
override public var currentLayout: CandidateLayout {
|
override public var currentLayout: NSUserInterfaceLayoutOrientation {
|
||||||
get { candidateView.isVerticalLayout ? .vertical : .horizontal }
|
get { candidateView.isVerticalLayout ? .vertical : .horizontal }
|
||||||
set {
|
set {
|
||||||
switch newValue {
|
switch newValue {
|
||||||
case .vertical: candidateView.isVerticalLayout = true
|
case .vertical: candidateView.isVerticalLayout = true
|
||||||
case .horizontal: candidateView.isVerticalLayout = false
|
case .horizontal: candidateView.isVerticalLayout = false
|
||||||
|
@unknown default: candidateView.isVerticalLayout = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public required init(_ layout: CandidateLayout = .horizontal) {
|
public required init(_ layout: NSUserInterfaceLayoutOrientation = .horizontal) {
|
||||||
var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0)
|
var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0)
|
||||||
let styleMask: NSWindow.StyleMask = [.nonactivatingPanel]
|
let styleMask: NSWindow.StyleMask = [.nonactivatingPanel]
|
||||||
let panel = NSPanel(
|
let panel = NSPanel(
|
||||||
|
@ -433,7 +434,7 @@ public class CtlCandidateUniversal: CtlCandidate {
|
||||||
@discardableResult override public func highlightNextCandidate() -> Bool {
|
@discardableResult override public func highlightNextCandidate() -> Bool {
|
||||||
guard let delegate = delegate else { return false }
|
guard let delegate = delegate else { return false }
|
||||||
selectedCandidateIndex =
|
selectedCandidateIndex =
|
||||||
(selectedCandidateIndex + 1 >= delegate.candidatePairs().count)
|
(selectedCandidateIndex + 1 >= delegate.candidatePairs(conv: false).count)
|
||||||
? 0 : selectedCandidateIndex + 1
|
? 0 : selectedCandidateIndex + 1
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -442,7 +443,7 @@ public class CtlCandidateUniversal: CtlCandidate {
|
||||||
guard let delegate = delegate else { return false }
|
guard let delegate = delegate else { return false }
|
||||||
selectedCandidateIndex =
|
selectedCandidateIndex =
|
||||||
(selectedCandidateIndex == 0)
|
(selectedCandidateIndex == 0)
|
||||||
? delegate.candidatePairs().count - 1 : selectedCandidateIndex - 1
|
? delegate.candidatePairs(conv: false).count - 1 : selectedCandidateIndex - 1
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -452,7 +453,7 @@ public class CtlCandidateUniversal: CtlCandidate {
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = currentPageIndex * keyLabels.count + index
|
let result = currentPageIndex * keyLabels.count + index
|
||||||
return result < delegate.candidatePairs().count ? result : Int.max
|
return result < delegate.candidatePairs(conv: false).count ? result : Int.max
|
||||||
}
|
}
|
||||||
|
|
||||||
override public var selectedCandidateIndex: Int {
|
override public var selectedCandidateIndex: Int {
|
||||||
|
@ -464,7 +465,7 @@ public class CtlCandidateUniversal: CtlCandidate {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let keyLabelCount = keyLabels.count
|
let keyLabelCount = keyLabels.count
|
||||||
if newValue < delegate.candidatePairs().count {
|
if newValue < delegate.candidatePairs(conv: false).count {
|
||||||
currentPageIndex = newValue / keyLabelCount
|
currentPageIndex = newValue / keyLabelCount
|
||||||
candidateView.highlightedIndex = newValue % keyLabelCount
|
candidateView.highlightedIndex = newValue % keyLabelCount
|
||||||
layoutCandidateView()
|
layoutCandidateView()
|
||||||
|
@ -478,7 +479,7 @@ extension CtlCandidateUniversal {
|
||||||
guard let delegate = delegate else {
|
guard let delegate = delegate else {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
let totalCount = delegate.candidatePairs().count
|
let totalCount = delegate.candidatePairs(conv: false).count
|
||||||
let keyLabelCount = keyLabels.count
|
let keyLabelCount = keyLabels.count
|
||||||
return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0)
|
return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0)
|
||||||
}
|
}
|
||||||
|
@ -487,7 +488,7 @@ extension CtlCandidateUniversal {
|
||||||
guard let delegate = delegate else {
|
guard let delegate = delegate else {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
let totalCount = delegate.candidatePairs().count
|
let totalCount = delegate.candidatePairs(conv: false).count
|
||||||
let keyLabelCount = keyLabels.count
|
let keyLabelCount = keyLabels.count
|
||||||
return totalCount % keyLabelCount
|
return totalCount % keyLabelCount
|
||||||
}
|
}
|
||||||
|
@ -497,7 +498,7 @@ extension CtlCandidateUniversal {
|
||||||
|
|
||||||
candidateView.set(keyLabelFont: keyLabelFont, candidateFont: candidateFont)
|
candidateView.set(keyLabelFont: keyLabelFont, candidateFont: candidateFont)
|
||||||
var candidates = [(String, String)]()
|
var candidates = [(String, String)]()
|
||||||
let count = delegate.candidatePairs().count
|
let count = delegate.candidatePairs(conv: false).count
|
||||||
let keyLabelCount = keyLabels.count
|
let keyLabelCount = keyLabels.count
|
||||||
|
|
||||||
let begin = currentPageIndex * keyLabelCount
|
let begin = currentPageIndex * keyLabelCount
|
||||||
|
|
|
@ -21,6 +21,10 @@ let package = Package(
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "Shared", package: "vChewing_Shared")
|
.product(name: "Shared", package: "vChewing_Shared")
|
||||||
]
|
]
|
||||||
)
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "CandidateWindowTests",
|
||||||
|
dependencies: ["CandidateWindow"]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
# CandidateWindow
|
# CandidateWindow
|
||||||
|
|
||||||
用以定義與威注音的選字窗有關的基礎內容,目前尚未完工。
|
用以定義與威注音的選字窗有關的基礎內容。此外,還包含了威注音自家的次世代選字窗「田所(TDK)」。
|
||||||
|
|
||||||
|
> 命名緣由:野獸先輩「田所」的姓氏。
|
||||||
|
|
||||||
|
TDK 選字窗以純 SwiftUI 構築,用以取代此前自上游繼承來的 Voltaire 選字窗。
|
||||||
|
|
||||||
|
然而,TDK 選字窗目前有下述侷限:
|
||||||
|
|
||||||
|
- 因 SwiftUI 自身特性所導致的嚴重的效能問題。基本上來講,如果您經常使用全字庫模式的話,請在偏好設定內啟用效能更高的 IMK 選字窗。
|
||||||
|
- TDK 選字窗目前僅完成了橫版矩陣陳列模式的實作,且尚未引入對縱排選字窗陳列佈局的支援。
|
||||||
|
|
||||||
|
因為這些問題恐怕需要很久才能全部解決,所以威注音會在這段時間內推薦使用者們優先使用 IMK 選字窗。
|
||||||
|
|
||||||
```
|
```
|
||||||
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
// (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
|
||||||
|
|
||||||
|
/// 候選字窗會用到的資料池單位。用 class 型別會更方便一些。
|
||||||
|
public class CandidatePool {
|
||||||
|
public var currentRowNumber = 0
|
||||||
|
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 private(set) var candidateRows: [[CandidateCellData]] = []
|
||||||
|
public var maxWindowHeight: Double { ceil(maxWindowWidth * 0.4) }
|
||||||
|
public var isVerticalLayout: Bool { maxColumnCapacity == 1 }
|
||||||
|
public var maxColumnWidth: Int { Int(Double(maxColumnCapacity + 3) * 2) * Int(ceil(CandidateCellData.unifiedSize)) }
|
||||||
|
public var maxWindowWidth: Double {
|
||||||
|
ceil(Double(maxColumnCapacity + 3) * 2.7 * ceil(CandidateCellData.unifiedSize) * 1.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum VerticalDirection {
|
||||||
|
case up
|
||||||
|
case down
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 初期化一個候選字池。
|
||||||
|
/// - Parameters:
|
||||||
|
/// - candidates: 要塞入的候選字詞陣列。
|
||||||
|
/// - columnCapacity: (第一行的最大候選字詞數量, 陣列畫面展開之後的每一行的最大候選字詞數量)。
|
||||||
|
public init(candidates: [String], columnCapacity: Int = 6, 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)
|
||||||
|
currentColumn.removeAll()
|
||||||
|
candidate.whichRow += 1
|
||||||
|
}
|
||||||
|
candidate.subIndex = currentColumn.count
|
||||||
|
candidate.locale = locale
|
||||||
|
currentColumn.append(candidate)
|
||||||
|
}
|
||||||
|
candidateRows.append(currentColumn)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
highlight(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)
|
||||||
|
highlight(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)
|
||||||
|
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)
|
||||||
|
highlight(at: targetRow[newSubIndex].index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func highlight(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, candidateColumn) in candidateRows.enumerated() {
|
||||||
|
if i != currentRowNumber {
|
||||||
|
candidateColumn.forEach {
|
||||||
|
$0.key = " "
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (i, neta) in candidateColumn.enumerated() {
|
||||||
|
neta.key = selectionKeys.map { String($0) }[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,8 +10,9 @@ import Cocoa
|
||||||
import Shared
|
import Shared
|
||||||
|
|
||||||
open class CtlCandidate: NSWindowController, CtlCandidateProtocol {
|
open class CtlCandidate: NSWindowController, CtlCandidateProtocol {
|
||||||
|
open var hint: String = ""
|
||||||
open var showPageButtons: Bool = false
|
open var showPageButtons: Bool = false
|
||||||
open var currentLayout: CandidateLayout = .horizontal
|
open var currentLayout: NSUserInterfaceLayoutOrientation = .horizontal
|
||||||
open var locale: String = ""
|
open var locale: String = ""
|
||||||
open var useLangIdentifier: Bool = false
|
open var useLangIdentifier: Bool = false
|
||||||
|
|
||||||
|
@ -67,7 +68,7 @@ open class CtlCandidate: NSWindowController, CtlCandidateProtocol {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public required init(_: CandidateLayout = .horizontal) {
|
public required init(_: NSUserInterfaceLayoutOrientation = .horizontal) {
|
||||||
super.init(window: .init())
|
super.init(window: .init())
|
||||||
visible = false
|
visible = false
|
||||||
}
|
}
|
||||||
|
@ -110,9 +111,9 @@ open class CtlCandidate: NSWindowController, CtlCandidateProtocol {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
open var keyLabels: [CandidateKeyLabel] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
open var keyLabels: [CandidateCellData] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
||||||
.map {
|
.map {
|
||||||
CandidateKeyLabel(key: $0, displayedText: $0)
|
CandidateCellData(key: $0, displayedText: $0)
|
||||||
}
|
}
|
||||||
|
|
||||||
open var candidateFont = NSFont.systemFont(ofSize: 18)
|
open var candidateFont = NSFont.systemFont(ofSize: 18)
|
||||||
|
@ -141,4 +142,6 @@ open class CtlCandidate: NSWindowController, CtlCandidateProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
open func reloadData() {}
|
open func reloadData() {}
|
||||||
|
|
||||||
|
open func updateDisplay() {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
// (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 }
|
||||||
|
let selectionKeys = keyLabels.map(\.key).joined()
|
||||||
|
thePool = .init(
|
||||||
|
candidates: delegate.candidatePairs(conv: true).map(\.1), selectionKeys: selectionKeys, locale: locale
|
||||||
|
)
|
||||||
|
thePool.highlight(at: 0)
|
||||||
|
updateDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
override open func updateDisplay() {
|
||||||
|
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 {
|
||||||
|
thePool.selectNewNeighborRow(direction: .down)
|
||||||
|
updateDisplay()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult override public func showPreviousPage() -> 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,116 @@
|
||||||
|
// (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 CandidatePoolViewUI_Previews: PreviewProvider {
|
||||||
|
@State static var testCandidates: [String] = [
|
||||||
|
"八月中秋山林涼", "八月中秋", "風吹大地", "山林涼", "草枝擺", "八月", "中秋",
|
||||||
|
"山林", "風吹", "大地", "草枝", "八", "月", "中", "秋", "山", "林", "涼", "風",
|
||||||
|
"吹", "大", "地", "草", "枝", "擺", "八", "月", "中", "秋", "山", "林", "涼", "風",
|
||||||
|
"吹", "大", "地", "草", "枝", "擺",
|
||||||
|
]
|
||||||
|
static var thePool: CandidatePool {
|
||||||
|
let result = CandidatePool(candidates: testCandidates, columnCapacity: 6)
|
||||||
|
// 下一行待解決:無論這裡怎麼指定高亮選中項是哪一筆,其所在行都得被卷動到使用者眼前。
|
||||||
|
result.highlight(at: 14)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
VwrCandidateTDK(controller: .init(.horizontal), thePool: thePool).fixedSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(macOS 12, *)
|
||||||
|
public struct VwrCandidateTDK: 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) {
|
||||||
|
ScrollViewReader { proxy in
|
||||||
|
ScrollView(.vertical, showsIndicators: true) {
|
||||||
|
VStack(alignment: .leading, spacing: 1.6) {
|
||||||
|
ForEach(thePool.candidateRows.indices, id: \.self) { columnIndex in
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
ForEach(Array(thePool.candidateRows[columnIndex]), id: \.self) { currentCandidate in
|
||||||
|
currentCandidate.attributedStringForSwiftUI.fixedSize()
|
||||||
|
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture { didSelectCandidateAt(currentCandidate.index) }
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}.frame(
|
||||||
|
minWidth: 0,
|
||||||
|
maxWidth: .infinity,
|
||||||
|
alignment: .topLeading
|
||||||
|
).id(columnIndex)
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onAppear {
|
||||||
|
proxy.scrollTo(thePool.currentRowNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(minHeight: thePool.maxWindowHeight, maxHeight: thePool.maxWindowHeight).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)
|
||||||
|
}
|
||||||
|
.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,46 @@
|
||||||
|
// (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 XCTest
|
||||||
|
|
||||||
|
@testable import CandidateWindow
|
||||||
|
|
||||||
|
final class CandidatePoolTests: XCTestCase {
|
||||||
|
let testCandidates: [String] = [
|
||||||
|
"八月中秋山林涼", "八月中秋", "風吹大地", "山林涼", "草枝擺", "八月", "中秋",
|
||||||
|
"山林", "風吹", "大地", "草枝", "涼", "擺", "涼", "擺", "涼", "擺", "涼", "擺",
|
||||||
|
"涼", "擺", "擺", "涼",
|
||||||
|
]
|
||||||
|
|
||||||
|
func testPoolHorizontal() throws {
|
||||||
|
let pool = CandidatePool(candidates: testCandidates, columnCapacity: 8)
|
||||||
|
var strOutput = ""
|
||||||
|
pool.candidateRows.forEach {
|
||||||
|
$0.forEach {
|
||||||
|
strOutput += $0.displayedText + ", "
|
||||||
|
}
|
||||||
|
strOutput += "\n"
|
||||||
|
}
|
||||||
|
print("The matrix:")
|
||||||
|
print(strOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPoolVertical() throws {
|
||||||
|
let pool = CandidatePool(candidates: testCandidates, columnCapacity: 8)
|
||||||
|
var strOutput = ""
|
||||||
|
pool.candidateRows.forEach {
|
||||||
|
$0.forEach {
|
||||||
|
strOutput += $0.displayedText + ", "
|
||||||
|
}
|
||||||
|
strOutput += "\n"
|
||||||
|
}
|
||||||
|
print("The matrix:")
|
||||||
|
print(strOutput)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
// (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
|
||||||
|
|
||||||
|
// MARK: - Classes used by Candidate Window
|
||||||
|
|
||||||
|
/// 用來管理選字窗內顯示的候選字的單位。用 class 型別會比較方便一些。
|
||||||
|
public class CandidateCellData: Hashable {
|
||||||
|
public var locale = ""
|
||||||
|
public static var unifiedSize: Double = 16
|
||||||
|
public static var highlightBackground: NSColor = {
|
||||||
|
if #available(macOS 10.14, *) {
|
||||||
|
return .selectedContentBackgroundColor
|
||||||
|
}
|
||||||
|
return NSColor.alternateSelectedControlColor
|
||||||
|
}()
|
||||||
|
|
||||||
|
public var key: String
|
||||||
|
public var displayedText: String
|
||||||
|
public var size: Double { Self.unifiedSize }
|
||||||
|
public var isSelected: Bool = false
|
||||||
|
public var whichRow: Int = 0
|
||||||
|
public var index: Int = 0
|
||||||
|
public var subIndex: Int = 0
|
||||||
|
|
||||||
|
public var fontSizeCandidate: Double { CandidateCellData.unifiedSize }
|
||||||
|
public var fontSizeKey: Double { ceil(CandidateCellData.unifiedSize * 0.8) }
|
||||||
|
public var fontColorKey: NSColor {
|
||||||
|
isSelected ? .selectedMenuItemTextColor.withAlphaComponent(0.8) : .secondaryLabelColor
|
||||||
|
}
|
||||||
|
|
||||||
|
public var fontColorCandidate: NSColor { isSelected ? .selectedMenuItemTextColor : .labelColor }
|
||||||
|
|
||||||
|
public init(key: String, displayedText: String, isSelected: Bool = false) {
|
||||||
|
self.key = key
|
||||||
|
self.displayedText = displayedText
|
||||||
|
self.isSelected = isSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
public var cellLength: Int {
|
||||||
|
let rect = attributedString.boundingRect(
|
||||||
|
with: NSSize(width: 1600.0, height: 1600.0),
|
||||||
|
options: [.usesLineFragmentOrigin]
|
||||||
|
)
|
||||||
|
let rawResult = ceil(rect.width + size / size)
|
||||||
|
return Int(rawResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var attributedStringHeader: NSAttributedString {
|
||||||
|
let paraStyleKey = NSMutableParagraphStyle()
|
||||||
|
paraStyleKey.setParagraphStyle(NSParagraphStyle.default)
|
||||||
|
paraStyleKey.alignment = .natural
|
||||||
|
let paraStyle = NSMutableParagraphStyle()
|
||||||
|
paraStyle.setParagraphStyle(NSParagraphStyle.default)
|
||||||
|
paraStyle.alignment = .natural
|
||||||
|
var attrKey: [NSAttributedString.Key: AnyObject] = [
|
||||||
|
.font: NSFont.monospacedDigitSystemFont(ofSize: size * 0.7, weight: .regular),
|
||||||
|
.paragraphStyle: paraStyleKey,
|
||||||
|
]
|
||||||
|
if isSelected {
|
||||||
|
attrKey[.foregroundColor] = NSColor.white.withAlphaComponent(0.8)
|
||||||
|
} else {
|
||||||
|
attrKey[.foregroundColor] = NSColor.secondaryLabelColor
|
||||||
|
}
|
||||||
|
let attrStrKey = NSMutableAttributedString(string: key, attributes: attrKey)
|
||||||
|
return attrStrKey
|
||||||
|
}
|
||||||
|
|
||||||
|
public var attributedString: NSAttributedString {
|
||||||
|
let paraStyleKey = NSMutableParagraphStyle()
|
||||||
|
paraStyleKey.setParagraphStyle(NSParagraphStyle.default)
|
||||||
|
paraStyleKey.alignment = .natural
|
||||||
|
let paraStyle = NSMutableParagraphStyle()
|
||||||
|
paraStyle.setParagraphStyle(NSParagraphStyle.default)
|
||||||
|
paraStyle.alignment = .natural
|
||||||
|
var attrCandidate: [NSAttributedString.Key: AnyObject] = [
|
||||||
|
.font: NSFont.monospacedDigitSystemFont(ofSize: size, weight: .regular),
|
||||||
|
.paragraphStyle: paraStyle,
|
||||||
|
]
|
||||||
|
if isSelected {
|
||||||
|
attrCandidate[.foregroundColor] = NSColor.white
|
||||||
|
} else {
|
||||||
|
attrCandidate[.foregroundColor] = NSColor.labelColor
|
||||||
|
}
|
||||||
|
if #available(macOS 12, *) {
|
||||||
|
if UserDefaults.standard.bool(forKey: UserDef.kHandleDefaultCandidateFontsByLangIdentifier.rawValue) {
|
||||||
|
attrCandidate[.languageIdentifier] = self.locale as AnyObject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let attrStrCandidate = NSMutableAttributedString(string: displayedText, attributes: attrCandidate)
|
||||||
|
return attrStrCandidate
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func == (lhs: CandidateCellData, rhs: CandidateCellData) -> Bool {
|
||||||
|
lhs.key == rhs.key && lhs.displayedText == rhs.displayedText
|
||||||
|
}
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(key)
|
||||||
|
hasher.combine(displayedText)
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,7 @@
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
public protocol CtlCandidateDelegate: AnyObject {
|
public protocol CtlCandidateDelegate: AnyObject {
|
||||||
func candidatePairs() -> [(String, String)]
|
func candidatePairs(conv: Bool) -> [(String, String)]
|
||||||
func candidatePairAt(_ index: Int) -> (String, String)
|
func candidatePairAt(_ index: Int) -> (String, String)
|
||||||
func candidatePairSelected(at index: Int)
|
func candidatePairSelected(at index: Int)
|
||||||
func buzz()
|
func buzz()
|
||||||
|
@ -17,21 +17,23 @@ public protocol CtlCandidateDelegate: AnyObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol CtlCandidateProtocol {
|
public protocol CtlCandidateProtocol {
|
||||||
|
var hint: String { get set }
|
||||||
var locale: String { get set }
|
var locale: String { get set }
|
||||||
var currentLayout: CandidateLayout { get set }
|
var currentLayout: NSUserInterfaceLayoutOrientation { get set }
|
||||||
var delegate: CtlCandidateDelegate? { get set }
|
var delegate: CtlCandidateDelegate? { get set }
|
||||||
var selectedCandidateIndex: Int { get set }
|
var selectedCandidateIndex: Int { get set }
|
||||||
var visible: Bool { get set }
|
var visible: Bool { get set }
|
||||||
var windowTopLeftPoint: NSPoint { get set }
|
var windowTopLeftPoint: NSPoint { get set }
|
||||||
var keyLabels: [CandidateKeyLabel] { get set }
|
var keyLabels: [CandidateCellData] { get set }
|
||||||
var keyLabelFont: NSFont { get set }
|
var keyLabelFont: NSFont { get set }
|
||||||
var candidateFont: NSFont { get set }
|
var candidateFont: NSFont { get set }
|
||||||
var tooltip: String { get set }
|
var tooltip: String { get set }
|
||||||
var useLangIdentifier: Bool { get set }
|
var useLangIdentifier: Bool { get set }
|
||||||
var showPageButtons: Bool { get set }
|
var showPageButtons: Bool { get set }
|
||||||
|
|
||||||
init(_ layout: CandidateLayout)
|
init(_ layout: NSUserInterfaceLayoutOrientation)
|
||||||
func reloadData()
|
func reloadData()
|
||||||
|
func updateDisplay()
|
||||||
func showNextPage() -> Bool
|
func showNextPage() -> Bool
|
||||||
func showPreviousPage() -> Bool
|
func showPreviousPage() -> Bool
|
||||||
func highlightNextCandidate() -> Bool
|
func highlightNextCandidate() -> Bool
|
||||||
|
|
|
@ -103,23 +103,6 @@ public enum UserDef: String, CaseIterable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Enums and Structs used by Candidate Window
|
|
||||||
|
|
||||||
public enum CandidateLayout {
|
|
||||||
case horizontal
|
|
||||||
case vertical
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct CandidateKeyLabel {
|
|
||||||
public private(set) var key: String
|
|
||||||
public private(set) var displayedText: String
|
|
||||||
|
|
||||||
public init(key: String, displayedText: String) {
|
|
||||||
self.key = key
|
|
||||||
self.displayedText = displayedText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Tooltip Color States
|
// MARK: - Tooltip Color States
|
||||||
|
|
||||||
public enum TooltipColorState {
|
public enum TooltipColorState {
|
||||||
|
|
|
@ -133,6 +133,8 @@ extension KeyHandler {
|
||||||
if !ctlCandidate.showPreviousPage() {
|
if !ctlCandidate.showPreviousPage() {
|
||||||
errorCallback("1919810D")
|
errorCallback("1919810D")
|
||||||
}
|
}
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -149,6 +151,8 @@ extension KeyHandler {
|
||||||
if !ctlCandidate.showNextPage() {
|
if !ctlCandidate.showNextPage() {
|
||||||
errorCallback("9244908D")
|
errorCallback("9244908D")
|
||||||
}
|
}
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -165,6 +169,8 @@ extension KeyHandler {
|
||||||
if !ctlCandidate.highlightPreviousCandidate() {
|
if !ctlCandidate.highlightPreviousCandidate() {
|
||||||
errorCallback("ASD9908D")
|
errorCallback("ASD9908D")
|
||||||
}
|
}
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -181,6 +187,8 @@ extension KeyHandler {
|
||||||
if !ctlCandidate.highlightNextCandidate() {
|
if !ctlCandidate.highlightNextCandidate() {
|
||||||
errorCallback("6B99908D")
|
errorCallback("6B99908D")
|
||||||
}
|
}
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -223,7 +231,7 @@ extension KeyHandler {
|
||||||
(state.type == .ofAssociates) ? input.inputTextIgnoringModifiers ?? "" : input.text
|
(state.type == .ofAssociates) ? input.inputTextIgnoringModifiers ?? "" : input.text
|
||||||
|
|
||||||
for j in 0..<ctlCandidate.keyLabels.count {
|
for j in 0..<ctlCandidate.keyLabels.count {
|
||||||
let label: CandidateKeyLabel = ctlCandidate.keyLabels[j]
|
let label: CandidateCellData = ctlCandidate.keyLabels[j]
|
||||||
if match.compare(label.key, options: .caseInsensitive, range: nil, locale: .current) == .orderedSame {
|
if match.compare(label.key, options: .caseInsensitive, range: nil, locale: .current) == .orderedSame {
|
||||||
index = j
|
index = j
|
||||||
break
|
break
|
||||||
|
|
|
@ -6,13 +6,13 @@
|
||||||
// marks, or product names of Contributor, except as required to fulfill notice
|
// marks, or product names of Contributor, except as required to fulfill notice
|
||||||
// requirements defined in MIT License.
|
// requirements defined in MIT License.
|
||||||
|
|
||||||
|
import CandidateWindow
|
||||||
import CocoaExtension
|
import CocoaExtension
|
||||||
import IMKUtils
|
import IMKUtils
|
||||||
import PopupCompositionBuffer
|
import PopupCompositionBuffer
|
||||||
import Shared
|
import Shared
|
||||||
import ShiftKeyUpChecker
|
import ShiftKeyUpChecker
|
||||||
import TooltipUI
|
import TooltipUI
|
||||||
import Voltaire
|
|
||||||
|
|
||||||
/// 輸入法控制模組,乃在輸入法端用以控制輸入行為的基礎型別。
|
/// 輸入法控制模組,乃在輸入法端用以控制輸入行為的基礎型別。
|
||||||
///
|
///
|
||||||
|
@ -28,8 +28,16 @@ class SessionCtl: IMKInputController {
|
||||||
static var areWeNerfing = false
|
static var areWeNerfing = false
|
||||||
|
|
||||||
/// 目前在用的的選字窗副本。
|
/// 目前在用的的選字窗副本。
|
||||||
static var ctlCandidateCurrent: CtlCandidateProtocol =
|
static var ctlCandidateCurrent: CtlCandidateProtocol = {
|
||||||
PrefMgr.shared.useIMKCandidateWindow ? CtlCandidateIMK(.horizontal) : CtlCandidateUniversal(.horizontal)
|
let direction: NSUserInterfaceLayoutOrientation =
|
||||||
|
PrefMgr.shared.useHorizontalCandidateList ? .horizontal : .vertical
|
||||||
|
if #available(macOS 12, *) {
|
||||||
|
return PrefMgr.shared.useIMKCandidateWindow
|
||||||
|
? CtlCandidateIMK(direction) : CtlCandidateTDK(direction)
|
||||||
|
} else {
|
||||||
|
return CtlCandidateIMK(direction)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
/// 工具提示視窗的共用副本。
|
/// 工具提示視窗的共用副本。
|
||||||
static var tooltipInstance = TooltipUI()
|
static var tooltipInstance = TooltipUI()
|
||||||
|
|
|
@ -54,8 +54,16 @@ extension SessionCtl: CtlCandidateDelegate {
|
||||||
ChineseConverter.kanjiConversionIfRequired(target)
|
ChineseConverter.kanjiConversionIfRequired(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
func candidatePairs() -> [(String, String)] {
|
func candidatePairs(conv: Bool = false) -> [(String, String)] {
|
||||||
state.isCandidateContainer ? state.candidates : []
|
if !state.isCandidateContainer { return [] }
|
||||||
|
if !conv { return state.candidates }
|
||||||
|
let convertedCandidates: [(String, String)] = state.candidates.map { theCandidatePair -> (String, String) in
|
||||||
|
let theCandidate = theCandidatePair.1
|
||||||
|
let theConverted = ChineseConverter.kanjiConversionIfRequired(theCandidate)
|
||||||
|
let result = (theCandidate == theConverted) ? theCandidate : "\(theConverted)(\(theCandidate))"
|
||||||
|
return (theCandidatePair.0, result)
|
||||||
|
}
|
||||||
|
return convertedCandidates
|
||||||
}
|
}
|
||||||
|
|
||||||
func candidatePairAt(_ index: Int) -> (String, String) {
|
func candidatePairAt(_ index: Int) -> (String, String) {
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
// marks, or product names of Contributor, except as required to fulfill notice
|
// marks, or product names of Contributor, except as required to fulfill notice
|
||||||
// requirements defined in MIT License.
|
// requirements defined in MIT License.
|
||||||
|
|
||||||
|
import CandidateWindow
|
||||||
import NSAttributedTextView
|
import NSAttributedTextView
|
||||||
import Shared
|
import Shared
|
||||||
import Voltaire
|
|
||||||
|
|
||||||
// MARK: - Tooltip Display and Candidate Display Methods
|
// MARK: - Tooltip Display and Candidate Display Methods
|
||||||
|
|
||||||
|
@ -72,21 +72,20 @@ extension SessionCtl {
|
||||||
func showCandidates() {
|
func showCandidates() {
|
||||||
guard let client = client() else { return }
|
guard let client = client() else { return }
|
||||||
var isCandidateWindowVertical: Bool {
|
var isCandidateWindowVertical: Bool {
|
||||||
var candidates: [(String, String)] = .init()
|
// var candidates: [(String, String)] = .init()
|
||||||
if state.isCandidateContainer {
|
// if state.isCandidateContainer { candidates = state.candidates }
|
||||||
candidates = state.candidates
|
|
||||||
}
|
|
||||||
if isVerticalTyping { return true }
|
if isVerticalTyping { return true }
|
||||||
// 接下來的判斷並非適用於 IMK 選字窗,所以先插入排除語句。
|
// 接下來的判斷並非適用於 IMK 選字窗,所以先插入排除語句。
|
||||||
guard Self.ctlCandidateCurrent is CtlCandidateUniversal else { return false }
|
// guard Self.ctlCandidateCurrent is CtlCandidateUniversal else { return false }
|
||||||
// 以上是通用情形。接下來決定橫排輸入時是否使用縱排選字窗。
|
// 以上是通用情形。接下來決定橫排輸入時是否使用縱排選字窗。
|
||||||
// 因為在拿候選字陣列時已經排序過了,所以這裡不用再多排序。
|
// 因為在拿候選字陣列時已經排序過了,所以這裡不用再多排序。
|
||||||
// 測量每頁顯示候選字的累計總長度。如果太長的話就強制使用縱排候選字窗。
|
// 測量每頁顯示候選字的累計總長度。如果太長的話就強制使用縱排候選字窗。
|
||||||
// 範例:「屬實牛逼」(會有一大串各種各樣的「鼠食牛Beer」的 emoji)。
|
// 範例:「屬實牛逼」(會有一大串各種各樣的「鼠食牛Beer」的 emoji)。
|
||||||
let maxCandidatesPerPage = PrefMgr.shared.candidateKeys.count
|
// let maxCandidatesPerPage = PrefMgr.shared.candidateKeys.count
|
||||||
let firstPageCandidates = candidates[0..<min(maxCandidatesPerPage, candidates.count)].map(\.1)
|
// let firstPageCandidates = candidates[0..<min(maxCandidatesPerPage, candidates.count)].map(\.1)
|
||||||
return firstPageCandidates.joined().count > Int(round(Double(maxCandidatesPerPage) * 1.8))
|
// return firstPageCandidates.joined().count > Int(round(Double(maxCandidatesPerPage) * 1.8))
|
||||||
// 上面這句如果是 true 的話,就會是縱排;反之則為橫排。
|
// 上面這句如果是 true 的話,就會是縱排;反之則為橫排。
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
state.isVerticalCandidateWindow = (isCandidateWindowVertical || !PrefMgr.shared.useHorizontalCandidateList)
|
state.isVerticalCandidateWindow = (isCandidateWindowVertical || !PrefMgr.shared.useHorizontalCandidateList)
|
||||||
|
@ -99,14 +98,18 @@ extension SessionCtl {
|
||||||
/// layoutCandidateView 在這裡無法起到糾正作用。
|
/// layoutCandidateView 在這裡無法起到糾正作用。
|
||||||
/// 該問題徹底解決的價值並不大,直接等到 macOS 10.x 全線淘汰之後用 SwiftUI 重寫選字窗吧。
|
/// 該問題徹底解決的價值並不大,直接等到 macOS 10.x 全線淘汰之後用 SwiftUI 重寫選字窗吧。
|
||||||
|
|
||||||
let candidateLayout: CandidateLayout =
|
let candidateLayout: NSUserInterfaceLayoutOrientation =
|
||||||
((isCandidateWindowVertical || !PrefMgr.shared.useHorizontalCandidateList)
|
((isCandidateWindowVertical || !PrefMgr.shared.useHorizontalCandidateList)
|
||||||
? CandidateLayout.vertical
|
? .vertical
|
||||||
: CandidateLayout.horizontal)
|
: .horizontal)
|
||||||
|
|
||||||
Self.ctlCandidateCurrent =
|
if #available(macOS 12, *) {
|
||||||
PrefMgr.shared.useIMKCandidateWindow
|
Self.ctlCandidateCurrent =
|
||||||
? CtlCandidateIMK(candidateLayout) : CtlCandidateUniversal(candidateLayout)
|
PrefMgr.shared.useIMKCandidateWindow
|
||||||
|
? CtlCandidateIMK(candidateLayout) : CtlCandidateTDK(candidateLayout)
|
||||||
|
} else {
|
||||||
|
Self.ctlCandidateCurrent = CtlCandidateIMK(candidateLayout)
|
||||||
|
}
|
||||||
|
|
||||||
// set the attributes for the candidate panel (which uses NSAttributedString)
|
// set the attributes for the candidate panel (which uses NSAttributedString)
|
||||||
let textSize = PrefMgr.shared.candidateListTextSize
|
let textSize = PrefMgr.shared.candidateListTextSize
|
||||||
|
@ -132,7 +135,11 @@ extension SessionCtl {
|
||||||
candidateKeys.count > 4 ? Array(candidateKeys) : Array(CandidateKey.defaultKeys)
|
candidateKeys.count > 4 ? Array(candidateKeys) : Array(CandidateKey.defaultKeys)
|
||||||
let keyLabelSuffix = state.type == .ofAssociates ? "^" : ""
|
let keyLabelSuffix = state.type == .ofAssociates ? "^" : ""
|
||||||
Self.ctlCandidateCurrent.keyLabels = keyLabels.map {
|
Self.ctlCandidateCurrent.keyLabels = keyLabels.map {
|
||||||
CandidateKeyLabel(key: String($0), displayedText: String($0) + keyLabelSuffix)
|
CandidateCellData(key: String($0), displayedText: String($0) + keyLabelSuffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.type == .ofAssociates {
|
||||||
|
Self.ctlCandidateCurrent.hint = NSLocalizedString("Hold ⇧ to choose associates.", comment: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
Self.ctlCandidateCurrent.delegate = self
|
Self.ctlCandidateCurrent.delegate = self
|
||||||
|
|
|
@ -196,6 +196,7 @@ extension SessionCtl {
|
||||||
switch imkC.currentLayout {
|
switch imkC.currentLayout {
|
||||||
case .horizontal: _ = event.isShiftHold ? imkC.moveUp(self) : imkC.moveDown(self)
|
case .horizontal: _ = event.isShiftHold ? imkC.moveUp(self) : imkC.moveDown(self)
|
||||||
case .vertical: _ = event.isShiftHold ? imkC.moveLeft(self) : imkC.moveRight(self)
|
case .vertical: _ = event.isShiftHold ? imkC.moveLeft(self) : imkC.moveRight(self)
|
||||||
|
@unknown default: break
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
} else if event.isSpace {
|
} else if event.isSpace {
|
||||||
|
|
|
@ -11,10 +11,11 @@ import Shared
|
||||||
|
|
||||||
/// 威注音自用的 IMKCandidates 型別。因為有用到 bridging header,所以無法弄成 Swift Package。
|
/// 威注音自用的 IMKCandidates 型別。因為有用到 bridging header,所以無法弄成 Swift Package。
|
||||||
public class CtlCandidateIMK: IMKCandidates, CtlCandidateProtocol {
|
public class CtlCandidateIMK: IMKCandidates, CtlCandidateProtocol {
|
||||||
|
public var hint: String = ""
|
||||||
public var showPageButtons: Bool = false
|
public var showPageButtons: Bool = false
|
||||||
public var locale: String = ""
|
public var locale: String = ""
|
||||||
public var useLangIdentifier: Bool = false
|
public var useLangIdentifier: Bool = false
|
||||||
public var currentLayout: CandidateLayout = .horizontal
|
public var currentLayout: NSUserInterfaceLayoutOrientation = .horizontal
|
||||||
public static let defaultIMKSelectionKey: [UInt16: String] = [
|
public static let defaultIMKSelectionKey: [UInt16: String] = [
|
||||||
18: "1", 19: "2", 20: "3", 21: "4", 23: "5", 22: "6", 26: "7", 28: "8", 25: "9",
|
18: "1", 19: "2", 20: "3", 21: "4", 23: "5", 22: "6", 26: "7", 28: "8", 25: "9",
|
||||||
]
|
]
|
||||||
|
@ -38,9 +39,9 @@ public class CtlCandidateIMK: IMKCandidates, CtlCandidateProtocol {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var keyLabels: [CandidateKeyLabel] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
public var keyLabels: [CandidateCellData] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
||||||
.map {
|
.map {
|
||||||
CandidateKeyLabel(key: $0, displayedText: $0)
|
CandidateCellData(key: $0, displayedText: $0)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var keyLabelFont = NSFont.monospacedDigitSystemFont(
|
public var keyLabelFont = NSFont.monospacedDigitSystemFont(
|
||||||
|
@ -68,7 +69,7 @@ public class CtlCandidateIMK: IMKCandidates, CtlCandidateProtocol {
|
||||||
var keyCount = 0
|
var keyCount = 0
|
||||||
var displayedCandidates = [String]()
|
var displayedCandidates = [String]()
|
||||||
|
|
||||||
public func specifyLayout(_ layout: CandidateLayout = .horizontal) {
|
public func specifyLayout(_ layout: NSUserInterfaceLayoutOrientation = .horizontal) {
|
||||||
currentLayout = layout
|
currentLayout = layout
|
||||||
switch currentLayout {
|
switch currentLayout {
|
||||||
case .horizontal:
|
case .horizontal:
|
||||||
|
@ -80,10 +81,14 @@ public class CtlCandidateIMK: IMKCandidates, CtlCandidateProtocol {
|
||||||
}
|
}
|
||||||
case .vertical:
|
case .vertical:
|
||||||
setPanelType(kIMKSingleColumnScrollingCandidatePanel)
|
setPanelType(kIMKSingleColumnScrollingCandidatePanel)
|
||||||
|
@unknown default:
|
||||||
|
setPanelType(kIMKSingleRowSteppingCandidatePanel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public required init(_ layout: CandidateLayout = .horizontal) {
|
public func updateDisplay() {}
|
||||||
|
|
||||||
|
public required init(_ layout: NSUserInterfaceLayoutOrientation = .horizontal) {
|
||||||
super.init(server: theServer, panelType: kIMKScrollingGridCandidatePanel)
|
super.init(server: theServer, panelType: kIMKScrollingGridCandidatePanel)
|
||||||
specifyLayout(layout)
|
specifyLayout(layout)
|
||||||
// 設為 true 表示先交給 ctlIME 處理
|
// 設為 true 表示先交給 ctlIME 處理
|
||||||
|
@ -115,7 +120,7 @@ public class CtlCandidateIMK: IMKCandidates, CtlCandidateProtocol {
|
||||||
|
|
||||||
private var pageCount: Int {
|
private var pageCount: Int {
|
||||||
guard let delegate = delegate else { return 0 }
|
guard let delegate = delegate else { return 0 }
|
||||||
let totalCount = delegate.candidatePairs().count
|
let totalCount = delegate.candidatePairs(conv: false).count
|
||||||
let keyLabelCount = keyLabels.count
|
let keyLabelCount = keyLabels.count
|
||||||
return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0)
|
return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0)
|
||||||
}
|
}
|
||||||
|
@ -147,7 +152,7 @@ public class CtlCandidateIMK: IMKCandidates, CtlCandidateProtocol {
|
||||||
public func candidateIndexAtKeyLabelIndex(_ index: Int) -> Int {
|
public func candidateIndexAtKeyLabelIndex(_ index: Int) -> Int {
|
||||||
guard let delegate = delegate else { return Int.max }
|
guard let delegate = delegate else { return Int.max }
|
||||||
let result = currentPageIndex * keyLabels.count + index
|
let result = currentPageIndex * keyLabels.count + index
|
||||||
return result < delegate.candidatePairs().count ? result : Int.max
|
return result < delegate.candidatePairs(conv: false).count ? result : Int.max
|
||||||
}
|
}
|
||||||
|
|
||||||
public var selectedCandidateIndex: Int {
|
public var selectedCandidateIndex: Int {
|
||||||
|
|
|
@ -82,6 +82,7 @@
|
||||||
"Optimize Memorized Phrases" = "Optimize Memorized Phrases";
|
"Optimize Memorized Phrases" = "Optimize Memorized Phrases";
|
||||||
"Clear Memorized Phrases" = "Clear Memorized Phrases";
|
"Clear Memorized Phrases" = "Clear Memorized Phrases";
|
||||||
"Currency Numeral Output" = "Currency Numeral Output";
|
"Currency Numeral Output" = "Currency Numeral Output";
|
||||||
|
"Hold ⇧ to choose associates." = "Hold ⇧ to choose associates.";
|
||||||
|
|
||||||
// The followings are the category names used in the Symbol menu.
|
// The followings are the category names used in the Symbol menu.
|
||||||
"catCommonSymbols" = "CommonSymbols";
|
"catCommonSymbols" = "CommonSymbols";
|
||||||
|
|
|
@ -82,6 +82,7 @@
|
||||||
"Optimize Memorized Phrases" = "Optimize Memorized Phrases";
|
"Optimize Memorized Phrases" = "Optimize Memorized Phrases";
|
||||||
"Clear Memorized Phrases" = "Clear Memorized Phrases";
|
"Clear Memorized Phrases" = "Clear Memorized Phrases";
|
||||||
"Currency Numeral Output" = "Currency Numeral Output";
|
"Currency Numeral Output" = "Currency Numeral Output";
|
||||||
|
"Hold ⇧ to choose associates." = "Hold ⇧ to choose associates.";
|
||||||
|
|
||||||
// The followings are the category names used in the Symbol menu.
|
// The followings are the category names used in the Symbol menu.
|
||||||
"catCommonSymbols" = "CommonSymbols";
|
"catCommonSymbols" = "CommonSymbols";
|
||||||
|
|
|
@ -82,6 +82,7 @@
|
||||||
"Optimize Memorized Phrases" = "臨時記憶資料を整う";
|
"Optimize Memorized Phrases" = "臨時記憶資料を整う";
|
||||||
"Clear Memorized Phrases" = "臨時記憶資料を削除";
|
"Clear Memorized Phrases" = "臨時記憶資料を削除";
|
||||||
"Currency Numeral Output" = "数字大字変換";
|
"Currency Numeral Output" = "数字大字変換";
|
||||||
|
"Hold ⇧ to choose associates." = "⇧を押しながら連想候補をご選択ください。";
|
||||||
|
|
||||||
// The followings are the category names used in the Symbol menu.
|
// The followings are the category names used in the Symbol menu.
|
||||||
"catCommonSymbols" = "常用";
|
"catCommonSymbols" = "常用";
|
||||||
|
|
|
@ -82,6 +82,7 @@
|
||||||
"Optimize Memorized Phrases" = "精简临时记忆语汇资料";
|
"Optimize Memorized Phrases" = "精简临时记忆语汇资料";
|
||||||
"Clear Memorized Phrases" = "清除临时记忆语汇资料";
|
"Clear Memorized Phrases" = "清除临时记忆语汇资料";
|
||||||
"Currency Numeral Output" = "大写汉字数字输出";
|
"Currency Numeral Output" = "大写汉字数字输出";
|
||||||
|
"Hold ⇧ to choose associates." = "摁住⇧以选取联想词。";
|
||||||
|
|
||||||
// The followings are the category names used in the Symbol menu.
|
// The followings are the category names used in the Symbol menu.
|
||||||
"catCommonSymbols" = "常用";
|
"catCommonSymbols" = "常用";
|
||||||
|
|
|
@ -82,6 +82,7 @@
|
||||||
"Optimize Memorized Phrases" = "精簡臨時記憶語彙資料";
|
"Optimize Memorized Phrases" = "精簡臨時記憶語彙資料";
|
||||||
"Clear Memorized Phrases" = "清除臨時記憶語彙資料";
|
"Clear Memorized Phrases" = "清除臨時記憶語彙資料";
|
||||||
"Currency Numeral Output" = "大寫漢字數字輸出";
|
"Currency Numeral Output" = "大寫漢字數字輸出";
|
||||||
|
"Hold ⇧ to choose associates." = "摁住⇧以選取聯想詞。";
|
||||||
|
|
||||||
// The followings are the category names used in the Symbol menu.
|
// The followings are the category names used in the Symbol menu.
|
||||||
"catCommonSymbols" = "常用";
|
"catCommonSymbols" = "常用";
|
||||||
|
|
Loading…
Reference in New Issue