Repo // Introducing CtlCandidateTDK (horizontal).

This commit is contained in:
ShikiSuen 2022-09-27 10:19:49 +08:00
parent 72b1099e63
commit eadae22dbb
22 changed files with 615 additions and 65 deletions

View File

@ -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

View File

@ -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"]
),
] ]
) )

View File

@ -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).

View File

@ -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]
}
}
}
}
}

View File

@ -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() {}
} }

View File

@ -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()
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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()

View File

@ -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) {

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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";

View File

@ -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";

View File

@ -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" = "常用";

View File

@ -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" = "常用";

View File

@ -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" = "常用";