1 | //============================================================================ |
2 | // |
3 | // SSSS tt lll lll |
4 | // SS SS tt ll ll |
5 | // SS tttttt eeee ll ll aaaa |
6 | // SSSS tt ee ee ll ll aa |
7 | // SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" |
8 | // SS SS tt ee ll ll aa aa |
9 | // SSSS ttt eeeee llll llll aaaaa |
10 | // |
11 | // Copyright (c) 1995-2019 by Bradford W. Mott, Stephen Anthony |
12 | // and the Stella Team |
13 | // |
14 | // See the file "License.txt" for information on usage and redistribution of |
15 | // this file, and for a DISCLAIMER OF ALL WARRANTIES. |
16 | //============================================================================ |
17 | |
18 | #include "Dialog.hxx" |
19 | #include "StellaKeys.hxx" |
20 | #include "FBSurface.hxx" |
21 | #include "Font.hxx" |
22 | #include "EditableWidget.hxx" |
23 | |
24 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
25 | EditableWidget::EditableWidget(GuiObject* boss, const GUI::Font& font, |
26 | int x, int y, int w, int h, const string& str) |
27 | : Widget(boss, font, x, y, w, h), |
28 | CommandSender(boss), |
29 | _editable(true), |
30 | _editString(str) |
31 | { |
32 | _caretVisible = false; |
33 | _caretTime = 0; |
34 | _caretPos = 0; |
35 | |
36 | _caretInverse = false; |
37 | |
38 | _editScrollOffset = 0; |
39 | |
40 | _bgcolor = kWidColor; |
41 | _bgcolorhi = kWidColor; |
42 | _bgcolorlo = kDlgColor; |
43 | _textcolor = kTextColor; |
44 | _textcolorhi = kTextColor; |
45 | |
46 | // By default, include all printable chars except quotes |
47 | _filter = [](char c) { return isprint(c) && c != '\"'; }; |
48 | } |
49 | |
50 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
51 | void EditableWidget::setText(const string& str, bool) |
52 | { |
53 | // Filter input string |
54 | _editString = "" ; |
55 | for(char c: str) |
56 | if(_filter(tolower(c))) |
57 | _editString.push_back(c); |
58 | |
59 | _caretPos = int(_editString.size()); |
60 | |
61 | _editScrollOffset = (_font.getStringWidth(_editString) - (getEditRect().w())); |
62 | if (_editScrollOffset < 0) |
63 | _editScrollOffset = 0; |
64 | |
65 | setDirty(); |
66 | } |
67 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
68 | void EditableWidget::setEditable(bool editable, bool hiliteBG) |
69 | { |
70 | _editable = editable; |
71 | if(_editable) |
72 | { |
73 | setFlags(Widget::FLAG_WANTS_RAWDATA | Widget::FLAG_RETAIN_FOCUS); |
74 | _bgcolor = kWidColor; |
75 | } |
76 | else |
77 | { |
78 | clearFlags(Widget::FLAG_WANTS_RAWDATA | Widget::FLAG_RETAIN_FOCUS); |
79 | _bgcolor = hiliteBG ? kBGColorHi : kWidColor; |
80 | } |
81 | } |
82 | |
83 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
84 | bool EditableWidget::tryInsertChar(char c, int pos) |
85 | { |
86 | if(_filter(tolower(c))) |
87 | { |
88 | _editString.insert(pos, 1, c); |
89 | return true; |
90 | } |
91 | return false; |
92 | } |
93 | |
94 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
95 | bool EditableWidget::handleText(char text) |
96 | { |
97 | if(!_editable) |
98 | return true; |
99 | |
100 | if(tryInsertChar(text, _caretPos)) |
101 | { |
102 | _caretPos++; |
103 | sendCommand(EditableWidget::kChangedCmd, 0, _id); |
104 | setDirty(); |
105 | return true; |
106 | } |
107 | return false; |
108 | } |
109 | |
110 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
111 | bool EditableWidget::handleKeyDown(StellaKey key, StellaMod mod) |
112 | { |
113 | if(!_editable) |
114 | return true; |
115 | |
116 | // Ignore all alt-mod keys |
117 | if(StellaModTest::isAlt(mod)) |
118 | return true; |
119 | |
120 | bool handled = true; |
121 | bool dirty = false; |
122 | |
123 | switch(key) |
124 | { |
125 | case KBDK_RETURN: |
126 | case KBDK_KP_ENTER: |
127 | // confirm edit and exit editmode |
128 | endEditMode(); |
129 | sendCommand(EditableWidget::kAcceptCmd, 0, _id); |
130 | dirty = true; |
131 | break; |
132 | |
133 | case KBDK_ESCAPE: |
134 | abortEditMode(); |
135 | sendCommand(EditableWidget::kCancelCmd, 0, _id); |
136 | dirty = true; |
137 | break; |
138 | |
139 | case KBDK_BACKSPACE: |
140 | dirty = killChar(-1); |
141 | if(dirty) sendCommand(EditableWidget::kChangedCmd, key, _id); |
142 | break; |
143 | |
144 | case KBDK_DELETE: |
145 | case KBDK_KP_PERIOD: |
146 | dirty = killChar(+1); |
147 | if(dirty) sendCommand(EditableWidget::kChangedCmd, key, _id); |
148 | break; |
149 | |
150 | case KBDK_LEFT: |
151 | if(StellaModTest::isControl(mod)) |
152 | dirty = specialKeys(key); |
153 | else if(_caretPos > 0) |
154 | dirty = setCaretPos(_caretPos - 1); |
155 | break; |
156 | |
157 | case KBDK_RIGHT: |
158 | if(StellaModTest::isControl(mod)) |
159 | dirty = specialKeys(key); |
160 | else if(_caretPos < int(_editString.size())) |
161 | dirty = setCaretPos(_caretPos + 1); |
162 | break; |
163 | |
164 | case KBDK_HOME: |
165 | dirty = setCaretPos(0); |
166 | break; |
167 | |
168 | case KBDK_END: |
169 | dirty = setCaretPos(int(_editString.size())); |
170 | break; |
171 | |
172 | default: |
173 | if (StellaModTest::isControl(mod)) |
174 | { |
175 | dirty = specialKeys(key); |
176 | } |
177 | else |
178 | handled = false; |
179 | } |
180 | |
181 | if (dirty) |
182 | setDirty(); |
183 | |
184 | return handled; |
185 | } |
186 | |
187 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
188 | int EditableWidget::getCaretOffset() const |
189 | { |
190 | int caretpos = 0; |
191 | for (int i = 0; i < _caretPos; i++) |
192 | caretpos += _font.getCharWidth(_editString[i]); |
193 | |
194 | caretpos -= _editScrollOffset; |
195 | |
196 | return caretpos; |
197 | } |
198 | |
199 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
200 | void EditableWidget::drawCaret() |
201 | { |
202 | // Only draw if item is visible |
203 | if (!_editable || !isVisible() || !_boss->isVisible() || !_hasFocus) |
204 | return; |
205 | |
206 | const Common::Rect& editRect = getEditRect(); |
207 | int x = editRect.x(); |
208 | int y = editRect.y(); |
209 | |
210 | x += getCaretOffset(); |
211 | |
212 | x += _x; |
213 | y += _y; |
214 | |
215 | FBSurface& s = _boss->dialog().surface(); |
216 | s.vLine(x, y+2, y + editRect.h() - 2, kTextColorHi); |
217 | } |
218 | |
219 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
220 | bool EditableWidget::setCaretPos(int newPos) |
221 | { |
222 | assert(newPos >= 0 && newPos <= int(_editString.size())); |
223 | _caretPos = newPos; |
224 | |
225 | return adjustOffset(); |
226 | } |
227 | |
228 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
229 | bool EditableWidget::adjustOffset() |
230 | { |
231 | // check if the caret is still within the textbox; if it isn't, |
232 | // adjust _editScrollOffset |
233 | |
234 | // For some reason (differences in ScummVM event handling??), |
235 | // this method should always return true. |
236 | |
237 | int caretpos = getCaretOffset(); |
238 | const int editWidth = getEditRect().w(); |
239 | |
240 | if (caretpos < 0) |
241 | { |
242 | // scroll left |
243 | _editScrollOffset += caretpos; |
244 | } |
245 | else if (caretpos >= editWidth) |
246 | { |
247 | // scroll right |
248 | _editScrollOffset -= (editWidth - caretpos); |
249 | } |
250 | else if (_editScrollOffset > 0) |
251 | { |
252 | const int strWidth = _font.getStringWidth(_editString); |
253 | if (strWidth - _editScrollOffset < editWidth) |
254 | { |
255 | // scroll right |
256 | _editScrollOffset = (strWidth - editWidth); |
257 | if (_editScrollOffset < 0) |
258 | _editScrollOffset = 0; |
259 | } |
260 | } |
261 | |
262 | return true; |
263 | } |
264 | |
265 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
266 | bool EditableWidget::specialKeys(StellaKey key) |
267 | { |
268 | bool handled = true; |
269 | |
270 | switch (key) |
271 | { |
272 | case KBDK_A: |
273 | setCaretPos(0); |
274 | break; |
275 | |
276 | case KBDK_C: |
277 | copySelectedText(); |
278 | if(handled) sendCommand(EditableWidget::kChangedCmd, key, _id); |
279 | break; |
280 | |
281 | case KBDK_E: |
282 | setCaretPos(int(_editString.size())); |
283 | break; |
284 | |
285 | case KBDK_D: |
286 | handled = killChar(+1); |
287 | if(handled) sendCommand(EditableWidget::kChangedCmd, key, _id); |
288 | break; |
289 | |
290 | case KBDK_K: |
291 | handled = killLine(+1); |
292 | if(handled) sendCommand(EditableWidget::kChangedCmd, key, _id); |
293 | break; |
294 | |
295 | case KBDK_U: |
296 | handled = killLine(-1); |
297 | if(handled) sendCommand(EditableWidget::kChangedCmd, key, _id); |
298 | break; |
299 | |
300 | case KBDK_V: |
301 | pasteSelectedText(); |
302 | if(handled) sendCommand(EditableWidget::kChangedCmd, key, _id); |
303 | break; |
304 | |
305 | case KBDK_W: |
306 | handled = killLastWord(); |
307 | if(handled) sendCommand(EditableWidget::kChangedCmd, key, _id); |
308 | break; |
309 | |
310 | case KBDK_LEFT: |
311 | handled = moveWord(-1); |
312 | break; |
313 | |
314 | case KBDK_RIGHT: |
315 | handled = moveWord(+1); |
316 | break; |
317 | |
318 | default: |
319 | handled = false; |
320 | } |
321 | |
322 | return handled; |
323 | } |
324 | |
325 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
326 | bool EditableWidget::killChar(int direction) |
327 | { |
328 | bool handled = false; |
329 | |
330 | if(direction == -1) // Delete previous character (backspace) |
331 | { |
332 | if(_caretPos > 0) |
333 | { |
334 | _caretPos--; |
335 | _editString.erase(_caretPos, 1); |
336 | handled = true; |
337 | } |
338 | } |
339 | else if(direction == 1) // Delete next character (delete) |
340 | { |
341 | _editString.erase(_caretPos, 1); |
342 | handled = true; |
343 | } |
344 | |
345 | return handled; |
346 | } |
347 | |
348 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
349 | bool EditableWidget::killLine(int direction) |
350 | { |
351 | bool handled = false; |
352 | |
353 | if(direction == -1) // erase from current position to beginning of line |
354 | { |
355 | int count = _caretPos; |
356 | if(count > 0) |
357 | { |
358 | for (int i = 0; i < count; i++) |
359 | killChar(-1); |
360 | |
361 | handled = true; |
362 | } |
363 | } |
364 | else if(direction == 1) // erase from current position to end of line |
365 | { |
366 | int count = int(_editString.size()) - _caretPos; |
367 | if(count > 0) |
368 | { |
369 | for (int i = 0; i < count; i++) |
370 | killChar(+1); |
371 | |
372 | handled = true; |
373 | } |
374 | } |
375 | |
376 | return handled; |
377 | } |
378 | |
379 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
380 | bool EditableWidget::killLastWord() |
381 | { |
382 | bool handled = false; |
383 | int count = 0, currentPos = _caretPos; |
384 | bool space = true; |
385 | while (currentPos > 0) |
386 | { |
387 | if (_editString[currentPos - 1] == ' ') |
388 | { |
389 | if (!space) |
390 | break; |
391 | } |
392 | else |
393 | space = false; |
394 | |
395 | currentPos--; |
396 | count++; |
397 | } |
398 | |
399 | if(count > 0) |
400 | { |
401 | for (int i = 0; i < count; i++) |
402 | killChar(-1); |
403 | |
404 | handled = true; |
405 | } |
406 | |
407 | return handled; |
408 | } |
409 | |
410 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
411 | bool EditableWidget::moveWord(int direction) |
412 | { |
413 | bool handled = false; |
414 | bool space = true; |
415 | int currentPos = _caretPos; |
416 | |
417 | if(direction == -1) // move to first character of previous word |
418 | { |
419 | while (currentPos > 0) |
420 | { |
421 | if (_editString[currentPos - 1] == ' ') |
422 | { |
423 | if (!space) |
424 | break; |
425 | } |
426 | else |
427 | space = false; |
428 | |
429 | currentPos--; |
430 | } |
431 | _caretPos = currentPos; |
432 | handled = true; |
433 | } |
434 | else if(direction == +1) // move to first character of next word |
435 | { |
436 | while (currentPos < int(_editString.size())) |
437 | { |
438 | if (_editString[currentPos - 1] == ' ') |
439 | { |
440 | if (!space) |
441 | break; |
442 | } |
443 | else |
444 | space = false; |
445 | |
446 | currentPos++; |
447 | } |
448 | _caretPos = currentPos; |
449 | handled = true; |
450 | } |
451 | |
452 | return handled; |
453 | } |
454 | |
455 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
456 | void EditableWidget::copySelectedText() |
457 | { |
458 | // _clippedText = _editString; |
459 | } |
460 | |
461 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
462 | void EditableWidget::pasteSelectedText() |
463 | { |
464 | // _editString = _clippedText; |
465 | } |
466 | |
467 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
468 | string EditableWidget::_clippedText = "" ; |
469 | |