Skip to content

Commit a5fadd7

Browse files
authored
feat: 予測入力のUI実装と、Zenzaiを利用しないPoCの追加 (#270)
* feat: 予測入力のUI実装と、Zenzaiを利用しないPoCの追加 * fix: reduce call of requestCandidate * feat: ヒステリシスを入れて入力時のちらつきを抑制
1 parent 36065c0 commit a5fadd7

File tree

8 files changed

+307
-6
lines changed

8 files changed

+307
-6
lines changed

Core/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ let package = Package(
2121
)
2222
],
2323
dependencies: [
24-
.package(url: "https://github.com/azooKey/AzooKeyKanaKanjiConverter", revision: "ecb569963d2969340cca81a3fe6840bdb9a0afc7", traits: kanaKanjiConverterTraits)
24+
.package(url: "https://github.com/azooKey/AzooKeyKanaKanjiConverter", revision: "44cfd766f3939ee4901e46b81dcee6ea48c2c7e9", traits: kanaKanjiConverterTraits)
2525
],
2626
targets: [
2727
.executableTarget(

Core/Sources/Core/Configs/BoolConfigItem.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ extension Config {
2626
static let `default` = false
2727
public static let key: String = "dev.ensan.inputmethod.azooKeyMac.preference.debug.enableDebugWindow"
2828
}
29+
/// 予測入力のデバッグ機能を有効化する設定
30+
public struct DebugPredictiveTyping: BoolConfigItem {
31+
public init() {}
32+
static let `default` = false
33+
public static let key: String = "dev.ensan.inputmethod.azooKeyMac.preference.debug.predictiveTyping"
34+
}
2935
/// ライブ変換を有効化する設定
3036
public struct LiveConversion: BoolConfigItem {
3137
public init() {}

Core/Sources/Core/InputUtils/Actions/ClientAction.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public enum ClientAction {
4949

5050
// PredictiveSuggestion
5151
case requestPredictiveSuggestion
52+
case acceptPredictionCandidate
5253

5354
// ReplaceSuggestion
5455
case requestReplaceSuggestion

Core/Sources/Core/InputUtils/InputState.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,10 @@ public enum InputState: Sendable, Hashable {
139139
case .ten:
140140
return (.submitHalfWidthRomanCandidate, .transition(.none))
141141
}
142-
case .forget, .tab:
142+
case .forget:
143143
return (.consume, .fallthrough)
144+
case .tab:
145+
return (.acceptPredictionCandidate, .fallthrough)
144146
case .英数:
145147
return (.selectInputLanguage(.english), .fallthrough)
146148
case .かな:

Core/Sources/Core/InputUtils/SegmentsManager.swift

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ public final class SegmentsManager {
4444
private var replaceSuggestions: [Candidate] = []
4545
private var suggestSelectionIndex: Int?
4646

47+
public struct PredictionCandidate: Sendable {
48+
public var displayText: String
49+
public var appendText: String
50+
}
51+
52+
private func candidateReading(_ candidate: Candidate) -> String {
53+
candidate.data.map(\.ruby).joined()
54+
}
55+
4756
private lazy var zenzaiPersonalizationMode: ConvertRequestOptions.ZenzaiMode.PersonalizationMode? = self.getZenzaiPersonalizationMode()
4857

4958
private func getZenzaiPersonalizationMode() -> ConvertRequestOptions.ZenzaiMode.PersonalizationMode? {
@@ -121,10 +130,15 @@ public final class SegmentsManager {
121130
}
122131
}
123132

124-
private func options(leftSideContext: String? = nil, requestRichCandidates: Bool = false) -> ConvertRequestOptions {
133+
private func options(
134+
leftSideContext: String?,
135+
requestRichCandidates: Bool,
136+
requireJapanesePrediction: ConvertRequestOptions.PredictionMode,
137+
requireEnglishPrediction: ConvertRequestOptions.PredictionMode
138+
) -> ConvertRequestOptions {
125139
.init(
126-
requireJapanesePrediction: false,
127-
requireEnglishPrediction: false,
140+
requireJapanesePrediction: requireJapanesePrediction,
141+
requireEnglishPrediction: requireEnglishPrediction,
128142
keyboardLanguage: .ja_JP,
129143
englishCandidateInRoman2KanaInput: false,
130144
fullWidthRomanCandidate: true,
@@ -361,7 +375,15 @@ public final class SegmentsManager {
361375

362376
let prefixComposingText = self.composingText.prefixToCursorPosition()
363377
let leftSideContext = forcedLeftSideContext ?? self.getCleanLeftSideContext(maxCount: 30)
364-
let result = self.kanaKanjiConverter.requestCandidates(prefixComposingText, options: options(leftSideContext: leftSideContext, requestRichCandidates: requestRichCandidates))
378+
let result = self.kanaKanjiConverter.requestCandidates(
379+
prefixComposingText,
380+
options: options(
381+
leftSideContext: leftSideContext,
382+
requestRichCandidates: requestRichCandidates,
383+
requireJapanesePrediction: Config.DebugPredictiveTyping().value ? .manualMix : .disabled,
384+
requireEnglishPrediction: Config.DebugPredictiveTyping().value ? .manualMix : .disabled
385+
)
386+
)
365387
self.rawCandidates = result
366388
}
367389

@@ -556,6 +578,52 @@ public final class SegmentsManager {
556578
suggestSelectionIndex = nil
557579
}
558580

581+
public func requestPredictionCandidates() -> [PredictionCandidate] {
582+
guard Config.DebugPredictiveTyping().value else {
583+
return []
584+
}
585+
586+
let target = self.composingText.convertTarget
587+
guard !target.isEmpty else {
588+
return []
589+
}
590+
591+
var matchTarget = target
592+
if let last = matchTarget.last,
593+
last.unicodeScalars.allSatisfy({ $0.isASCII && CharacterSet.letters.contains($0) }) {
594+
matchTarget.removeLast()
595+
}
596+
guard matchTarget.count >= 2 else {
597+
return []
598+
}
599+
matchTarget = matchTarget.toHiragana()
600+
601+
guard let rawCandidates else {
602+
return []
603+
}
604+
605+
for candidate in rawCandidates.predictionResults {
606+
let reading = candidateReading(candidate)
607+
guard !reading.isEmpty else {
608+
continue
609+
}
610+
let readingHiragana = reading.toHiragana()
611+
guard readingHiragana.hasPrefix(matchTarget) else {
612+
continue
613+
}
614+
guard matchTarget.count < readingHiragana.count else {
615+
continue
616+
}
617+
let appendText = String(readingHiragana.dropFirst(matchTarget.count))
618+
guard !appendText.isEmpty else {
619+
continue
620+
}
621+
return [.init(displayText: candidate.text, appendText: appendText)]
622+
}
623+
624+
return []
625+
}
626+
559627
// swiftlint:disable:next cyclomatic_complexity
560628
public func getCurrentMarkedText(inputState: InputState) -> MarkedText {
561629
switch inputState {

azooKeyMac/InputController/CandidateWindow/CandidateView.swift

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,54 @@ class CandidatesViewController: BaseCandidateViewController {
8080
}
8181
}
8282
}
83+
84+
class PredictionCandidatesViewController: BaseCandidateViewController {
85+
private let prefixTabStop: CGFloat = 24
86+
private let prefixSymbolName = "arrow.forward.to.line.compact"
87+
private let prefixFontSize: CGFloat = 12
88+
89+
override var numberOfVisibleRows: Int {
90+
min(3, self.tableView.numberOfRows)
91+
}
92+
93+
override internal func configureCellView(_ cell: CandidateTableCellView, forRow row: Int) {
94+
let candidateText = candidates[row].text
95+
let attributedString = NSMutableAttributedString()
96+
97+
let isSelected = currentSelectedRow == row
98+
let candidateColor = isSelected ? NSColor.white : NSColor.labelColor
99+
100+
if let symbol = NSImage(systemSymbolName: prefixSymbolName, accessibilityDescription: nil) {
101+
let config = NSImage.SymbolConfiguration(pointSize: prefixFontSize, weight: .regular)
102+
let configured = symbol.withSymbolConfiguration(config) ?? symbol
103+
let attachment = NSTextAttachment()
104+
attachment.image = configured
105+
attachment.bounds = NSRect(x: 0, y: -2, width: prefixFontSize + 2, height: prefixFontSize + 2)
106+
attributedString.append(NSAttributedString(attachment: attachment))
107+
}
108+
attributedString.append(NSAttributedString(string: "\t"))
109+
let candidateAttributed = NSAttributedString(
110+
string: candidateText,
111+
attributes: [
112+
.font: NSFont.systemFont(ofSize: 18),
113+
.foregroundColor: candidateColor
114+
]
115+
)
116+
attributedString.append(candidateAttributed)
117+
118+
let fullRange = NSRange(location: 0, length: attributedString.length)
119+
120+
let paragraphStyle = NSMutableParagraphStyle()
121+
paragraphStyle.tabStops = [
122+
NSTextTab(textAlignment: .left, location: prefixTabStop, options: [:])
123+
]
124+
paragraphStyle.defaultTabInterval = prefixTabStop
125+
attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: fullRange)
126+
127+
cell.candidateTextField.attributedStringValue = attributedString
128+
}
129+
130+
override func getWindowWidth(maxContentWidth: CGFloat) -> CGFloat {
131+
maxContentWidth + prefixTabStop + 20
132+
}
133+
}

0 commit comments

Comments
 (0)