1 | // Aseprite UI Library |
2 | // Copyright (C) 2018-2022 Igara Studio S.A. |
3 | // Copyright (C) 2001-2018 David Capello |
4 | // |
5 | // This file is released under the terms of the MIT license. |
6 | // Read LICENSE.txt for more information. |
7 | |
8 | #ifdef HAVE_CONFIG_H |
9 | #include "config.h" |
10 | #endif |
11 | |
12 | #include "ui/entry.h" |
13 | |
14 | #include "base/string.h" |
15 | #include "os/draw_text.h" |
16 | #include "os/font.h" |
17 | #include "os/system.h" |
18 | #include "ui/display.h" |
19 | #include "ui/menu.h" |
20 | #include "ui/message.h" |
21 | #include "ui/scale.h" |
22 | #include "ui/size_hint_event.h" |
23 | #include "ui/system.h" |
24 | #include "ui/theme.h" |
25 | #include "ui/timer.h" |
26 | #include "ui/widget.h" |
27 | |
28 | #include <algorithm> |
29 | #include <cctype> |
30 | #include <cstdarg> |
31 | #include <cstdio> |
32 | #include <memory> |
33 | |
34 | namespace ui { |
35 | |
36 | // Shared timer between all entries. |
37 | static std::unique_ptr<Timer> s_timer; |
38 | |
39 | static inline bool is_word_char(int ch) { |
40 | return (ch && !std::isspace(ch) && !std::ispunct(ch)); |
41 | } |
42 | |
43 | Entry::Entry(const int maxsize, const char* format, ...) |
44 | : Widget(kEntryWidget) |
45 | , m_maxsize(maxsize) |
46 | , m_caret(0) |
47 | , m_scroll(0) |
48 | , m_select(0) |
49 | , m_hidden(false) |
50 | , m_state(false) |
51 | , m_readonly(false) |
52 | , m_recent_focused(false) |
53 | , m_lock_selection(false) |
54 | , m_translate_dead_keys(true) |
55 | { |
56 | enableFlags(CTRL_RIGHT_CLICK); |
57 | |
58 | // formatted string |
59 | char buf[4096]; // TODO buffer overflow |
60 | if (format) { |
61 | va_list ap; |
62 | va_start(ap, format); |
63 | vsprintf(buf, format, ap); |
64 | va_end(ap); |
65 | } |
66 | // empty string |
67 | else { |
68 | buf[0] = 0; |
69 | } |
70 | |
71 | // TODO support for text alignment and multi-line |
72 | // widget->align = LEFT | MIDDLE; |
73 | setText(buf); |
74 | |
75 | setFocusStop(true); |
76 | initTheme(); |
77 | } |
78 | |
79 | Entry::~Entry() |
80 | { |
81 | stopTimer(); |
82 | } |
83 | |
84 | void Entry::setMaxTextLength(const int maxsize) |
85 | { |
86 | m_maxsize = maxsize; |
87 | } |
88 | |
89 | bool Entry::isReadOnly() const |
90 | { |
91 | return m_readonly; |
92 | } |
93 | |
94 | void Entry::setReadOnly(bool state) |
95 | { |
96 | m_readonly = state; |
97 | } |
98 | |
99 | void Entry::showCaret() |
100 | { |
101 | m_hidden = false; |
102 | if (shouldStartTimer(hasFocus())) |
103 | startTimer(); |
104 | invalidate(); |
105 | } |
106 | |
107 | void Entry::hideCaret() |
108 | { |
109 | m_hidden = true; |
110 | stopTimer(); |
111 | invalidate(); |
112 | } |
113 | |
114 | int Entry::lastCaretPos() const |
115 | { |
116 | return int(m_boxes.size()-1); |
117 | } |
118 | |
119 | void Entry::setCaretPos(int pos) |
120 | { |
121 | gfx::Size caretSize = theme()->getEntryCaretSize(this); |
122 | int textlen = lastCaretPos(); |
123 | m_caret = std::clamp(pos, 0, textlen); |
124 | m_scroll = std::clamp(m_scroll, 0, textlen); |
125 | |
126 | // Backward scroll |
127 | if (m_caret < m_scroll) |
128 | m_scroll = m_caret; |
129 | // Forward scroll |
130 | else if (m_caret > m_scroll) { |
131 | int xLimit = bounds().x2() - border().right(); |
132 | while (m_caret > m_scroll) { |
133 | int segmentWidth = 0; |
134 | for (int j=m_scroll; j<m_caret; ++j) |
135 | segmentWidth += m_boxes[j].width; |
136 | |
137 | int x = bounds().x + border().left() + segmentWidth + caretSize.w; |
138 | if (x < xLimit) |
139 | break; |
140 | else |
141 | ++m_scroll; |
142 | } |
143 | } |
144 | |
145 | if (shouldStartTimer(hasFocus())) |
146 | startTimer(); |
147 | m_state = true; |
148 | |
149 | invalidate(); |
150 | } |
151 | |
152 | void Entry::setCaretToEnd() |
153 | { |
154 | int end = lastCaretPos(); |
155 | selectText(end, end); |
156 | } |
157 | |
158 | void Entry::selectText(int from, int to) |
159 | { |
160 | int end = lastCaretPos(); |
161 | |
162 | m_select = from; |
163 | setCaretPos(from); // to move scroll |
164 | setCaretPos((to >= 0)? to: end+to+1); |
165 | |
166 | invalidate(); |
167 | } |
168 | |
169 | void Entry::selectAllText() |
170 | { |
171 | selectText(0, -1); |
172 | } |
173 | |
174 | void Entry::deselectText() |
175 | { |
176 | m_select = -1; |
177 | invalidate(); |
178 | } |
179 | |
180 | std::string Entry::selectedText() const |
181 | { |
182 | Range range = selectedRange(); |
183 | if (!range.isEmpty()) |
184 | return text().substr(m_boxes[range.from].from, |
185 | m_boxes[range.to-1].to - m_boxes[range.from].from); |
186 | else |
187 | return std::string(); |
188 | } |
189 | |
190 | Entry::Range Entry::selectedRange() const |
191 | { |
192 | Range range; |
193 | if ((m_select >= 0) && |
194 | (m_caret != m_select)) { |
195 | range.from = std::min(m_caret, m_select); |
196 | range.to = std::max(m_caret, m_select); |
197 | |
198 | ASSERT(range.from >= 0 && range.from < int(m_boxes.size())); |
199 | ASSERT(range.to >= 0 && range.to <= int(m_boxes.size())); |
200 | } |
201 | return range; |
202 | } |
203 | |
204 | void Entry::setSuffix(const std::string& suffix) |
205 | { |
206 | // No-op cases |
207 | if ((!m_suffix && suffix.empty()) || |
208 | (m_suffix && *m_suffix == suffix)) |
209 | return; |
210 | |
211 | m_suffix = std::make_unique<std::string>(suffix); |
212 | invalidate(); |
213 | } |
214 | |
215 | std::string Entry::getSuffix() |
216 | { |
217 | return (m_suffix ? *m_suffix: std::string()); |
218 | } |
219 | |
220 | void Entry::setTranslateDeadKeys(bool state) |
221 | { |
222 | m_translate_dead_keys = state; |
223 | } |
224 | |
225 | void Entry::getEntryThemeInfo(int* scroll, int* caret, int* state, Range* range) const |
226 | { |
227 | if (scroll) *scroll = m_scroll; |
228 | if (caret) *caret = m_caret; |
229 | if (state) *state = !m_hidden && m_state; |
230 | if (range) *range = selectedRange(); |
231 | } |
232 | |
233 | gfx::Rect Entry::getEntryTextBounds() const |
234 | { |
235 | return onGetEntryTextBounds(); |
236 | } |
237 | |
238 | bool Entry::onProcessMessage(Message* msg) |
239 | { |
240 | switch (msg->type()) { |
241 | |
242 | case kTimerMessage: |
243 | if (hasFocus() && static_cast<TimerMessage*>(msg)->timer() == s_timer.get()) { |
244 | // Blinking caret |
245 | m_state = (m_state ? false: true); |
246 | invalidate(); |
247 | } |
248 | break; |
249 | |
250 | case kFocusEnterMessage: |
251 | if (shouldStartTimer(true)) |
252 | startTimer(); |
253 | |
254 | m_state = true; |
255 | invalidate(); |
256 | |
257 | if (m_lock_selection) { |
258 | m_lock_selection = false; |
259 | } |
260 | else { |
261 | selectAllText(); |
262 | m_recent_focused = true; |
263 | } |
264 | |
265 | // Start processing dead keys |
266 | if (m_translate_dead_keys) |
267 | os::instance()->setTranslateDeadKeys(true); |
268 | break; |
269 | |
270 | case kFocusLeaveMessage: |
271 | invalidate(); |
272 | |
273 | stopTimer(); |
274 | |
275 | if (!m_lock_selection) |
276 | deselectText(); |
277 | |
278 | m_recent_focused = false; |
279 | |
280 | // Stop processing dead keys |
281 | if (m_translate_dead_keys) |
282 | os::instance()->setTranslateDeadKeys(false); |
283 | break; |
284 | |
285 | case kKeyDownMessage: |
286 | if (hasFocus() && !isReadOnly()) { |
287 | // Command to execute |
288 | EntryCmd cmd = EntryCmd::NoOp; |
289 | KeyMessage* keymsg = static_cast<KeyMessage*>(msg); |
290 | KeyScancode scancode = keymsg->scancode(); |
291 | |
292 | switch (scancode) { |
293 | |
294 | case kKeyLeft: |
295 | if (msg->ctrlPressed() || msg->altPressed()) |
296 | cmd = EntryCmd::BackwardWord; |
297 | else if (msg->cmdPressed()) |
298 | cmd = EntryCmd::BeginningOfLine; |
299 | else |
300 | cmd = EntryCmd::BackwardChar; |
301 | break; |
302 | |
303 | case kKeyRight: |
304 | if (msg->ctrlPressed() || msg->altPressed()) |
305 | cmd = EntryCmd::ForwardWord; |
306 | else if (msg->cmdPressed()) |
307 | cmd = EntryCmd::EndOfLine; |
308 | else |
309 | cmd = EntryCmd::ForwardChar; |
310 | break; |
311 | |
312 | case kKeyHome: |
313 | cmd = EntryCmd::BeginningOfLine; |
314 | break; |
315 | |
316 | case kKeyEnd: |
317 | cmd = EntryCmd::EndOfLine; |
318 | break; |
319 | |
320 | case kKeyDel: |
321 | if (msg->shiftPressed()) |
322 | cmd = EntryCmd::Cut; |
323 | else if (msg->ctrlPressed()) |
324 | cmd = EntryCmd::DeleteForwardToEndOfLine; |
325 | else |
326 | cmd = EntryCmd::DeleteForward; |
327 | break; |
328 | |
329 | case kKeyInsert: |
330 | if (msg->shiftPressed()) |
331 | cmd = EntryCmd::Paste; |
332 | else if (msg->ctrlPressed()) |
333 | cmd = EntryCmd::Copy; |
334 | break; |
335 | |
336 | case kKeyBackspace: |
337 | if (msg->ctrlPressed() || msg->altPressed()) |
338 | cmd = EntryCmd::DeleteBackwardWord; |
339 | else |
340 | cmd = EntryCmd::DeleteBackward; |
341 | break; |
342 | |
343 | default: |
344 | // Map common macOS/Windows shortcuts for Cut/Copy/Paste/Select all |
345 | #if defined __APPLE__ |
346 | if (msg->onlyCmdPressed()) |
347 | #else |
348 | if (msg->onlyCtrlPressed()) |
349 | #endif |
350 | { |
351 | switch (scancode) { |
352 | case kKeyX: cmd = EntryCmd::Cut; break; |
353 | case kKeyC: cmd = EntryCmd::Copy; break; |
354 | case kKeyV: cmd = EntryCmd::Paste; break; |
355 | case kKeyA: cmd = EntryCmd::SelectAll; break; |
356 | } |
357 | } |
358 | break; |
359 | } |
360 | |
361 | if (cmd == EntryCmd::NoOp) { |
362 | if (keymsg->unicodeChar() >= 32) { |
363 | executeCmd(EntryCmd::InsertChar, keymsg->unicodeChar(), |
364 | (msg->shiftPressed()) ? true: false); |
365 | |
366 | // Select dead-key |
367 | if (keymsg->isDeadKey()) { |
368 | if (lastCaretPos() < m_maxsize) |
369 | selectText(m_caret-1, m_caret); |
370 | } |
371 | return true; |
372 | } |
373 | // Consume all key down of modifiers only, e.g. so the user |
374 | // can press first "Ctrl" key, and then "Ctrl+C" |
375 | // combination. |
376 | else if (keymsg->scancode() >= kKeyFirstModifierScancode) { |
377 | return true; |
378 | } |
379 | else { |
380 | break; // Propagate to manager |
381 | } |
382 | } |
383 | |
384 | executeCmd(cmd, keymsg->unicodeChar(), |
385 | (msg->shiftPressed()) ? true: false); |
386 | return true; |
387 | } |
388 | break; |
389 | |
390 | case kMouseDownMessage: |
391 | captureMouse(); |
392 | |
393 | // Disable selecting words if we click again (only |
394 | // double-clicking enables selecting words again). |
395 | if (!m_selecting_words.isEmpty()) |
396 | m_selecting_words.reset(); |
397 | |
398 | [[fallthrough]]; |
399 | |
400 | case kMouseMoveMessage: |
401 | if (hasCapture()) { |
402 | bool is_dirty = false; |
403 | int c = getCaretFromMouse(static_cast<MouseMessage*>(msg)); |
404 | |
405 | if (static_cast<MouseMessage*>(msg)->left() || !isPosInSelection(c)) { |
406 | // Move caret |
407 | if (m_caret != c) { |
408 | setCaretPos(c); |
409 | is_dirty = true; |
410 | invalidate(); |
411 | } |
412 | |
413 | // Move selection |
414 | if (m_recent_focused) { |
415 | m_recent_focused = false; |
416 | m_select = m_caret; |
417 | } |
418 | // Deselect |
419 | else if (msg->type() == kMouseDownMessage) { |
420 | m_select = m_caret; |
421 | } |
422 | // Continue selecting words |
423 | else if (!m_selecting_words.isEmpty()) { |
424 | Range toWord = wordRange(m_caret); |
425 | if (toWord.from < m_selecting_words.from) { |
426 | m_select = std::max(m_selecting_words.to, toWord.to); |
427 | setCaretPos(std::min(m_selecting_words.from, toWord.from)); |
428 | } |
429 | else { |
430 | m_select = std::min(m_selecting_words.from, toWord.from); |
431 | setCaretPos(std::max(m_selecting_words.to, toWord.to)); |
432 | } |
433 | } |
434 | } |
435 | |
436 | // Show the caret |
437 | if (is_dirty) { |
438 | if (shouldStartTimer(true)) |
439 | startTimer(); |
440 | m_state = true; |
441 | } |
442 | |
443 | return true; |
444 | } |
445 | break; |
446 | |
447 | case kMouseUpMessage: |
448 | if (hasCapture()) { |
449 | releaseMouse(); |
450 | |
451 | if (!m_selecting_words.isEmpty()) |
452 | m_selecting_words.reset(); |
453 | |
454 | MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg); |
455 | if (mouseMsg->right()) { |
456 | // This flag is disabled in kFocusEnterMessage message handler. |
457 | m_lock_selection = true; |
458 | |
459 | showEditPopupMenu(mouseMsg->position()); |
460 | requestFocus(); |
461 | } |
462 | } |
463 | return true; |
464 | |
465 | case kDoubleClickMessage: |
466 | if (!hasFocus()) |
467 | requestFocus(); |
468 | |
469 | m_selecting_words = wordRange(m_caret); |
470 | selectText(m_selecting_words.from, m_selecting_words.to); |
471 | |
472 | // Capture mouse to continue selecting words on kMouseMoveMessage |
473 | captureMouse(); |
474 | return true; |
475 | |
476 | case kMouseEnterMessage: |
477 | case kMouseLeaveMessage: |
478 | // TODO theme stuff |
479 | if (isEnabled()) |
480 | invalidate(); |
481 | break; |
482 | } |
483 | |
484 | return Widget::onProcessMessage(msg); |
485 | } |
486 | |
487 | // static |
488 | gfx::Size Entry::sizeHintWithText(Entry* entry, |
489 | const std::string& text) |
490 | { |
491 | int w = |
492 | entry->font()->textLength(text) + |
493 | + 2*entry->theme()->getEntryCaretSize(entry).w |
494 | + entry->border().width(); |
495 | |
496 | w = std::min(w, entry->display()->workareaSizeUIScale().w/2); |
497 | |
498 | int h = |
499 | + entry->font()->height() |
500 | + entry->border().height(); |
501 | |
502 | return gfx::Size(w, h); |
503 | } |
504 | |
505 | void Entry::onSizeHint(SizeHintEvent& ev) |
506 | { |
507 | int trailing = font()->textLength(getSuffix()); |
508 | trailing = std::max(trailing, 2*theme()->getEntryCaretSize(this).w); |
509 | |
510 | int w = |
511 | font()->textLength("w" ) * std::min(m_maxsize, 6) + |
512 | + trailing |
513 | + border().width(); |
514 | |
515 | w = std::min(w, display()->workareaSizeUIScale().w/2); |
516 | |
517 | int h = |
518 | + font()->height() |
519 | + border().height(); |
520 | |
521 | ev.setSizeHint(w, h); |
522 | } |
523 | |
524 | void Entry::onPaint(PaintEvent& ev) |
525 | { |
526 | theme()->paintEntry(ev); |
527 | } |
528 | |
529 | void Entry::onSetText() |
530 | { |
531 | Widget::onSetText(); |
532 | recalcCharBoxes(text()); |
533 | |
534 | int textlen = lastCaretPos(); |
535 | if (m_caret >= 0 && m_caret > textlen) |
536 | m_caret = textlen; |
537 | } |
538 | |
539 | void Entry::onChange() |
540 | { |
541 | Change(); |
542 | } |
543 | |
544 | gfx::Rect Entry::onGetEntryTextBounds() const |
545 | { |
546 | gfx::Rect bounds = clientBounds(); |
547 | bounds.x += border().left(); |
548 | bounds.y += bounds.h/2 - textHeight()/2; |
549 | bounds.w -= border().width(); |
550 | bounds.h = textHeight(); |
551 | return bounds; |
552 | } |
553 | |
554 | int Entry::getCaretFromMouse(MouseMessage* mousemsg) |
555 | { |
556 | int mouseX = mousemsg->position().x; |
557 | if (mouseX < bounds().x+border().left()) { |
558 | // Scroll to the left |
559 | return std::max(0, m_scroll-1); |
560 | } |
561 | |
562 | int lastPos = lastCaretPos(); |
563 | int i = std::min(m_scroll, lastPos); |
564 | for (; i<lastPos; ++i) { |
565 | int segmentWidth = 0; |
566 | int indexBox = 0; |
567 | for (int j=m_scroll; j<i; ++j) { |
568 | segmentWidth += m_boxes[j].width; |
569 | indexBox = j+1; |
570 | } |
571 | |
572 | int x = bounds().x + border().left() + segmentWidth + m_boxes[indexBox].width / 2; |
573 | |
574 | if (mouseX > bounds().x2() - border().right()) { |
575 | if (x >= bounds().x2() - border().right()) { |
576 | // Scroll to the right |
577 | break; |
578 | } |
579 | } |
580 | else if (x > mouseX) |
581 | break; |
582 | } |
583 | |
584 | return std::clamp(i, 0, lastPos); |
585 | } |
586 | |
587 | void Entry::executeCmd(EntryCmd cmd, int unicodeChar, bool shift_pressed) |
588 | { |
589 | std::string text = this->text(); |
590 | const Range range = selectedRange(); |
591 | |
592 | switch (cmd) { |
593 | |
594 | case EntryCmd::NoOp: |
595 | break; |
596 | |
597 | case EntryCmd::InsertChar: |
598 | // delete the entire selection |
599 | if (!range.isEmpty()) { |
600 | deleteRange(range, text); |
601 | |
602 | // We set the caret to the beginning of the erased selection, |
603 | // needed to show the first inserted character in case |
604 | // m_scroll > m_caret. E.g. we select all text and insert a |
605 | // new character to replace the whole text, the new inserted |
606 | // character makes m_caret=1, so m_scroll will be 1 too, but |
607 | // we need to make m_scroll=0 to show the new inserted char.) |
608 | // In this way, we first ensure a m_scroll value enough to |
609 | // show the new inserted character. |
610 | recalcCharBoxes(text); |
611 | setCaretPos(m_caret); |
612 | } |
613 | |
614 | // Convert the unicode character -> wstring -> utf-8 string -> insert the utf-8 string |
615 | if (lastCaretPos() < m_maxsize) { |
616 | ASSERT(m_caret <= lastCaretPos()); |
617 | |
618 | std::wstring unicodeStr; |
619 | unicodeStr.push_back(unicodeChar); |
620 | |
621 | text.insert(m_boxes[m_caret].from, |
622 | base::to_utf8(unicodeStr)); |
623 | recalcCharBoxes(text); |
624 | ++m_caret; |
625 | } |
626 | |
627 | m_select = -1; |
628 | break; |
629 | |
630 | case EntryCmd::BackwardChar: |
631 | case EntryCmd::BackwardWord: |
632 | // selection |
633 | if (shift_pressed) { |
634 | if (m_select < 0) |
635 | m_select = m_caret; |
636 | } |
637 | else |
638 | m_select = -1; |
639 | |
640 | // backward word |
641 | if (cmd == EntryCmd::BackwardWord) { |
642 | backwardWord(); |
643 | } |
644 | // backward char |
645 | else if (m_caret > 0) { |
646 | m_caret--; |
647 | } |
648 | break; |
649 | |
650 | case EntryCmd::ForwardChar: |
651 | case EntryCmd::ForwardWord: |
652 | // selection |
653 | if (shift_pressed) { |
654 | if (m_select < 0) |
655 | m_select = m_caret; |
656 | } |
657 | else |
658 | m_select = -1; |
659 | |
660 | // forward word |
661 | if (cmd == EntryCmd::ForwardWord) { |
662 | forwardWord(); |
663 | } |
664 | // forward char |
665 | else if (m_caret < (int)text.size()) { |
666 | m_caret++; |
667 | } |
668 | break; |
669 | |
670 | case EntryCmd::BeginningOfLine: |
671 | // selection |
672 | if (shift_pressed) { |
673 | if (m_select < 0) |
674 | m_select = m_caret; |
675 | } |
676 | else |
677 | m_select = -1; |
678 | |
679 | m_caret = 0; |
680 | break; |
681 | |
682 | case EntryCmd::EndOfLine: |
683 | // selection |
684 | if (shift_pressed) { |
685 | if (m_select < 0) |
686 | m_select = m_caret; |
687 | } |
688 | else |
689 | m_select = -1; |
690 | |
691 | m_caret = lastCaretPos(); |
692 | break; |
693 | |
694 | case EntryCmd::DeleteForward: |
695 | case EntryCmd::Cut: |
696 | // delete the entire selection |
697 | if (!range.isEmpty()) { |
698 | // *cut* text! |
699 | if (cmd == EntryCmd::Cut) |
700 | set_clipboard_text(selectedText()); |
701 | |
702 | // remove text |
703 | deleteRange(range, text); |
704 | } |
705 | // delete the next character |
706 | else { |
707 | if (m_caret < (int)text.size()) |
708 | text.erase(m_boxes[m_caret].from, |
709 | m_boxes[m_caret].to - m_boxes[m_caret].from); |
710 | } |
711 | |
712 | m_select = -1; |
713 | break; |
714 | |
715 | case EntryCmd::Paste: { |
716 | std::string clipboard; |
717 | if (get_clipboard_text(clipboard)) { |
718 | // delete the entire selection |
719 | if (!range.isEmpty()) { |
720 | deleteRange(range, text); |
721 | m_select = -1; |
722 | } |
723 | |
724 | // Paste text |
725 | recalcCharBoxes(text); |
726 | int oldBoxes = m_boxes.size(); |
727 | |
728 | text.insert(m_boxes[m_caret].from, clipboard); |
729 | |
730 | // Remove extra chars that do not fit in m_maxsize |
731 | recalcCharBoxes(text); |
732 | if (lastCaretPos() > m_maxsize) { |
733 | text.erase(m_boxes[m_maxsize].from, |
734 | text.size() - m_boxes[m_maxsize].from); |
735 | recalcCharBoxes(text); |
736 | } |
737 | |
738 | int newBoxes = m_boxes.size(); |
739 | setCaretPos(m_caret+(newBoxes - oldBoxes)); |
740 | } |
741 | break; |
742 | } |
743 | |
744 | case EntryCmd::Copy: |
745 | if (!range.isEmpty()) |
746 | set_clipboard_text(selectedText()); |
747 | break; |
748 | |
749 | case EntryCmd::DeleteBackward: |
750 | // delete the entire selection |
751 | if (!range.isEmpty()) { |
752 | deleteRange(range, text); |
753 | } |
754 | // delete the previous character |
755 | else { |
756 | if (m_caret > 0) { |
757 | --m_caret; |
758 | text.erase(m_boxes[m_caret].from, |
759 | m_boxes[m_caret].to - m_boxes[m_caret].from); |
760 | } |
761 | } |
762 | |
763 | m_select = -1; |
764 | break; |
765 | |
766 | case EntryCmd::DeleteBackwardWord: |
767 | m_select = m_caret; |
768 | backwardWord(); |
769 | if (m_caret < m_select) { |
770 | text.erase(m_boxes[m_caret].from, |
771 | m_boxes[m_select-1].to - m_boxes[m_caret].from); |
772 | } |
773 | m_select = -1; |
774 | break; |
775 | |
776 | case EntryCmd::DeleteForwardToEndOfLine: |
777 | text.erase(m_boxes[m_caret].from); |
778 | break; |
779 | |
780 | case EntryCmd::SelectAll: |
781 | selectAllText(); |
782 | break; |
783 | } |
784 | |
785 | if (text != this->text()) { |
786 | setText(text); |
787 | onChange(); |
788 | } |
789 | |
790 | setCaretPos(m_caret); |
791 | invalidate(); |
792 | } |
793 | |
794 | void Entry::forwardWord() |
795 | { |
796 | int textlen = lastCaretPos(); |
797 | |
798 | for (; m_caret < textlen; ++m_caret) { |
799 | if (is_word_char(m_boxes[m_caret].codepoint)) |
800 | break; |
801 | } |
802 | |
803 | for (; m_caret < textlen; ++m_caret) { |
804 | if (!is_word_char(m_boxes[m_caret].codepoint)) |
805 | break; |
806 | } |
807 | } |
808 | |
809 | void Entry::backwardWord() |
810 | { |
811 | for (--m_caret; m_caret >= 0; --m_caret) { |
812 | if (is_word_char(m_boxes[m_caret].codepoint)) |
813 | break; |
814 | } |
815 | |
816 | for (; m_caret >= 0; --m_caret) { |
817 | if (!is_word_char(m_boxes[m_caret].codepoint)) { |
818 | ++m_caret; |
819 | break; |
820 | } |
821 | } |
822 | |
823 | if (m_caret < 0) |
824 | m_caret = 0; |
825 | } |
826 | |
827 | Entry::Range Entry::wordRange(int pos) |
828 | { |
829 | const int last = lastCaretPos(); |
830 | pos = std::clamp(pos, 0, last); |
831 | |
832 | int i, j; |
833 | i = j = pos; |
834 | |
835 | // Select word space |
836 | if (is_word_char(m_boxes[pos].codepoint)) { |
837 | for (; i>=0; --i) { |
838 | if (!is_word_char(m_boxes[i].codepoint)) |
839 | break; |
840 | } |
841 | ++i; |
842 | for (; j<=last; ++j) { |
843 | if (!is_word_char(m_boxes[j].codepoint)) |
844 | break; |
845 | } |
846 | } |
847 | // Select punctuation space |
848 | else { |
849 | for (; i>=0; --i) { |
850 | if (is_word_char(m_boxes[i].codepoint)) |
851 | break; |
852 | } |
853 | ++i; |
854 | for (; j<=last; ++j) { |
855 | if (is_word_char(m_boxes[j].codepoint)) |
856 | break; |
857 | } |
858 | } |
859 | return Range(i, j); |
860 | } |
861 | |
862 | bool Entry::isPosInSelection(int pos) |
863 | { |
864 | return (pos >= std::min(m_caret, m_select) && |
865 | pos <= std::max(m_caret, m_select)); |
866 | } |
867 | |
868 | void Entry::(const gfx::Point& pt) |
869 | { |
870 | Menu ; |
871 | MenuItem cut("Cut" ); |
872 | MenuItem copy("Copy" ); |
873 | MenuItem paste("Paste" ); |
874 | menu.addChild(&cut); |
875 | menu.addChild(©); |
876 | menu.addChild(&paste); |
877 | cut.Click.connect([this]{ executeCmd(EntryCmd::Cut, 0, false); }); |
878 | copy.Click.connect([this]{ executeCmd(EntryCmd::Copy, 0, false); }); |
879 | paste.Click.connect([this]{ executeCmd(EntryCmd::Paste, 0, false); }); |
880 | |
881 | if (isReadOnly()) { |
882 | cut.setEnabled(false); |
883 | paste.setEnabled(false); |
884 | } |
885 | |
886 | menu.showPopup(pt, display()); |
887 | } |
888 | |
889 | class Entry::CalcBoxesTextDelegate : public os::DrawTextDelegate { |
890 | public: |
891 | CalcBoxesTextDelegate(const int end) : m_end(end) { |
892 | } |
893 | |
894 | const Entry::CharBoxes& boxes() const { return m_boxes; } |
895 | |
896 | void preProcessChar(const int index, |
897 | const int codepoint, |
898 | gfx::Color& fg, |
899 | gfx::Color& bg, |
900 | const gfx::Rect& charBounds) override { |
901 | if (!m_boxes.empty()) |
902 | m_boxes.back().to = index; |
903 | |
904 | m_box = CharBox(); |
905 | m_box.codepoint = codepoint; |
906 | m_box.from = index; |
907 | m_box.to = m_end; |
908 | } |
909 | |
910 | bool preDrawChar(const gfx::Rect& charBounds) override { |
911 | m_box.width = charBounds.w; |
912 | return true; |
913 | } |
914 | |
915 | void postDrawChar(const gfx::Rect& charBounds) override { |
916 | m_boxes.push_back(m_box); |
917 | } |
918 | |
919 | private: |
920 | Entry::CharBox m_box; |
921 | Entry::CharBoxes m_boxes; |
922 | int m_end; |
923 | }; |
924 | |
925 | void Entry::recalcCharBoxes(const std::string& text) |
926 | { |
927 | int lastTextIndex = int(text.size()); |
928 | CalcBoxesTextDelegate delegate(lastTextIndex); |
929 | os::draw_text(nullptr, font(), text, |
930 | gfx::ColorNone, gfx::ColorNone, 0, 0, &delegate); |
931 | m_boxes = delegate.boxes(); |
932 | |
933 | if (!m_boxes.empty()) { |
934 | m_boxes.back().to = lastTextIndex; |
935 | } |
936 | |
937 | // A last box for the last position |
938 | CharBox box; |
939 | box.codepoint = 0; |
940 | box.from = box.to = lastTextIndex; |
941 | m_boxes.push_back(box); |
942 | } |
943 | |
944 | bool Entry::shouldStartTimer(bool hasFocus) |
945 | { |
946 | return (!m_hidden && hasFocus && isEnabled()); |
947 | } |
948 | |
949 | void Entry::deleteRange(const Range& range, std::string& text) |
950 | { |
951 | text.erase(m_boxes[range.from].from, |
952 | m_boxes[range.to-1].to - m_boxes[range.from].from); |
953 | m_caret = range.from; |
954 | } |
955 | |
956 | void Entry::startTimer() |
957 | { |
958 | if (s_timer) |
959 | s_timer->stop(); |
960 | s_timer = std::make_unique<Timer>(500, this); |
961 | s_timer->start(); |
962 | } |
963 | |
964 | void Entry::stopTimer() |
965 | { |
966 | if (s_timer) { |
967 | s_timer->stop(); |
968 | s_timer.reset(); |
969 | } |
970 | } |
971 | |
972 | } // namespace ui |
973 | |