From 030a8cb77628093bb5c22cd0b4e7c247bb6b48b7 Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Thu, 29 Sep 2022 21:22:51 +0800 Subject: [PATCH] CtlCandidateTDK // Vertical candidate layout support, etc. - SP2: Fix a color scheme mistake in bright mode. --- .../CandidateCellDataExtension.swift | 37 ++++ .../CandidateWindow/CandidatePool.swift | 166 +++++++++++--- .../CandidateWindow/CtlCandidate.swift | 8 + .../CandidateWindow/CtlCandidateTDK.swift | 122 ---------- .../INMUCandidateSuite/CtlCandidateTDK.swift | 209 ++++++++++++++++++ .../VwrCandidateHorizontal.swift} | 76 +++---- .../VwrCandidateVertical.swift | 109 +++++++++ .../CandidatePoolTests.swift | 6 +- .../Sources/Shared/CandidateBasicUnits.swift | 16 +- .../Protocols/CtlCandidateProtocol.swift | 2 + .../Modules/KeyHandler_HandleCandidate.swift | 49 +--- .../CandidateUI/IMKCandidatesImpl.swift | 12 + .../UIModules/PrefUI/suiPrefPaneGeneral.swift | 12 +- .../Resources/Base.lproj/Localizable.strings | 1 - Source/Resources/en.lproj/Localizable.strings | 1 - Source/Resources/ja.lproj/Localizable.strings | 3 +- .../zh-Hans.lproj/Localizable.strings | 1 - .../zh-Hant.lproj/Localizable.strings | 1 - 18 files changed, 573 insertions(+), 258 deletions(-) create mode 100644 Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidateCellDataExtension.swift delete mode 100644 Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CtlCandidateTDK.swift create mode 100644 Packages/vChewing_CandidateWindow/Sources/CandidateWindow/INMUCandidateSuite/CtlCandidateTDK.swift rename Packages/vChewing_CandidateWindow/Sources/CandidateWindow/{VwrCandidateTDK.swift => INMUCandidateSuite/VwrCandidateHorizontal.swift} (55%) create mode 100644 Packages/vChewing_CandidateWindow/Sources/CandidateWindow/INMUCandidateSuite/VwrCandidateVertical.swift diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidateCellDataExtension.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidateCellDataExtension.swift new file mode 100644 index 00000000..4016374c --- /dev/null +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidateCellDataExtension.swift @@ -0,0 +1,37 @@ +// (c) 2022 and onwards The vChewing Project (MIT-NTL License). +// ==================== +// This code is released under the MIT license (SPDX-License-Identifier: MIT) +// ... with NTL restriction stating that: +// No trademark license is granted to use the trade names, trademarks, service +// marks, or product names of Contributor, except as required to fulfill notice +// requirements defined in MIT License. + +import Shared +import SwiftUI + +@available(macOS 12, *) +extension CandidateCellData { + public var attributedStringForSwiftUI: some View { + var result: some View { + ZStack(alignment: .leading) { + if isSelected { + Color(nsColor: CandidateCellData.highlightBackground).ignoresSafeArea().cornerRadius(6) + } + VStack(spacing: 0) { + HStack(spacing: 4) { + if UserDefaults.standard.bool(forKey: UserDef.kHandleDefaultCandidateFontsByLangIdentifier.rawValue) { + Text(AttributedString(attributedStringHeader)).frame(width: CandidateCellData.unifiedSize / 2) + Text(AttributedString(attributedString)) + } else { + Text(key).font(.system(size: fontSizeKey).monospaced()) + .foregroundColor(.init(nsColor: fontColorKey)).lineLimit(1) + Text(displayedText).font(.system(size: fontSizeCandidate)) + .foregroundColor(.init(nsColor: fontColorCandidate)).lineLimit(1) + } + }.padding(4) + } + }.fixedSize(horizontal: false, vertical: true) + } + return result + } +} diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool.swift index e0a6fd23..a63d4a20 100644 --- a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool.swift +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool.swift @@ -12,53 +12,105 @@ import Shared /// 候選字窗會用到的資料池單位。 public class CandidatePool { public let blankCell = CandidateCellData(key: " ", displayedText: " ", isSelected: false) - public var currentRowNumber = 0 - public var maximumLinesPerPage = 3 + public private(set) var candidateDataAll: [CandidateCellData] = [] public private(set) var selectionKeys: String public private(set) var highlightedIndex: Int = 0 - public private(set) var maxColumnCapacity: Int = 6 - public private(set) var candidateDataAll: [CandidateCellData] = [] + + // 下述變數只有橫排選字窗才會用到 + public var currentRowNumber = 0 + public var maximumRowsPerPage = 3 + public private(set) var maxRowCapacity: Int = 6 public private(set) var candidateRows: [[CandidateCellData]] = [] - public var isVerticalLayout: Bool { maxColumnCapacity == 1 } - public var maxColumnWidth: Int { Int(Double(maxColumnCapacity + 3) * 2) * Int(ceil(CandidateCellData.unifiedSize)) } + + // 下述變數只有縱排選字窗才會用到 + public var currentColumnNumber = 0 + public var maximumColumnsPerPage = 3 + public private(set) var maxColumnCapacity: Int = 6 + public private(set) var candidateColumns: [[CandidateCellData]] = [] + + // 動態變數 + public var maxRowWidth: Int { Int(Double(maxRowCapacity + 3) * 2) * Int(ceil(CandidateCellData.unifiedSize)) } public var maxWindowWidth: Double { - ceil(Double(maxColumnCapacity + 3) * 2.7 * ceil(CandidateCellData.unifiedSize) * 1.2) + ceil(Double(maxRowCapacity + 3) * 2.7 * ceil(CandidateCellData.unifiedSize) * 1.2) } - public var rangeForCurrentPage: Range { - currentRowNumber.. { + currentRowNumber.. { 0..<(maximumLinesPerPage - rangeForCurrentPage.count) } + public var rangeForCurrentVerticalPage: Range { + currentColumnNumber.. { + 0..<(maximumRowsPerPage - rangeForCurrentHorizontalPage.count) + } + + public var rangeForLastVerticalPageBlanked: Range { + 0..<(maximumColumnsPerPage - rangeForCurrentVerticalPage.count) + } public enum VerticalDirection { case up case down } - /// 初期化一個候選字池。 + public enum HorizontalDirection { + case left + case right + } + + /// 初期化一個縱排候選字窗專用資料池。 /// - Parameters: /// - candidates: 要塞入的候選字詞陣列。 - /// - columnCapacity: (第一行的最大候選字詞數量, 陣列畫面展開之後的每一行的最大候選字詞數量)。 - public init(candidates: [String], columnCapacity: Int = 6, selectionKeys: String = "123456789", locale: String = "") { + /// - columnCapacity: (第一縱列的最大候選字詞數量, 陣列畫面展開之後的每一縱列的最大候選字詞數量)。 + /// - selectionKeys: 選字鍵。 + /// - locale: 區域編碼。例:「zh-Hans」或「zh-Hant」。 + public init(candidates: [String], columnCapacity: Int, selectionKeys: String = "123456789", locale: String = "") { maxColumnCapacity = max(1, columnCapacity) self.selectionKeys = selectionKeys candidateDataAll = candidates.map { .init(key: "0", displayedText: $0) } var currentColumn: [CandidateCellData] = [] for (i, candidate) in candidateDataAll.enumerated() { candidate.index = i - candidate.whichRow = candidateRows.count - let isOverflown: Bool = currentColumn.map(\.cellLength).reduce(0, +) + candidate.cellLength > maxColumnWidth - if isOverflown || currentColumn.count == maxColumnCapacity, !currentColumn.isEmpty { - candidateRows.append(currentColumn) + candidate.whichColumn = candidateColumns.count + if currentColumn.count == maxColumnCapacity, !currentColumn.isEmpty { + candidateColumns.append(currentColumn) currentColumn.removeAll() - candidate.whichRow += 1 + candidate.whichColumn += 1 } candidate.subIndex = currentColumn.count candidate.locale = locale currentColumn.append(candidate) } - candidateRows.append(currentColumn) + candidateColumns.append(currentColumn) + } + + /// 初期化一個橫排候選字窗專用資料池。 + /// - Parameters: + /// - candidates: 要塞入的候選字詞陣列。 + /// - rowCapacity: (第一橫行的最大候選字詞數量, 陣列畫面展開之後的每一橫行的最大候選字詞數量)。 + /// - selectionKeys: 選字鍵。 + /// - locale: 區域編碼。例:「zh-Hans」或「zh-Hant」。 + public init(candidates: [String], rowCapacity: Int, selectionKeys: String = "123456789", locale: String = "") { + maxRowCapacity = max(1, rowCapacity) + self.selectionKeys = selectionKeys + candidateDataAll = candidates.map { .init(key: "0", displayedText: $0) } + var currentRow: [CandidateCellData] = [] + for (i, candidate) in candidateDataAll.enumerated() { + candidate.index = i + candidate.whichRow = candidateRows.count + let isOverflown: Bool = currentRow.map(\.cellLength).reduce(0, +) + candidate.cellLength > maxRowWidth + if isOverflown || currentRow.count == maxRowCapacity, !currentRow.isEmpty { + candidateRows.append(currentRow) + currentRow.removeAll() + candidate.whichRow += 1 + } + candidate.subIndex = currentRow.count + candidate.locale = locale + currentRow.append(candidate) + } + candidateRows.append(currentRow) } public func selectNewNeighborRow(direction: VerticalDirection) { @@ -70,7 +122,7 @@ public class CandidatePool { if candidateRows.isEmpty { break } let firstRow = candidateRows[0] let newSubIndex = min(currentSubIndex, firstRow.count - 1) - highlight(at: firstRow[newSubIndex].index) + highlightHorizontal(at: firstRow[newSubIndex].index) break } if currentRowNumber >= candidateRows.count - 1 { currentRowNumber = candidateRows.count - 1 } @@ -80,13 +132,13 @@ public class CandidatePool { } let targetRow = candidateRows[currentRowNumber - 1] let newSubIndex = min(result, targetRow.count - 1) - highlight(at: targetRow[newSubIndex].index) + highlightHorizontal(at: targetRow[newSubIndex].index) case .down: if currentRowNumber >= candidateRows.count - 1 { if candidateRows.isEmpty { break } let finalRow = candidateRows[candidateRows.count - 1] let newSubIndex = min(currentSubIndex, finalRow.count - 1) - highlight(at: finalRow[newSubIndex].index) + highlightHorizontal(at: finalRow[newSubIndex].index) break } if candidateRows[currentRowNumber].count != candidateRows[currentRowNumber + 1].count { @@ -95,11 +147,40 @@ public class CandidatePool { } let targetRow = candidateRows[currentRowNumber + 1] let newSubIndex = min(result, targetRow.count - 1) - highlight(at: targetRow[newSubIndex].index) + highlightHorizontal(at: targetRow[newSubIndex].index) } } - public func highlight(at indexSpecified: Int) { + public func selectNewNeighborColumn(direction: HorizontalDirection) { + let currentSubIndex = candidateDataAll[highlightedIndex].subIndex + switch direction { + case .left: + if currentColumnNumber <= 0 { + if candidateColumns.isEmpty { break } + let firstColumn = candidateColumns[0] + let newSubIndex = min(currentSubIndex, firstColumn.count - 1) + highlightVertical(at: firstColumn[newSubIndex].index) + break + } + if currentColumnNumber >= candidateColumns.count - 1 { currentColumnNumber = candidateColumns.count - 1 } + let targetColumn = candidateColumns[currentColumnNumber - 1] + let newSubIndex = min(currentSubIndex, targetColumn.count - 1) + highlightVertical(at: targetColumn[newSubIndex].index) + case .right: + if currentColumnNumber >= candidateColumns.count - 1 { + if candidateColumns.isEmpty { break } + let finalColumn = candidateColumns[candidateColumns.count - 1] + let newSubIndex = min(currentSubIndex, finalColumn.count - 1) + highlightVertical(at: finalColumn[newSubIndex].index) + break + } + let targetColumn = candidateColumns[currentColumnNumber + 1] + let newSubIndex = min(currentSubIndex, targetColumn.count - 1) + highlightVertical(at: targetColumn[newSubIndex].index) + } + } + + public func highlightHorizontal(at indexSpecified: Int) { var indexSpecified = indexSpecified highlightedIndex = indexSpecified if !(0.. Bool { + false + } + + @discardableResult open func showPreviousLine() -> Bool { + false + } + @discardableResult open func highlightNextCandidate() -> Bool { false } diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CtlCandidateTDK.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CtlCandidateTDK.swift deleted file mode 100644 index f611393a..00000000 --- a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CtlCandidateTDK.swift +++ /dev/null @@ -1,122 +0,0 @@ -// (c) 2022 and onwards The vChewing Project (MIT-NTL License). -// ==================== -// This code is released under the MIT license (SPDX-License-Identifier: MIT) -// ... with NTL restriction stating that: -// No trademark license is granted to use the trade names, trademarks, service -// marks, or product names of Contributor, except as required to fulfill notice -// requirements defined in MIT License. - -import Cocoa -import CocoaExtension -import Shared -import SwiftUI - -@available(macOS 12, *) -public class CtlCandidateTDK: CtlCandidate { - public var thePool: CandidatePool = .init(candidates: []) - public var theView: VwrCandidateTDK { .init(controller: self, thePool: thePool, hint: hint) } - public required init(_ layout: NSUserInterfaceLayoutOrientation = .horizontal) { - var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0) - let styleMask: NSWindow.StyleMask = [.nonactivatingPanel] - let panel = NSPanel( - contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false - ) - panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 2) - panel.hasShadow = true - panel.isOpaque = false - panel.backgroundColor = NSColor.clear - - contentRect.origin = NSPoint.zero - - super.init(layout) - window = panel - currentLayout = layout - reloadData() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func reloadData() { - CandidateCellData.highlightBackground = highlightedColor() - CandidateCellData.unifiedSize = candidateFont.pointSize - guard let delegate = delegate else { return } - thePool = .init( - candidates: delegate.candidatePairs(conv: true).map(\.1), - selectionKeys: delegate.selectionKeys, locale: locale - ) - thePool.highlight(at: 0) - updateDisplay() - } - - override open func updateDisplay() { - DispatchQueue.main.async { [self] in - let newView = NSHostingView(rootView: theView.fixedSize()) - let newSize = newView.fittingSize - var newFrame = NSRect.zero - if let window = window { newFrame = window.frame } - newFrame.size = newSize - window?.setFrame(newFrame, display: false) - window?.contentView = NSHostingView(rootView: theView.fixedSize()) - window?.setContentSize(newSize) - } - } - - @discardableResult override public func showNextPage() -> Bool { - for _ in 0.. Bool { - for _ in 0.. Bool { - thePool.selectNewNeighborRow(direction: .down) - updateDisplay() - return true - } - - @discardableResult public func showPreviousLine() -> Bool { - thePool.selectNewNeighborRow(direction: .up) - updateDisplay() - return true - } - - @discardableResult override public func highlightNextCandidate() -> Bool { - thePool.highlight(at: thePool.highlightedIndex + 1) - updateDisplay() - return true - } - - @discardableResult override public func highlightPreviousCandidate() -> Bool { - thePool.highlight(at: thePool.highlightedIndex - 1) - updateDisplay() - return true - } - - override public func candidateIndexAtKeyLabelIndex(_ id: Int) -> Int { - let currentRow = thePool.candidateRows[thePool.currentRowNumber] - let actualID = max(0, min(id, currentRow.count - 1)) - return thePool.candidateRows[thePool.currentRowNumber][actualID].index - } - - override public var selectedCandidateIndex: Int { - get { - thePool.highlightedIndex - } - set { - thePool.highlight(at: newValue) - updateDisplay() - } - } -} diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/INMUCandidateSuite/CtlCandidateTDK.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/INMUCandidateSuite/CtlCandidateTDK.swift new file mode 100644 index 00000000..493517cb --- /dev/null +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/INMUCandidateSuite/CtlCandidateTDK.swift @@ -0,0 +1,209 @@ +// (c) 2022 and onwards The vChewing Project (MIT-NTL License). +// ==================== +// This code is released under the MIT license (SPDX-License-Identifier: MIT) +// ... with NTL restriction stating that: +// No trademark license is granted to use the trade names, trademarks, service +// marks, or product names of Contributor, except as required to fulfill notice +// requirements defined in MIT License. + +import Cocoa +import CocoaExtension +import Shared +import SwiftUI + +@available(macOS 12, *) +public class CtlCandidateTDK: CtlCandidate { + public var thePoolHorizontal: CandidatePool = .init(candidates: [], rowCapacity: 6) + public var theViewHorizontal: VwrCandidateHorizontal { + .init(controller: self, thePool: thePoolHorizontal, hint: hint) + } + + public var thePoolVertical: CandidatePool = .init(candidates: [], columnCapacity: 6) + public var theViewVertical: VwrCandidateVertical { .init(controller: self, thePool: thePoolVertical, hint: hint) } + public required init(_ layout: NSUserInterfaceLayoutOrientation = .horizontal) { + var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0) + let styleMask: NSWindow.StyleMask = [.nonactivatingPanel] + let panel = NSPanel( + contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false + ) + panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 2) + panel.hasShadow = true + panel.isOpaque = false + panel.backgroundColor = NSColor.clear + + contentRect.origin = NSPoint.zero + + super.init(layout) + window = panel + currentLayout = layout + reloadData() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func reloadData() { + CandidateCellData.highlightBackground = highlightedColor() + CandidateCellData.unifiedSize = candidateFont.pointSize + guard let delegate = delegate else { return } + switch currentLayout { + case .horizontal: + thePoolHorizontal = .init( + candidates: delegate.candidatePairs(conv: true).map(\.1), rowCapacity: 6, + selectionKeys: delegate.selectionKeys, locale: locale + ) + thePoolHorizontal.highlightHorizontal(at: 0) + case .vertical: + thePoolVertical = .init( + candidates: delegate.candidatePairs(conv: true).map(\.1), columnCapacity: 6, + selectionKeys: delegate.selectionKeys, locale: locale + ) + thePoolVertical.highlightVertical(at: 0) + @unknown default: + return + } + updateDisplay() + } + + override open func updateDisplay() { + switch currentLayout { + case .horizontal: + DispatchQueue.main.async { [self] in + let newView = NSHostingView(rootView: theViewHorizontal) + let newSize = newView.fittingSize + window?.contentView = newView + window?.setContentSize(newSize) + } + case .vertical: + DispatchQueue.main.async { [self] in + let newView = NSHostingView(rootView: theViewVertical) + let newSize = newView.fittingSize + window?.contentView = newView + window?.setContentSize(newSize) + } + @unknown default: + return + } + } + + @discardableResult override public func showNextPage() -> Bool { + switch currentLayout { + case .horizontal: + for _ in 0.. Bool { + switch currentLayout { + case .horizontal: + for _ in 0.. Bool { + switch currentLayout { + case .horizontal: + thePoolHorizontal.selectNewNeighborRow(direction: .down) + case .vertical: + thePoolVertical.selectNewNeighborColumn(direction: .right) + @unknown default: + return false + } + updateDisplay() + return true + } + + @discardableResult override public func showPreviousLine() -> Bool { + switch currentLayout { + case .horizontal: + thePoolHorizontal.selectNewNeighborRow(direction: .up) + case .vertical: + thePoolVertical.selectNewNeighborColumn(direction: .left) + @unknown default: + return false + } + updateDisplay() + return true + } + + @discardableResult override public func highlightNextCandidate() -> Bool { + switch currentLayout { + case .horizontal: + thePoolHorizontal.highlightHorizontal(at: thePoolHorizontal.highlightedIndex + 1) + case .vertical: + thePoolVertical.highlightVertical(at: thePoolVertical.highlightedIndex + 1) + @unknown default: + return false + } + updateDisplay() + return true + } + + @discardableResult override public func highlightPreviousCandidate() -> Bool { + switch currentLayout { + case .horizontal: + thePoolHorizontal.highlightHorizontal(at: thePoolHorizontal.highlightedIndex - 1) + case .vertical: + thePoolVertical.highlightVertical(at: thePoolVertical.highlightedIndex - 1) + @unknown default: + return false + } + updateDisplay() + return true + } + + override public func candidateIndexAtKeyLabelIndex(_ id: Int) -> Int { + switch currentLayout { + case .horizontal: + let currentRow = thePoolHorizontal.candidateRows[thePoolHorizontal.currentRowNumber] + let actualID = max(0, min(id, currentRow.count - 1)) + return thePoolHorizontal.candidateRows[thePoolHorizontal.currentRowNumber][actualID].index + case .vertical: + let currentColumn = thePoolVertical.candidateColumns[thePoolVertical.currentColumnNumber] + let actualID = max(0, min(id, currentColumn.count - 1)) + return thePoolVertical.candidateColumns[thePoolVertical.currentColumnNumber][actualID].index + @unknown default: + return 0 + } + } + + override public var selectedCandidateIndex: Int { + get { + switch currentLayout { + case .horizontal: return thePoolHorizontal.highlightedIndex + case .vertical: return thePoolVertical.highlightedIndex + @unknown default: return 0 + } + } + set { + switch currentLayout { + case .horizontal: thePoolHorizontal.highlightHorizontal(at: newValue) + case .vertical: thePoolVertical.highlightVertical(at: newValue) + @unknown default: return + } + updateDisplay() + } + } +} diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/VwrCandidateTDK.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/INMUCandidateSuite/VwrCandidateHorizontal.swift similarity index 55% rename from Packages/vChewing_CandidateWindow/Sources/CandidateWindow/VwrCandidateTDK.swift rename to Packages/vChewing_CandidateWindow/Sources/CandidateWindow/INMUCandidateSuite/VwrCandidateHorizontal.swift index 414d156c..ec36a61e 100644 --- a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/VwrCandidateTDK.swift +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/INMUCandidateSuite/VwrCandidateHorizontal.swift @@ -13,27 +13,28 @@ import SwiftUI // MARK: - Some useless tests @available(macOS 12, *) -struct CandidatePoolViewUI_Previews: PreviewProvider { +struct CandidatePoolViewUIHorizontal_Previews: PreviewProvider { @State static var testCandidates: [String] = [ "八月中秋山林涼", "八月中秋", "風吹大地", "山林涼", "草枝擺", "八月", "中秋", + "🐂🍺🐂🍺", "🐃🍺", "🐂🍺", "🐃🐂🍺🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺", "🐂🍺", "🐃🍺", "山林", "風吹", "大地", "草枝", "八", "月", "中", "秋", "山", "林", "涼", "風", "吹", "大", "地", "草", "枝", "擺", "八", "月", "中", "秋", "山", "林", "涼", "風", "吹", "大", "地", "草", "枝", "擺", ] static var thePool: CandidatePool { - let result = CandidatePool(candidates: testCandidates, columnCapacity: 6) + let result = CandidatePool(candidates: testCandidates, rowCapacity: 6) // 下一行待解決:無論這裡怎麼指定高亮選中項是哪一筆,其所在行都得被卷動到使用者眼前。 - result.highlight(at: 14) + result.highlightHorizontal(at: 5) return result } static var previews: some View { - VwrCandidateTDK(controller: .init(.horizontal), thePool: thePool).fixedSize() + VwrCandidateHorizontal(controller: .init(.horizontal), thePool: thePool).fixedSize() } } @available(macOS 12, *) -public struct VwrCandidateTDK: View { +public struct VwrCandidateHorizontal: View { public var controller: CtlCandidateTDK @State public var thePool: CandidatePool @State public var hint: String = "" @@ -52,26 +53,28 @@ public struct VwrCandidateTDK: View { VStack(alignment: .leading, spacing: 0) { ScrollView(.vertical, showsIndicators: true) { VStack(alignment: .leading, spacing: 1.6) { - ForEach(thePool.rangeForCurrentPage, id: \.self) { columnIndex in + ForEach(thePool.rangeForCurrentHorizontalPage, id: \.self) { rowIndex in HStack(spacing: 10) { - ForEach(Array(thePool.candidateRows[columnIndex]), id: \.self) { currentCandidate in + ForEach(Array(thePool.candidateRows[rowIndex]), id: \.self) { currentCandidate in currentCandidate.attributedStringForSwiftUI.fixedSize() - .frame(maxWidth: .infinity, alignment: .topLeading) + .frame( + maxWidth: .infinity, + alignment: .topLeading + ) .contentShape(Rectangle()) .onTapGesture { didSelectCandidateAt(currentCandidate.index) } } - Spacer() }.frame( minWidth: 0, maxWidth: .infinity, alignment: .topLeading - ).id(columnIndex) + ).id(rowIndex) Divider() } - if thePool.maximumLinesPerPage - thePool.rangeForCurrentPage.count > 0 { - ForEach(thePool.rangeForLastPageBlanked, id: \.self) { _ in + if thePool.maximumRowsPerPage - thePool.rangeForCurrentHorizontalPage.count > 0 { + ForEach(thePool.rangeForLastHorizontalPageBlanked, id: \.self) { _ in HStack(spacing: 0) { - thePool.blankCell.attributedStringForSwiftUI.fixedSize() + thePool.blankCell.attributedStringForSwiftUI .frame(maxWidth: .infinity, alignment: .topLeading) .contentShape(Rectangle()) Spacer() @@ -87,41 +90,20 @@ public struct VwrCandidateTDK: View { } .fixedSize(horizontal: false, vertical: true).padding(5) .background(Color(nsColor: NSColor.controlBackgroundColor).ignoresSafeArea()) - HStack(alignment: .bottom) { - Text(hint).font(.system(size: max(CandidateCellData.unifiedSize * 0.7, 11), weight: .bold)).lineLimit(1) - Spacer() - Text(positionLabel).font(.system(size: max(CandidateCellData.unifiedSize * 0.7, 11), weight: .bold)).lineLimit( - 1) - }.padding(6).foregroundColor(.init(nsColor: .controlTextColor)) - .shadow(color: .init(nsColor: .textBackgroundColor), radius: 1) + ZStack(alignment: .leading) { + Color(nsColor: hint.isEmpty ? .windowBackgroundColor : CandidateCellData.highlightBackground).ignoresSafeArea() + HStack(alignment: .bottom) { + Text(hint).font(.system(size: max(CandidateCellData.unifiedSize * 0.7, 11), weight: .bold)).lineLimit(1) + Spacer() + Text(positionLabel).font(.system(size: max(CandidateCellData.unifiedSize * 0.7, 11), weight: .bold)) + .lineLimit( + 1) + } + .padding(6).foregroundColor( + .init(nsColor: hint.isEmpty ? .controlTextColor : .selectedMenuItemTextColor.withAlphaComponent(0.9)) + ) + } } .frame(minWidth: thePool.maxWindowWidth, maxWidth: thePool.maxWindowWidth) } } - -@available(macOS 12, *) -extension CandidateCellData { - public var attributedStringForSwiftUI: some View { - var result: some View { - ZStack(alignment: .leading) { - if isSelected { - Color(nsColor: CandidateCellData.highlightBackground).ignoresSafeArea().cornerRadius(6) - } - VStack(spacing: 0) { - HStack(spacing: 4) { - if UserDefaults.standard.bool(forKey: UserDef.kHandleDefaultCandidateFontsByLangIdentifier.rawValue) { - Text(AttributedString(attributedStringHeader)).frame(width: CandidateCellData.unifiedSize / 2) - Text(AttributedString(attributedString)) - } else { - Text(key).font(.system(size: fontSizeKey).monospaced()) - .foregroundColor(.init(nsColor: fontColorKey)).lineLimit(1) - Text(displayedText).font(.system(size: fontSizeCandidate)) - .foregroundColor(.init(nsColor: fontColorCandidate)).lineLimit(1) - } - }.padding(4) - } - }.fixedSize(horizontal: false, vertical: true) - } - return result - } -} diff --git a/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/INMUCandidateSuite/VwrCandidateVertical.swift b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/INMUCandidateSuite/VwrCandidateVertical.swift new file mode 100644 index 00000000..8531bf40 --- /dev/null +++ b/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/INMUCandidateSuite/VwrCandidateVertical.swift @@ -0,0 +1,109 @@ +// (c) 2022 and onwards The vChewing Project (MIT-NTL License). +// ==================== +// This code is released under the MIT license (SPDX-License-Identifier: MIT) +// ... with NTL restriction stating that: +// No trademark license is granted to use the trade names, trademarks, service +// marks, or product names of Contributor, except as required to fulfill notice +// requirements defined in MIT License. + +import Cocoa +import Shared +import SwiftUI + +// MARK: - Some useless tests + +@available(macOS 12, *) +struct CandidatePoolViewUIVertical_Previews: PreviewProvider { + @State static var testCandidates: [String] = [ + "八月中秋山林涼", "八月中秋", "風吹大地", "山林涼", "草枝擺", "🐂🍺", "🐃🍺", "八月", "中秋", + "山林", "風吹", "大地", "草枝", "八", "月", "中", "秋", "山", "林", "涼", "風", + "吹", "大", "地", "草", "枝", "擺", "八", "月", "中", "秋", "山", "林", "涼", "風", + "吹", "大", "地", "草", "枝", "擺", + ] + static var thePool: CandidatePool { + let result = CandidatePool(candidates: testCandidates, columnCapacity: 6, selectionKeys: "123456789") + // 下一行待解決:無論這裡怎麼指定高亮選中項是哪一筆,其所在行都得被卷動到使用者眼前。 + result.highlightVertical(at: 5) + return result + } + + static var previews: some View { + VwrCandidateVertical(controller: .init(.horizontal), thePool: thePool).fixedSize() + } +} + +@available(macOS 12, *) +public struct VwrCandidateVertical: View { + public var controller: CtlCandidateTDK + @State public var thePool: CandidatePool + @State public var hint: String = "" + + private var positionLabel: String { + (thePool.highlightedIndex + 1).description + "/" + thePool.candidateDataAll.count.description + } + + private func didSelectCandidateAt(_ pos: Int) { + if let delegate = controller.delegate { + delegate.candidatePairSelected(at: pos) + } + } + + public var body: some View { + VStack(alignment: .leading, spacing: 0) { + ScrollView(.horizontal, showsIndicators: true) { + HStack(alignment: .top, spacing: 10) { + ForEach(thePool.rangeForCurrentVerticalPage, id: \.self) { columnIndex in + VStack(alignment: .leading, spacing: 0) { + ForEach(Array(thePool.candidateColumns[columnIndex]), id: \.self) { currentCandidate in + HStack(spacing: 0) { + currentCandidate.attributedStringForSwiftUI.fixedSize(horizontal: false, vertical: true) + .frame( + maxWidth: .infinity, + alignment: .topLeading + ) + .contentShape(Rectangle()) + .onTapGesture { didSelectCandidateAt(currentCandidate.index) } + } + } + }.frame( + minWidth: Double(CandidateCellData.unifiedSize * 5), + alignment: .topLeading + ).id(columnIndex) + Divider() + } + if thePool.maximumColumnsPerPage - thePool.rangeForCurrentVerticalPage.count > 0 { + ForEach(thePool.rangeForLastVerticalPageBlanked, id: \.self) { _ in + VStack(alignment: .leading, spacing: 0) { + ForEach(0.. Bool func showPreviousPage() -> Bool + func showNextLine() -> Bool + func showPreviousLine() -> Bool func highlightNextCandidate() -> Bool func highlightPreviousCandidate() -> Bool func candidateIndexAtKeyLabelIndex(_: Int) -> Int diff --git a/Source/Modules/KeyHandler_HandleCandidate.swift b/Source/Modules/KeyHandler_HandleCandidate.swift index d70e10d6..50759059 100644 --- a/Source/Modules/KeyHandler_HandleCandidate.swift +++ b/Source/Modules/KeyHandler_HandleCandidate.swift @@ -131,7 +131,7 @@ extension KeyHandler { errorCallback("1145148D") } case .vertical: - if !ctlCandidate.showPreviousPage() { + if !ctlCandidate.showPreviousLine() { errorCallback("1919810D") } @unknown default: @@ -149,7 +149,7 @@ extension KeyHandler { errorCallback("9B65138D") } case .vertical: - if !ctlCandidate.showNextPage() { + if !ctlCandidate.showNextLine() { errorCallback("9244908D") } @unknown default: @@ -163,21 +163,8 @@ extension KeyHandler { if input.isUp { switch ctlCandidate.currentLayout { case .horizontal: - if #available(macOS 12, *) { - if let ctlCandidate = ctlCandidate as? CtlCandidateTDK { - ctlCandidate.showPreviousLine() - break - } else { - if !ctlCandidate.showPreviousPage() { - errorCallback("9B614524") - break - } - } - } else { - if !ctlCandidate.showPreviousPage() { - errorCallback("9B614524") - break - } + if !ctlCandidate.showPreviousLine() { + errorCallback("9B614524") } case .vertical: if !ctlCandidate.highlightPreviousCandidate() { @@ -194,21 +181,9 @@ extension KeyHandler { if input.isDown { switch ctlCandidate.currentLayout { case .horizontal: - if #available(macOS 12, *) { - if let ctlCandidate = ctlCandidate as? CtlCandidateTDK { - ctlCandidate.showNextLine() - break - } else { - if !ctlCandidate.showNextPage() { - errorCallback("92B990DD") - break - } - } - } else { - if !ctlCandidate.showNextPage() { - errorCallback("92B990DD") - break - } + if !ctlCandidate.showNextLine() { + errorCallback("92B990DD") + break } case .vertical: if !ctlCandidate.highlightNextCandidate() { @@ -322,15 +297,7 @@ extension KeyHandler { if input.isSymbolMenuPhysicalKey { var updated = true - if #available(macOS 12, *) { - if let ctlCandidate = ctlCandidate as? CtlCandidateTDK { - updated = input.isShiftHold ? ctlCandidate.showPreviousLine() : ctlCandidate.showNextLine() - } else { - updated = input.isShiftHold ? ctlCandidate.showPreviousPage() : ctlCandidate.showNextPage() - } - } else { - updated = input.isShiftHold ? ctlCandidate.showPreviousPage() : ctlCandidate.showNextPage() - } + updated = input.isShiftHold ? ctlCandidate.showPreviousLine() : ctlCandidate.showNextLine() if !updated { errorCallback("66F3477B") } diff --git a/Source/Modules/UIModules/CandidateUI/IMKCandidatesImpl.swift b/Source/Modules/UIModules/CandidateUI/IMKCandidatesImpl.swift index 089f4b55..973fb49f 100644 --- a/Source/Modules/UIModules/CandidateUI/IMKCandidatesImpl.swift +++ b/Source/Modules/UIModules/CandidateUI/IMKCandidatesImpl.swift @@ -148,6 +148,18 @@ public class CtlCandidateIMK: IMKCandidates, CtlCandidateProtocol { return true } + // 該函式會影響 IMK 選字窗。 + public func showNextLine() -> Bool { + do { currentLayout == .vertical ? moveRight(self) : moveDown(self) } + return true + } + + // 該函式會影響 IMK 選字窗。 + public func showPreviousLine() -> Bool { + do { currentLayout == .vertical ? moveLeft(self) : moveUp(self) } + return true + } + public func candidateIndexAtKeyLabelIndex(_ index: Int) -> Int { guard let delegate = delegate else { return Int.max } let result = currentPageIndex * keyLabels.count + index diff --git a/Source/Modules/UIModules/PrefUI/suiPrefPaneGeneral.swift b/Source/Modules/UIModules/PrefUI/suiPrefPaneGeneral.swift index 379846b0..e99bdcd9 100644 --- a/Source/Modules/UIModules/PrefUI/suiPrefPaneGeneral.swift +++ b/Source/Modules/UIModules/PrefUI/suiPrefPaneGeneral.swift @@ -127,18 +127,8 @@ struct suiPrefPaneGeneral: View { .labelsHidden() .horizontalRadioGroupLayout() .pickerStyle(RadioGroupPickerStyle()) - .disabled(!PrefMgr.shared.useIMKCandidateWindow) - if PrefMgr.shared.useIMKCandidateWindow { - Text(LocalizedStringKey("Choose your preferred layout of the candidate window.")) - .preferenceDescription() - } else { - Text( - LocalizedStringKey( - "Tadokoro candidate window only supports horizontal grid view. Enable IMK candidate window in DevZone page first if you want to choose vertical candidate window." - ) - ) + Text(LocalizedStringKey("Choose your preferred layout of the candidate window.")) .preferenceDescription() - } } Preferences.Section(label: { Text(LocalizedStringKey("Output Settings:")) }) { Toggle( diff --git a/Source/Resources/Base.lproj/Localizable.strings b/Source/Resources/Base.lproj/Localizable.strings index 78778ced..95929ca2 100644 --- a/Source/Resources/Base.lproj/Localizable.strings +++ b/Source/Resources/Base.lproj/Localizable.strings @@ -216,7 +216,6 @@ "Specify the behavior of intonation key when syllable composer is empty." = "Specify the behavior of intonation key when syllable composer is empty."; "Starlight" = "Starlight"; "Stop farting (when typed phonetic combination is invalid, etc.)" = "Stop farting (when typed phonetic combination is invalid, etc.)"; -"Tadokoro candidate window only supports horizontal grid view. Enable IMK candidate window in DevZone page first if you want to choose vertical candidate window." = "Tadokoro candidate window only supports horizontal grid view. Enable IMK candidate window in DevZone page first if you want to choose vertical candidate window."; "This only works with Tadokoro candidate window." = "This only works with Tadokoro candidate window."; "Traditional Chinese" = "Traditional Chinese"; "Trim unfinished readings on commit" = "Trim unfinished readings on commit"; diff --git a/Source/Resources/en.lproj/Localizable.strings b/Source/Resources/en.lproj/Localizable.strings index 78778ced..95929ca2 100644 --- a/Source/Resources/en.lproj/Localizable.strings +++ b/Source/Resources/en.lproj/Localizable.strings @@ -216,7 +216,6 @@ "Specify the behavior of intonation key when syllable composer is empty." = "Specify the behavior of intonation key when syllable composer is empty."; "Starlight" = "Starlight"; "Stop farting (when typed phonetic combination is invalid, etc.)" = "Stop farting (when typed phonetic combination is invalid, etc.)"; -"Tadokoro candidate window only supports horizontal grid view. Enable IMK candidate window in DevZone page first if you want to choose vertical candidate window." = "Tadokoro candidate window only supports horizontal grid view. Enable IMK candidate window in DevZone page first if you want to choose vertical candidate window."; "This only works with Tadokoro candidate window." = "This only works with Tadokoro candidate window."; "Traditional Chinese" = "Traditional Chinese"; "Trim unfinished readings on commit" = "Trim unfinished readings on commit"; diff --git a/Source/Resources/ja.lproj/Localizable.strings b/Source/Resources/ja.lproj/Localizable.strings index aad16d7c..10d56374 100644 --- a/Source/Resources/ja.lproj/Localizable.strings +++ b/Source/Resources/ja.lproj/Localizable.strings @@ -81,7 +81,7 @@ "Optimize Memorized Phrases" = "臨時記憶資料を整う"; "Clear Memorized Phrases" = "臨時記憶資料を削除"; "Currency Numeral Output" = "数字大字変換"; -"Hold ⇧ to choose associates." = "⇧を押しながら連想候補をご選択ください。"; +"Hold ⇧ to choose associates." = "⇧を押しながら連想候補を選択。"; // The followings are the category names used in the Symbol menu. "catCommonSymbols" = "常用"; @@ -216,7 +216,6 @@ "Specify the behavior of intonation key when syllable composer is empty." = "音読組立緩衝列が空かされた時の音調キーの行為をご指定ください。"; "Starlight" = "星光配列"; "Stop farting (when typed phonetic combination is invalid, etc.)" = "マナーモード // 外すと入力間違った時に変な声が出る"; -"Tadokoro candidate window only supports horizontal grid view. Enable IMK candidate window in DevZone page first if you want to choose vertical candidate window." = "田所候補陳列ウィンドウは格子状横型陳列しかできません。開発道場のページで IMK 候補陳列ウィンドウを起用してから、今のこのページで縦型・横型陳列の選択はできます。"; "This only works with Tadokoro candidate window." = "これは田所候補陳列ウィンドウだけに効ける機能である。"; "Traditional Chinese" = "繁体中国語"; "Trim unfinished readings on commit" = "送り出す緩衝列内容から未完成な音読みを除く"; diff --git a/Source/Resources/zh-Hans.lproj/Localizable.strings b/Source/Resources/zh-Hans.lproj/Localizable.strings index dd386d41..6b3fff45 100644 --- a/Source/Resources/zh-Hans.lproj/Localizable.strings +++ b/Source/Resources/zh-Hans.lproj/Localizable.strings @@ -216,7 +216,6 @@ "Specify the behavior of intonation key when syllable composer is empty." = "指定声调键(在注拼槽为「空」状态时)的行为。"; "Starlight" = "星光排列"; "Stop farting (when typed phonetic combination is invalid, etc.)" = "廉耻模式 // 取消勾选的话,敲错字时会有异音"; -"Tadokoro candidate window only supports horizontal grid view. Enable IMK candidate window in DevZone page first if you want to choose vertical candidate window." = "田所选字窗仅支援横排矩阵布局模式。如欲使用纵排布局模式者,请在开发道场内先启用 IMK 选字窗。"; "This only works with Tadokoro candidate window." = "该方法仅对田所选字窗起作用。"; "Traditional Chinese" = "繁体中文"; "Trim unfinished readings on commit" = "在递交时清理未完成拼写的读音"; diff --git a/Source/Resources/zh-Hant.lproj/Localizable.strings b/Source/Resources/zh-Hant.lproj/Localizable.strings index 79553cd3..07a09382 100644 --- a/Source/Resources/zh-Hant.lproj/Localizable.strings +++ b/Source/Resources/zh-Hant.lproj/Localizable.strings @@ -216,7 +216,6 @@ "Specify the behavior of intonation key when syllable composer is empty." = "指定聲調鍵(在注拼槽為「空」狀態時)的行為。"; "Starlight" = "星光排列"; "Stop farting (when typed phonetic combination is invalid, etc.)" = "廉恥模式 // 取消勾選的話,敲錯字時會有異音"; -"Tadokoro candidate window only supports horizontal grid view. Enable IMK candidate window in DevZone page first if you want to choose vertical candidate window." = "田所選字窗僅支援橫排矩陣佈局模式。如欲使用縱排佈局模式者,請在開發道場內先啟用 IMK 選字窗。"; "This only works with Tadokoro candidate window." = "該方法僅對田所選字窗起作用。"; "Traditional Chinese" = "繁體中文"; "Trim unfinished readings on commit" = "在遞交時清理未完成拼寫的讀音";