forked from openvanilla/McBopomofo
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathInputState.swift
300 lines (255 loc) · 12 KB
/
InputState.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
import Cocoa
/// Represents the states for the input method controller.
///
/// An input method is actually a finite state machine. It receives the inputs
/// from hardware like keyboard and mouse, changes its state, updates user
/// interface by the state, and finally produces the text output and then them
/// to the client apps. It should be a one-way data flow, and the user interface
/// and text output should follow unconditionally one single data source.
///
/// The InputState class is for representing what the input controller is doing,
/// and the place to store the variables that could be used. For example, the
/// array for the candidate list is useful only when the user is choosing a
/// candidate, and the array should not exist when the input controller is in
/// another state.
///
/// They are immutable objects. When the state changes, the controller should
/// create a new state object to replace the current state instead of modifying
/// the existing one.
///
/// McBopomofo's input controller has following possible states:
///
/// - Deactivated: The user is not using McBopomofo yet.
/// - Empty: The user has switched to McBopomofo but did not input anything yet,
/// or, he or she has committed text into the client apps and starts a new
/// input phase.
/// - Committing: The input controller is sending text to the client apps.
/// - Inputting: The user has inputted something and the input buffer is
/// visible.
/// - Marking: The user is creating a area in the input buffer and about to
/// create a new user phrase.
/// - Choosing Candidate: The candidate window is open to let the user to choose
/// one among the candidates.
class InputState: NSObject {
/// Represents that the input controller is deactivated.
@objc (InputStateDeactivated)
class Deactivated: InputState {
override var description: String {
"<InputState.Deactivated>"
}
}
// MARK: -
/// Represents that the composing buffer is empty.
@objc (InputStateEmpty)
class Empty: InputState {
@objc var composingBuffer: String {
""
}
override var description: String {
"<InputState.Empty>"
}
}
// MARK: -
/// Represents that the composing buffer is empty.
@objc (InputStateEmptyIgnoringPreviousState)
class EmptyIgnoringPreviousState: InputState {
@objc var composingBuffer: String {
""
}
override var description: String {
"<InputState.EmptyIgnoringPreviousState>"
}
}
// MARK: -
/// Represents that the input controller is committing text into client app.
@objc (InputStateCommitting)
class Committing: InputState {
@objc private(set) var poppedText: String = ""
@objc convenience init(poppedText: String) {
self.init()
self.poppedText = poppedText
}
override var description: String {
"<InputState.Committing poppedText:\(poppedText)>"
}
}
// MARK: -
/// Represents that the composing buffer is not empty.
@objc (InputStateNotEmpty)
class NotEmpty: InputState {
@objc private(set) var composingBuffer: String
@objc private(set) var cursorIndex: UInt
@objc init(composingBuffer: String, cursorIndex: UInt) {
self.composingBuffer = composingBuffer
self.cursorIndex = cursorIndex
}
override var description: String {
"<InputState.NotEmpty, composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex)>"
}
}
// MARK: -
/// Represents that the user is inputting text.
@objc (InputStateInputting)
class Inputting: NotEmpty {
@objc var poppedText: String = ""
@objc var tooltip: String = ""
@objc override init(composingBuffer: String, cursorIndex: UInt) {
super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex)
}
@objc var attributedString: NSAttributedString {
let attributedSting = NSAttributedString(string: composingBuffer, attributes: [
.underlineStyle: NSUnderlineStyle.single.rawValue,
.markedClauseSegment: 0
])
return attributedSting
}
override var description: String {
"<InputState.Inputting, composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex)>, poppedText:\(poppedText)>"
}
}
// MARK: -
private let kMinMarkRangeLength = 2
private let kMaxMarkRangeLength = 6
/// Represents that the user is marking a range in the composing buffer.
@objc (InputStateMarking)
class Marking: NotEmpty {
@objc private(set) var markerIndex: UInt
@objc private(set) var markedRange: NSRange
@objc var tooltip: String {
if composingBuffer.count != readings.count {
return NSLocalizedString("There are special phrases in your text. We don't support adding new phrases in this case.", comment: "")
}
if Preferences.phraseReplacementEnabled {
return NSLocalizedString("Phrase replacement mode is on. Not suggested to add phrase in the mode.", comment: "")
}
if Preferences.chineseConversionStyle == 1 && Preferences.chineseConversionEnabled {
return NSLocalizedString("Model based Chinese conversion is on. Not suggested to add phrase in the mode.", comment: "")
}
if markedRange.length == 0 {
return ""
}
let text = (composingBuffer as NSString).substring(with: markedRange)
if markedRange.length < kMinMarkRangeLength {
return String(format: NSLocalizedString("You are now selecting \"%@\". You can add a phrase with two or more characters.", comment: ""), text)
} else if (markedRange.length > kMaxMarkRangeLength) {
return String(format: NSLocalizedString("You are now selecting \"%@\". A phrase cannot be longer than %d characters.", comment: ""), text, kMaxMarkRangeLength)
}
return String(format: NSLocalizedString("You are now selecting \"%@\". Press enter to add a new phrase.", comment: ""), text)
}
@objc var tooltipForInputting: String = ""
@objc private(set) var readings: [String]
@objc init(composingBuffer: String, cursorIndex: UInt, markerIndex: UInt, readings: [String]) {
self.markerIndex = markerIndex
let begin = min(cursorIndex, markerIndex)
let end = max(cursorIndex, markerIndex)
markedRange = NSMakeRange(Int(begin), Int(end - begin))
self.readings = readings
super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex)
}
@objc var attributedString: NSAttributedString {
let attributedSting = NSMutableAttributedString(string: composingBuffer)
let end = markedRange.location + markedRange.length
attributedSting.setAttributes([
.underlineStyle: NSUnderlineStyle.single.rawValue,
.markedClauseSegment: 0
], range: NSRange(location: 0, length: markedRange.location))
attributedSting.setAttributes([
.underlineStyle: NSUnderlineStyle.thick.rawValue,
.markedClauseSegment: 1
], range: markedRange)
attributedSting.setAttributes([
.underlineStyle: NSUnderlineStyle.single.rawValue,
.markedClauseSegment: 2
], range: NSRange(location: end,
length: (composingBuffer as NSString).length - end))
return attributedSting
}
override var description: String {
"<InputState.Marking, composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex), markedRange:\(markedRange)>"
}
@objc func convertToInputting() -> Inputting {
let state = Inputting(composingBuffer: composingBuffer, cursorIndex: cursorIndex)
state.tooltip = tooltipForInputting
return state
}
@objc var validToWrite: Bool {
/// McBopomofo allows users to input a string whose length differs
/// from the amount of Bopomofo readings. In this case, the range
/// in the composing buffer and the readings could not match, so
/// we disable the function to write user phrases in this case.
if composingBuffer.count != readings.count {
return false
}
return markedRange.length >= kMinMarkRangeLength &&
markedRange.length <= kMaxMarkRangeLength
}
@objc var userPhrase: String {
let text = (composingBuffer as NSString).substring(with: markedRange)
let exactBegin = StringUtils.convertToCharIndex(from: markedRange.location, in: composingBuffer)
let exactEnd = StringUtils.convertToCharIndex(from: markedRange.location + markedRange.length, in: composingBuffer)
let readings = readings[exactBegin..<exactEnd]
let joined = readings.joined(separator: "-")
return "\(text) \(joined)"
}
}
// MARK: -
/// Represents that the user is choosing in a candidates list.
@objc (InputStateChoosingCandidate)
class ChoosingCandidate: NotEmpty {
@objc private(set) var candidates: [String]
@objc private(set) var useVerticalMode: Bool
@objc init(composingBuffer: String, cursorIndex: UInt, candidates: [String], useVerticalMode: Bool) {
self.candidates = candidates
self.useVerticalMode = useVerticalMode
super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex)
}
@objc var attributedString: NSAttributedString {
let attributedSting = NSAttributedString(string: composingBuffer, attributes: [
.underlineStyle: NSUnderlineStyle.single.rawValue,
.markedClauseSegment: 0
])
return attributedSting
}
override var description: String {
"<InputState.ChoosingCandidate, candidates:\(candidates), useVerticalMode:\(useVerticalMode), composingBuffer:\(composingBuffer), cursorIndex:\(cursorIndex)>"
}
}
// MARK: -
/// Represents that the user is choosing in a candidates list
/// in the associated phrases mode.
@objc (InputStateAssociatedPhrases)
class AssociatedPhrases: InputState {
@objc private(set) var candidates: [String] = []
@objc private(set) var useVerticalMode: Bool = false
@objc init(candidates: [String], useVerticalMode: Bool) {
self.candidates = candidates
self.useVerticalMode = useVerticalMode
super.init()
}
override var description: String {
"<InputState.AssociatedPhrases, candidates:\(candidates), useVerticalMode:\(useVerticalMode)>"
}
}
}