diff --git a/Packages/OpenVanilla_Voltaire/Sources/Voltaire/CtlCandidateUniversal.swift b/Packages/OpenVanilla_Voltaire/Sources/Voltaire/CtlCandidateUniversal.swift index 858d215f..64791caf 100644 --- a/Packages/OpenVanilla_Voltaire/Sources/Voltaire/CtlCandidateUniversal.swift +++ b/Packages/OpenVanilla_Voltaire/Sources/Voltaire/CtlCandidateUniversal.swift @@ -301,17 +301,18 @@ public class CtlCandidateUniversal: CtlCandidate { private var nextPageButton: NSButton private var pageCounterLabel: NSTextField private var currentPageIndex: Int = 0 - override public var currentLayout: CandidateLayout { + override public var currentLayout: NSUserInterfaceLayoutOrientation { get { candidateView.isVerticalLayout ? .vertical : .horizontal } set { switch newValue { case .vertical: candidateView.isVerticalLayout = true 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) let styleMask: NSWindow.StyleMask = [.nonactivatingPanel] let panel = NSPanel( @@ -433,7 +434,7 @@ public class CtlCandidateUniversal: CtlCandidate { @discardableResult override public func highlightNextCandidate() -> Bool { guard let delegate = delegate else { return false } selectedCandidateIndex = - (selectedCandidateIndex + 1 >= delegate.candidatePairs().count) + (selectedCandidateIndex + 1 >= delegate.candidatePairs(conv: false).count) ? 0 : selectedCandidateIndex + 1 return true } @@ -442,7 +443,7 @@ public class CtlCandidateUniversal: CtlCandidate { guard let delegate = delegate else { return false } selectedCandidateIndex = (selectedCandidateIndex == 0) - ? delegate.candidatePairs().count - 1 : selectedCandidateIndex - 1 + ? delegate.candidatePairs(conv: false).count - 1 : selectedCandidateIndex - 1 return true } @@ -452,7 +453,7 @@ public class CtlCandidateUniversal: CtlCandidate { } 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 { @@ -464,7 +465,7 @@ public class CtlCandidateUniversal: CtlCandidate { return } let keyLabelCount = keyLabels.count - if newValue < delegate.candidatePairs().count { + if newValue < delegate.candidatePairs(conv: false).count { currentPageIndex = newValue / keyLabelCount candidateView.highlightedIndex = newValue % keyLabelCount layoutCandidateView() @@ -478,7 +479,7 @@ extension CtlCandidateUniversal { guard let delegate = delegate else { return 0 } - let totalCount = delegate.candidatePairs().count + let totalCount = delegate.candidatePairs(conv: false).count let keyLabelCount = keyLabels.count return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0) } @@ -487,7 +488,7 @@ extension CtlCandidateUniversal { guard let delegate = delegate else { return 0 } - let totalCount = delegate.candidatePairs().count + let totalCount = delegate.candidatePairs(conv: false).count let keyLabelCount = keyLabels.count return totalCount % keyLabelCount } @@ -497,7 +498,7 @@ extension CtlCandidateUniversal { candidateView.set(keyLabelFont: keyLabelFont, candidateFont: candidateFont) var candidates = [(String, String)]() - let count = delegate.candidatePairs().count + let count = delegate.candidatePairs(conv: false).count let keyLabelCount = keyLabels.count let begin = currentPageIndex * keyLabelCount diff --git a/Packages/vChewing_CandidateWindow/Package.swift b/Packages/vChewing_CandidateWindow/Package.swift index 0ca9b8be..837e3572 100644 --- a/Packages/vChewing_CandidateWindow/Package.swift +++ b/Packages/vChewing_CandidateWindow/Package.swift @@ -21,6 +21,10 @@ let package = Package( dependencies: [ .product(name: "Shared", package: "vChewing_Shared") ] - ) + ), + .testTarget( + name: "CandidateWindowTests", + dependencies: ["CandidateWindow"] + ), ] ) diff --git a/Packages/vChewing_CandidateWindow/README.md b/Packages/vChewing_CandidateWindow/README.md index 628d5b76..e929a70b 100644 --- a/Packages/vChewing_CandidateWindow/README.md +++ b/Packages/vChewing_CandidateWindow/README.md @@ -1,6 +1,17 @@ # CandidateWindow -用以定義與威注音的選字窗有關的基礎內容,目前尚未完工。 +用以定義與威注音的選字窗有關的基礎內容。此外,還包含了威注音自家的次世代選字窗「田所(TDK)」。 + +> 命名緣由:野獸先輩「田所」的姓氏。 + +TDK 選字窗以純 SwiftUI 構築,用以取代此前自上游繼承來的 Voltaire 選字窗。 + +然而,TDK 選字窗目前有下述侷限: + +- 因 SwiftUI 自身特性所導致的嚴重的效能問題。基本上來講,如果您經常使用全字庫模式的話,請在偏好設定內啟用效能更高的 IMK 選字窗。 +- TDK 選字窗目前僅完成了橫版矩陣陳列模式的實作,且尚未引入對縱排選字窗陳列佈局的支援。 + +因為這些問題恐怕需要很久才能全部解決,所以威注音會在這段時間內推薦使用者們優先使用 IMK 選字窗。 ``` // (c) 2021 and onwards The vChewing Project (MIT-NTL License). diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool.swift new file mode 100644 index 00000000..dcdae5b8 --- /dev/null +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool.swift @@ -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.. 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() + } + } +} diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/VwrCandidateTDK.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/VwrCandidateTDK.swift new file mode 100644 index 00000000..adbbda0e --- /dev/null +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/VwrCandidateTDK.swift @@ -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 + } +} diff --git a/Packages/vChewing_CandidateWindow/Tests/CandidateWindowTests/CandidatePoolTests.swift b/Packages/vChewing_CandidateWindow/Tests/CandidateWindowTests/CandidatePoolTests.swift new file mode 100644 index 00000000..33b69f6f --- /dev/null +++ b/Packages/vChewing_CandidateWindow/Tests/CandidateWindowTests/CandidatePoolTests.swift @@ -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) + } +} diff --git a/Packages/vChewing_Shared/Sources/Shared/CandidateBasicUnits.swift b/Packages/vChewing_Shared/Sources/Shared/CandidateBasicUnits.swift new file mode 100644 index 00000000..74f57af4 --- /dev/null +++ b/Packages/vChewing_Shared/Sources/Shared/CandidateBasicUnits.swift @@ -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) + } +} diff --git a/Packages/vChewing_Shared/Sources/Shared/Protocols/CtlCandidateProtocol.swift b/Packages/vChewing_Shared/Sources/Shared/Protocols/CtlCandidateProtocol.swift index f7ab9800..b066f4c2 100644 --- a/Packages/vChewing_Shared/Sources/Shared/Protocols/CtlCandidateProtocol.swift +++ b/Packages/vChewing_Shared/Sources/Shared/Protocols/CtlCandidateProtocol.swift @@ -9,7 +9,7 @@ import Cocoa public protocol CtlCandidateDelegate: AnyObject { - func candidatePairs() -> [(String, String)] + func candidatePairs(conv: Bool) -> [(String, String)] func candidatePairAt(_ index: Int) -> (String, String) func candidatePairSelected(at index: Int) func buzz() @@ -17,21 +17,23 @@ public protocol CtlCandidateDelegate: AnyObject { } public protocol CtlCandidateProtocol { + var hint: String { get set } var locale: String { get set } - var currentLayout: CandidateLayout { get set } + var currentLayout: NSUserInterfaceLayoutOrientation { get set } var delegate: CtlCandidateDelegate? { get set } var selectedCandidateIndex: Int { get set } var visible: Bool { get set } var windowTopLeftPoint: NSPoint { get set } - var keyLabels: [CandidateKeyLabel] { get set } + var keyLabels: [CandidateCellData] { get set } var keyLabelFont: NSFont { get set } var candidateFont: NSFont { get set } var tooltip: String { get set } var useLangIdentifier: Bool { get set } var showPageButtons: Bool { get set } - init(_ layout: CandidateLayout) + init(_ layout: NSUserInterfaceLayoutOrientation) func reloadData() + func updateDisplay() func showNextPage() -> Bool func showPreviousPage() -> Bool func highlightNextCandidate() -> Bool diff --git a/Packages/vChewing_Shared/Sources/Shared/Shared.swift b/Packages/vChewing_Shared/Sources/Shared/Shared.swift index 78c63280..d7ba66e8 100644 --- a/Packages/vChewing_Shared/Sources/Shared/Shared.swift +++ b/Packages/vChewing_Shared/Sources/Shared/Shared.swift @@ -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 public enum TooltipColorState { diff --git a/Source/Modules/KeyHandler_HandleCandidate.swift b/Source/Modules/KeyHandler_HandleCandidate.swift index b449d2bc..0c54ed88 100644 --- a/Source/Modules/KeyHandler_HandleCandidate.swift +++ b/Source/Modules/KeyHandler_HandleCandidate.swift @@ -133,6 +133,8 @@ extension KeyHandler { if !ctlCandidate.showPreviousPage() { errorCallback("1919810D") } + @unknown default: + break } return true } @@ -149,6 +151,8 @@ extension KeyHandler { if !ctlCandidate.showNextPage() { errorCallback("9244908D") } + @unknown default: + break } return true } @@ -165,6 +169,8 @@ extension KeyHandler { if !ctlCandidate.highlightPreviousCandidate() { errorCallback("ASD9908D") } + @unknown default: + break } return true } @@ -181,6 +187,8 @@ extension KeyHandler { if !ctlCandidate.highlightNextCandidate() { errorCallback("6B99908D") } + @unknown default: + break } return true } @@ -223,7 +231,7 @@ extension KeyHandler { (state.type == .ofAssociates) ? input.inputTextIgnoringModifiers ?? "" : input.text for j in 0.. [(String, String)] { - state.isCandidateContainer ? state.candidates : [] + func candidatePairs(conv: Bool = false) -> [(String, String)] { + 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) { diff --git a/Source/Modules/SessionCtl_HandleDisplay.swift b/Source/Modules/SessionCtl_HandleDisplay.swift index 574cdfc4..21b412c8 100644 --- a/Source/Modules/SessionCtl_HandleDisplay.swift +++ b/Source/Modules/SessionCtl_HandleDisplay.swift @@ -6,9 +6,9 @@ // marks, or product names of Contributor, except as required to fulfill notice // requirements defined in MIT License. +import CandidateWindow import NSAttributedTextView import Shared -import Voltaire // MARK: - Tooltip Display and Candidate Display Methods @@ -72,21 +72,20 @@ extension SessionCtl { func showCandidates() { guard let client = client() else { return } var isCandidateWindowVertical: Bool { - var candidates: [(String, String)] = .init() - if state.isCandidateContainer { - candidates = state.candidates - } + // var candidates: [(String, String)] = .init() + // if state.isCandidateContainer { candidates = state.candidates } if isVerticalTyping { return true } // 接下來的判斷並非適用於 IMK 選字窗,所以先插入排除語句。 - guard Self.ctlCandidateCurrent is CtlCandidateUniversal else { return false } + // guard Self.ctlCandidateCurrent is CtlCandidateUniversal else { return false } // 以上是通用情形。接下來決定橫排輸入時是否使用縱排選字窗。 // 因為在拿候選字陣列時已經排序過了,所以這裡不用再多排序。 // 測量每頁顯示候選字的累計總長度。如果太長的話就強制使用縱排候選字窗。 // 範例:「屬實牛逼」(會有一大串各種各樣的「鼠食牛Beer」的 emoji)。 - let maxCandidatesPerPage = PrefMgr.shared.candidateKeys.count - let firstPageCandidates = candidates[0.. Int(round(Double(maxCandidatesPerPage) * 1.8)) + // let maxCandidatesPerPage = PrefMgr.shared.candidateKeys.count + // let firstPageCandidates = candidates[0.. Int(round(Double(maxCandidatesPerPage) * 1.8)) // 上面這句如果是 true 的話,就會是縱排;反之則為橫排。 + return false } state.isVerticalCandidateWindow = (isCandidateWindowVertical || !PrefMgr.shared.useHorizontalCandidateList) @@ -99,14 +98,18 @@ extension SessionCtl { /// layoutCandidateView 在這裡無法起到糾正作用。 /// 該問題徹底解決的價值並不大,直接等到 macOS 10.x 全線淘汰之後用 SwiftUI 重寫選字窗吧。 - let candidateLayout: CandidateLayout = + let candidateLayout: NSUserInterfaceLayoutOrientation = ((isCandidateWindowVertical || !PrefMgr.shared.useHorizontalCandidateList) - ? CandidateLayout.vertical - : CandidateLayout.horizontal) + ? .vertical + : .horizontal) - Self.ctlCandidateCurrent = - PrefMgr.shared.useIMKCandidateWindow - ? CtlCandidateIMK(candidateLayout) : CtlCandidateUniversal(candidateLayout) + if #available(macOS 12, *) { + Self.ctlCandidateCurrent = + PrefMgr.shared.useIMKCandidateWindow + ? CtlCandidateIMK(candidateLayout) : CtlCandidateTDK(candidateLayout) + } else { + Self.ctlCandidateCurrent = CtlCandidateIMK(candidateLayout) + } // set the attributes for the candidate panel (which uses NSAttributedString) let textSize = PrefMgr.shared.candidateListTextSize @@ -132,7 +135,11 @@ extension SessionCtl { candidateKeys.count > 4 ? Array(candidateKeys) : Array(CandidateKey.defaultKeys) let keyLabelSuffix = state.type == .ofAssociates ? "^" : "" 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 diff --git a/Source/Modules/SessionCtl_HandleEvent.swift b/Source/Modules/SessionCtl_HandleEvent.swift index 5b55c778..801c7242 100644 --- a/Source/Modules/SessionCtl_HandleEvent.swift +++ b/Source/Modules/SessionCtl_HandleEvent.swift @@ -196,6 +196,7 @@ extension SessionCtl { switch imkC.currentLayout { case .horizontal: _ = event.isShiftHold ? imkC.moveUp(self) : imkC.moveDown(self) case .vertical: _ = event.isShiftHold ? imkC.moveLeft(self) : imkC.moveRight(self) + @unknown default: break } return true } else if event.isSpace { diff --git a/Source/Modules/UIModules/CandidateUI/IMKCandidatesImpl.swift b/Source/Modules/UIModules/CandidateUI/IMKCandidatesImpl.swift index 40dce0b2..5257ab85 100644 --- a/Source/Modules/UIModules/CandidateUI/IMKCandidatesImpl.swift +++ b/Source/Modules/UIModules/CandidateUI/IMKCandidatesImpl.swift @@ -11,10 +11,11 @@ import Shared /// 威注音自用的 IMKCandidates 型別。因為有用到 bridging header,所以無法弄成 Swift Package。 public class CtlCandidateIMK: IMKCandidates, CtlCandidateProtocol { + public var hint: String = "" public var showPageButtons: Bool = false public var locale: String = "" public var useLangIdentifier: Bool = false - public var currentLayout: CandidateLayout = .horizontal + public var currentLayout: NSUserInterfaceLayoutOrientation = .horizontal public static let defaultIMKSelectionKey: [UInt16: String] = [ 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 { - CandidateKeyLabel(key: $0, displayedText: $0) + CandidateCellData(key: $0, displayedText: $0) } public var keyLabelFont = NSFont.monospacedDigitSystemFont( @@ -68,7 +69,7 @@ public class CtlCandidateIMK: IMKCandidates, CtlCandidateProtocol { var keyCount = 0 var displayedCandidates = [String]() - public func specifyLayout(_ layout: CandidateLayout = .horizontal) { + public func specifyLayout(_ layout: NSUserInterfaceLayoutOrientation = .horizontal) { currentLayout = layout switch currentLayout { case .horizontal: @@ -80,10 +81,14 @@ public class CtlCandidateIMK: IMKCandidates, CtlCandidateProtocol { } case .vertical: 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) specifyLayout(layout) // 設為 true 表示先交給 ctlIME 處理 @@ -115,7 +120,7 @@ public class CtlCandidateIMK: IMKCandidates, CtlCandidateProtocol { private var pageCount: Int { guard let delegate = delegate else { return 0 } - let totalCount = delegate.candidatePairs().count + let totalCount = delegate.candidatePairs(conv: false).count let keyLabelCount = keyLabels.count return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0) } @@ -147,7 +152,7 @@ public class CtlCandidateIMK: IMKCandidates, CtlCandidateProtocol { public func candidateIndexAtKeyLabelIndex(_ index: Int) -> Int { guard let delegate = delegate else { return Int.max } 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 { diff --git a/Source/Resources/Base.lproj/Localizable.strings b/Source/Resources/Base.lproj/Localizable.strings index 09705640..e8d6aee9 100644 --- a/Source/Resources/Base.lproj/Localizable.strings +++ b/Source/Resources/Base.lproj/Localizable.strings @@ -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." = "Hold ⇧ to choose associates."; // The followings are the category names used in the Symbol menu. "catCommonSymbols" = "CommonSymbols"; diff --git a/Source/Resources/en.lproj/Localizable.strings b/Source/Resources/en.lproj/Localizable.strings index 09705640..e8d6aee9 100644 --- a/Source/Resources/en.lproj/Localizable.strings +++ b/Source/Resources/en.lproj/Localizable.strings @@ -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." = "Hold ⇧ to choose associates."; // The followings are the category names used in the Symbol menu. "catCommonSymbols" = "CommonSymbols"; diff --git a/Source/Resources/ja.lproj/Localizable.strings b/Source/Resources/ja.lproj/Localizable.strings index 3ce6f421..a442a596 100644 --- a/Source/Resources/ja.lproj/Localizable.strings +++ b/Source/Resources/ja.lproj/Localizable.strings @@ -82,6 +82,7 @@ "Optimize Memorized Phrases" = "臨時記憶資料を整う"; "Clear Memorized Phrases" = "臨時記憶資料を削除"; "Currency Numeral Output" = "数字大字変換"; +"Hold ⇧ to choose associates." = "⇧を押しながら連想候補をご選択ください。"; // The followings are the category names used in the Symbol menu. "catCommonSymbols" = "常用"; diff --git a/Source/Resources/zh-Hans.lproj/Localizable.strings b/Source/Resources/zh-Hans.lproj/Localizable.strings index a79cc57d..f9c74258 100644 --- a/Source/Resources/zh-Hans.lproj/Localizable.strings +++ b/Source/Resources/zh-Hans.lproj/Localizable.strings @@ -82,6 +82,7 @@ "Optimize Memorized Phrases" = "精简临时记忆语汇资料"; "Clear Memorized Phrases" = "清除临时记忆语汇资料"; "Currency Numeral Output" = "大写汉字数字输出"; +"Hold ⇧ to choose associates." = "摁住⇧以选取联想词。"; // The followings are the category names used in the Symbol menu. "catCommonSymbols" = "常用"; diff --git a/Source/Resources/zh-Hant.lproj/Localizable.strings b/Source/Resources/zh-Hant.lproj/Localizable.strings index 23a3f5db..350b2751 100644 --- a/Source/Resources/zh-Hant.lproj/Localizable.strings +++ b/Source/Resources/zh-Hant.lproj/Localizable.strings @@ -82,6 +82,7 @@ "Optimize Memorized Phrases" = "精簡臨時記憶語彙資料"; "Clear Memorized Phrases" = "清除臨時記憶語彙資料"; "Currency Numeral Output" = "大寫漢字數字輸出"; +"Hold ⇧ to choose associates." = "摁住⇧以選取聯想詞。"; // The followings are the category names used in the Symbol menu. "catCommonSymbols" = "常用";