1 // Copyright (C) 2010-2015 Petr Pavlu <setup@dagobah.cz>
3 // This file is part of CenterIM.
5 // CenterIM is free software; you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation; either version 2 of the License, or
8 // (at your option) any later version.
10 // CenterIM is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with CenterIM. If not, see <http://www.gnu.org/licenses/>.
19 /// TextEdit class implementation
21 /// @ingroup cppconsui
23 // Gap buffer implementation based on code from Hsin Tsao
24 // (stsao@lazyhacker.com).
28 #include "ColorScheme.h"
34 // Gap expand size when the gap becomes filled.
35 #define GAP_SIZE_EXPAND 4096
39 TextEdit::TextEdit(int w
, int h
, const char *text
, int flags
, bool single_line
,
40 bool accept_tabs
, bool masked
)
41 : Widget(w
, h
), flags_(flags
), editable_(true), overwrite_mode_(false),
42 single_line_mode_(single_line
), accept_tabs_(accept_tabs
), masked_(masked
),
43 buffer_(nullptr), screen_lines_dirty_(false)
56 bool TextEdit::processInputText(const TermKeyKey
&key
)
61 if (single_line_mode_
&& key
.code
.codepoint
== '\n')
64 if (!accept_tabs_
&& key
.code
.codepoint
== '\t')
67 // Filter out unwanted input.
69 if ((flags_
& FLAG_NUMERIC
) && !UTF8::isUniCharDigit(key
.code
.codepoint
))
71 if ((flags_
& FLAG_NOSPACE
) && UTF8::isUniCharSpace(key
.code
.codepoint
))
75 insertTextAtCursor(key
.utf8
);
79 int TextEdit::draw(Curses::ViewPort area
, Error
&error
)
81 assertUpdatedScreenLines();
83 DRAW(area
.erase(error
));
86 DRAW(getAttributes(ColorScheme::PROPERTY_TEXTEDIT_TEXT
, &attrs
, error
));
87 DRAW(area
.attrOn(attrs
, error
));
89 ScreenLines::iterator i
;
91 for (i
= screen_lines_
.begin() + view_top_
, j
= 0;
92 i
!= screen_lines_
.end() && j
< real_height_
; ++i
, ++j
) {
93 const char *p
= i
->start
;
95 for (std::size_t k
= 0; k
< i
->length
&& *p
!= '\n'; ++k
) {
98 DRAW(area
.addChar(w
, j
, '*', error
, &printed
));
100 UTF8::UniChar uc
= UTF8::getUniChar(p
);
102 printed
= onScreenWidth(uc
, w
);
103 for (int l
= 0; l
< printed
; ++l
)
104 DRAW(area
.addChar(w
+ l
, j
, ' ', error
));
107 DRAW(area
.addChar(w
, j
, uc
, error
, &printed
));
114 DRAW(area
.attrOff(attrs
, error
));
117 const char *line
= screen_lines_
[current_sc_line_
].start
;
118 int sc_x
= width(line
, current_sc_linepos_
);
119 int sc_y
= current_sc_line_
- view_top_
;
120 DRAW(area
.changeAt(sc_x
, sc_y
, 1, Curses::Attr::REVERSE
, 0, error
));
126 void TextEdit::setText(const char *new_text
)
128 if (new_text
== nullptr) {
133 // XXX should the text be validated (FLAG_*)?
134 std::size_t size
= strlen(new_text
);
135 initBuffer(size
+ GAP_SIZE_EXPAND
);
136 insertTextAtCursor(new_text
, size
);
139 void TextEdit::clear()
141 initBuffer(GAP_SIZE_EXPAND
);
145 const char *TextEdit::getText() const
147 assert(gapend_
> gapstart_
);
149 screen_lines_dirty_
= true;
151 // Move gap to the end.
152 bool point_after_gap
= point_
>= gapend_
;
154 // '-1' so the last '\n' is still in the end of the buffer.
155 std::memmove(gapstart_
, gapend_
, bufend_
- gapend_
- 1);
157 point_
-= gapend_
- gapstart_
;
158 gapstart_
+= bufend_
- gapend_
- 1;
159 gapend_
= bufend_
- 1;
166 void TextEdit::setFlags(int new_flags
, bool revalidate
)
168 if (new_flags
== flags_
)
173 if (flags_
!= 0 && revalidate
) {
175 const char *p
= getTextStart();
176 while (p
< bufend_
- 1) {
177 UTF8::UniChar uc
= UTF8::getUniChar(p
);
178 if ((flags_
& FLAG_NUMERIC
) && !UTF8::isUniCharDigit(uc
)) {
182 if ((flags_
& FLAG_NOSPACE
) && UTF8::isUniCharSpace(uc
)) {
193 void TextEdit::setSingleLineMode(bool new_single_line_mode
)
195 if (new_single_line_mode
== single_line_mode_
)
198 single_line_mode_
= new_single_line_mode
;
201 void TextEdit::setAcceptTabs(bool new_accept_tabs
)
203 if (new_accept_tabs
== accept_tabs_
)
206 accept_tabs_
= new_accept_tabs
;
209 void TextEdit::setMasked(bool new_masked
)
211 if (new_masked
== masked_
)
214 masked_
= new_masked
;
215 // In the masked mode, the tab character and wide characters lose their width
216 // property, thus screen lines and cursor have to be updated.
218 updateScreenCursor();
222 bool TextEdit::ScreenLine::operator==(const ScreenLine
&other
) const
224 return start
== other
.start
&& end
== other
.end
&& length
== other
.length
;
227 bool TextEdit::CmpScreenLineEnd::operator()(ScreenLine
&sline
, const char *tag
)
229 return sline
.end
< tag
;
232 void TextEdit::updateArea()
234 // Update screen lines and cursor position if the area width changed.
236 updateScreenCursor();
239 void TextEdit::initBuffer(std::size_t size
)
244 buffer_
= new char[size
];
247 point_
= gapstart_
= buffer_
;
249 bufend_
= gapend_
= buffer_
+ size
;
250 gapend_
= bufend_
- 1;
251 // Insert an empty line.
256 current_sc_line_
= 0;
257 current_sc_linepos_
= 0;
264 std::size_t TextEdit::getGapSize() const
266 // '-1' so '\0' character can be stored at the gapstart position in the
268 return gapend_
- gapstart_
- 1;
271 void TextEdit::expandGap(std::size_t size
)
273 std::size_t gap_size
= getGapSize();
274 if (size
<= gap_size
)
277 size
+= GAP_SIZE_EXPAND
- gap_size
;
279 char *origbuffer
= buffer_
;
280 bool point_after_gap
= point_
>= gapend_
;
282 std::size_t alloc_size
= (bufend_
- buffer_
) + size
;
283 buffer_
= new char[alloc_size
];
284 std::memcpy(buffer_
, origbuffer
, alloc_size
);
286 point_
= buffer_
+ (point_
- origbuffer
);
287 bufend_
= buffer_
+ (bufend_
- origbuffer
);
288 gapstart_
= buffer_
+ (gapstart_
- origbuffer
);
289 gapend_
= buffer_
+ (gapend_
- origbuffer
);
293 std::memmove(gapend_
+ size
, gapend_
, bufend_
- gapend_
);
295 if (point_after_gap
) {
296 // This should never happen because moveGapToCursor() is always called
297 // before expandGap().
304 void TextEdit::moveGapToCursor()
306 if (point_
== gapstart_
)
309 if (point_
== gapend_
) {
314 // Move gap towards the left.
315 if (point_
< gapstart_
) {
316 // Move the point over by gapsize.
317 std::memmove(point_
+ (gapend_
- gapstart_
), point_
, gapstart_
- point_
);
318 gapend_
-= gapstart_
- point_
;
322 // Since point is after the gap, find distance between gapend and point and
323 // that is how much we move from gapend to gapstart.
324 std::memmove(gapstart_
, gapend_
, point_
- gapend_
);
325 gapstart_
+= point_
- gapend_
;
331 char *TextEdit::getTextStart() const
333 if (buffer_
== gapstart_
)
334 return const_cast<char *>(gapend_
);
335 return const_cast<char *>(buffer_
);
338 char *TextEdit::prevChar(const char *p
) const
341 if ((p
= UTF8::findPrevChar(gapend_
, p
)))
342 return const_cast<char *>(p
);
347 if ((p
= UTF8::findPrevChar(buffer_
, p
)))
348 return const_cast<char *>(p
);
350 return const_cast<char *>(buffer_
);
353 char *TextEdit::nextChar(const char *p
) const
355 // This happens when point_ == gapstart_.
360 if ((p
= UTF8::findNextChar(p
, gapstart_
)))
361 return const_cast<char *>(p
);
363 return const_cast<char *>(gapend_
);
366 if ((p
= UTF8::findNextChar(p
, bufend_
)))
367 return const_cast<char *>(p
);
369 return const_cast<char *>(bufend_
);
372 int TextEdit::width(const char *start
, std::size_t chars
) const
374 assert(start
!= nullptr);
378 while (chars
-- > 0) {
379 UTF8::UniChar uc
= UTF8::getUniChar(start
);
380 width
+= onScreenWidth(uc
, width
);
381 start
= nextChar(start
);
386 int TextEdit::onScreenWidth(UTF8::UniChar uc
, int w
) const
390 return Curses::onScreenWidth(uc
, w
);
393 char *TextEdit::getScreenLine(
394 const char *text
, int max_width
, std::size_t *res_length
) const
396 assert(text
!= nullptr);
397 assert(text
< bufend_
);
398 assert(max_width
> 0);
399 assert(res_length
!= nullptr);
401 const char *cur
= text
;
402 const char *res
= text
;
405 std::size_t cur_length
= 0;
409 while (cur
< bufend_
) {
410 prev_width
= cur_width
;
411 UTF8::UniChar uc
= UTF8::getUniChar(cur
);
412 cur_width
+= onScreenWidth(uc
, cur_width
);
415 if (prev_width
> max_width
)
418 // Possibly too long word.
419 if (cur_width
> max_width
&& !*res_length
) {
420 *res_length
= cur_length
- 1;
424 // End of line (paragraph on screen) found.
426 *res_length
= cur_length
;
427 return nextChar(cur
);
430 if (UTF8::isUniCharSpace(uc
))
433 // Found start of a word and everything before that can fit into one
435 *res_length
= cur_length
- 1;
443 // Fix for very small max_width and characters wider that 1 cell. For example,
444 // max_width = 1 and text = "W" where W is a wide character (2 cells width)
445 // (or simply for tabs). In that case we can not draw anything but we want to
446 // skip to another character.
452 return const_cast<char *>(res
);
455 void TextEdit::updateScreenLines()
457 screen_lines_
.clear();
459 if (real_width_
<= 1)
462 const char *p
= getTextStart();
464 while (p
< bufend_
) {
467 // Lower max width by one to make a room for the cursor.
468 p
= getScreenLine(p
, real_width_
- 1, &length
);
469 screen_lines_
.push_back(ScreenLine(s
, p
, length
));
473 void TextEdit::updateScreenLines(const char *begin
, const char *end
)
475 assert(begin
!= nullptr);
476 assert(end
!= nullptr);
478 if (real_width_
<= 1)
481 ScreenLines::iterator b
, i
;
482 b
= std::lower_bound(screen_lines_
.begin(), screen_lines_
.end(), begin
,
483 TextEdit::CmpScreenLineEnd());
484 if (b
!= screen_lines_
.begin()) {
485 // Initial Correct final
486 // situation situation
487 // --------- ---------
488 // |aaaa | -> |aaaa b |
489 // |bcdddd | |cdddd |
491 // User inserts a space in front of the 'c' character. The 'b' string can be
492 // moved on the previous line thus one more extra line before has to be
493 // recalculated to handle the situation correctly.
498 ScreenLines new_screen_lines
;
500 const char *p
= b
->start
;
501 if (i
== screen_lines_
.begin())
504 while (p
< bufend_
) {
507 // Lower max width by one to make a room for the cursor.
508 p
= getScreenLine(p
, real_width_
- 1, &length
);
509 ScreenLine
sline(s
, p
, length
);
510 new_screen_lines
.push_back(sline
);
512 i
!= screen_lines_
.end() && (i
->end
<= end
|| i
->start
< s
|| i
->end
< p
))
514 if (i
!= screen_lines_
.end() && sline
== *i
) {
515 // Screen lines are same thus it is not necessary to recalculate more
520 if (i
!= screen_lines_
.end())
523 // Replace old screen lines with new screen lines.
524 ScreenLines::iterator j
;
525 for (j
= new_screen_lines
.begin(); j
!= new_screen_lines
.end() && b
!= i
;
529 if (j
!= new_screen_lines
.end()) {
531 screen_lines_
.insert(b
, j
, new_screen_lines
.end());
535 screen_lines_
.erase(b
, i
);
539 void TextEdit::assertUpdatedScreenLines()
541 if (!screen_lines_dirty_
)
545 screen_lines_dirty_
= false;
548 void TextEdit::updateScreenCursor()
550 std::size_t acu_length
= 0;
551 current_sc_line_
= 0;
552 current_sc_linepos_
= 0;
554 assertUpdatedScreenLines();
556 for (ScreenLine
&line
: screen_lines_
) {
557 std::size_t length
= line
.length
;
558 if (acu_length
<= current_pos_
&& current_pos_
< acu_length
+ length
) {
559 current_sc_linepos_
= current_pos_
- acu_length
;
563 acu_length
+= length
;
566 // Fix cursor visibility.
567 if (view_top_
<= current_sc_line_
&&
568 current_sc_line_
< view_top_
+ real_height_
)
570 while (view_top_
> current_sc_line_
)
572 while (view_top_
+ real_height_
<= current_sc_line_
)
576 void TextEdit::insertTextAtCursor(
577 const char *new_text
, std::size_t new_text_bytes
)
579 assert(new_text
!= nullptr);
581 assertUpdatedScreenLines();
583 // Move the gap if the point is not already at the start of the gap.
584 char *min
= gapstart_
;
587 char *min2
= gapstart_
;
589 // Make sure that the gap has enough room.
590 bool full_screen_lines_update
= false;
591 if (new_text_bytes
> getGapSize()) {
592 expandGap(new_text_bytes
);
593 full_screen_lines_update
= true;
596 std::size_t n_chars
= 0;
597 const char *p
= new_text
;
598 while (p
!= nullptr && *p
!= '\0') {
600 p
= UTF8::findNextChar(p
, new_text
+ new_text_bytes
);
602 text_length_
+= n_chars
;
603 current_pos_
+= n_chars
;
605 while (new_text_bytes
) {
606 *gapstart_
++ = *new_text
++;
611 if (full_screen_lines_update
)
614 updateScreenLines(std::min(min
, min2
), std::max(max
, gapend_
));
615 updateScreenCursor();
618 signal_text_change(*this);
621 void TextEdit::insertTextAtCursor(const char *new_text
)
623 assert(new_text
!= nullptr);
625 insertTextAtCursor(new_text
, strlen(new_text
));
628 void TextEdit::deleteFromCursor(DeleteType type
, Direction dir
)
633 assertUpdatedScreenLines();
639 count
= moveLogicallyFromCursor(dir
) - current_pos_
;
641 case DELETE_WORD_ENDS
:
642 count
= moveWordFromCursor(dir
, true) - current_pos_
;
649 char *min
= gapstart_
;
654 gapend_
= nextChar(gapend_
);
660 gapstart_
= prevChar(gapstart_
);
667 updateScreenLines(std::min(min
, gapstart_
), std::max(max
, gapend_
));
668 updateScreenCursor();
671 signal_text_change(*this);
675 void TextEdit::moveCursor(CursorMovement step
, Direction dir
)
677 assertUpdatedScreenLines();
679 std::size_t old_pos
= current_pos_
;
681 case MOVE_LOGICAL_POSITIONS
:
682 current_pos_
= moveLogicallyFromCursor(dir
);
685 current_pos_
= moveWordFromCursor(dir
, false);
687 case MOVE_DISPLAY_LINES
:
688 if (dir
== DIR_FORWARD
) {
689 if (current_sc_line_
+ 1 < screen_lines_
.size()) {
691 width(screen_lines_
[current_sc_line_
].start
, current_sc_linepos_
);
692 // First move to end of current line.
694 screen_lines_
[current_sc_line_
].length
- current_sc_linepos_
;
695 // Find a character close to the original position.
696 const char *ch
= screen_lines_
[current_sc_line_
+ 1].start
;
699 while (w
< oldw
&& i
< screen_lines_
[current_sc_line_
+ 1].length
- 1) {
700 UTF8::UniChar uc
= UTF8::getUniChar(ch
);
701 w
+= onScreenWidth(uc
, w
);
709 if (current_sc_line_
> 0) {
711 width(screen_lines_
[current_sc_line_
].start
, current_sc_linepos_
);
712 // First move to start of current line.
713 current_pos_
-= current_sc_linepos_
;
714 // Move to the start of the previous line.
715 current_pos_
-= screen_lines_
[current_sc_line_
- 1].length
;
716 // Find a character close to the original position.
717 const char *ch
= screen_lines_
[current_sc_line_
- 1].start
;
720 while (w
< oldw
&& i
< screen_lines_
[current_sc_line_
- 1].length
- 1) {
721 UTF8::UniChar uc
= UTF8::getUniChar(ch
);
722 w
+= onScreenWidth(uc
, w
);
730 case MOVE_DISPLAY_LINE_ENDS
:
731 if (dir
== DIR_FORWARD
)
733 screen_lines_
[current_sc_line_
].length
- current_sc_linepos_
- 1;
735 current_pos_
-= current_sc_linepos_
;
742 while (old_pos
> current_pos_
) {
743 point_
= prevChar(point_
);
746 while (old_pos
< current_pos_
) {
747 point_
= nextChar(point_
);
751 updateScreenCursor();
755 void TextEdit::toggleOverwrite()
757 overwrite_mode_
= !overwrite_mode_
;
760 std::size_t TextEdit::moveLogicallyFromCursor(Direction dir
) const
762 if (dir
== DIR_FORWARD
&& current_pos_
< text_length_
)
763 return current_pos_
+ 1;
764 else if (dir
== DIR_BACK
&& current_pos_
> 0)
765 return current_pos_
- 1;
769 std::size_t TextEdit::moveWordFromCursor(Direction dir
, bool word_end
) const
771 std::size_t new_pos
= current_pos_
;
772 const char *cur
= point_
;
773 if (cur
== gapstart_
)
776 if (dir
== DIR_FORWARD
) {
778 // Search for the first white character after non-white characters.
779 bool nonwhite
= false;
780 while (new_pos
< text_length_
) {
781 if (!UTF8::isUniCharSpace(UTF8::getUniChar(cur
)) && *cur
!= '\n')
791 // Search for the first nonwhite character after white characters.
793 while (new_pos
< text_length_
) {
794 if (UTF8::isUniCharSpace(UTF8::getUniChar(cur
)) || *cur
== '\n')
808 // Always move at least one character back.
812 // Search for the first white character before nonwhite characters.
813 bool nonwhite
= false;
814 while (new_pos
!= static_cast<std::size_t>(-1)) {
815 if (!UTF8::isUniCharSpace(UTF8::getUniChar(cur
)) && *cur
!= '\n')
827 void TextEdit::actionMoveCursor(CursorMovement step
, Direction dir
)
829 moveCursor(step
, dir
);
832 void TextEdit::actionDelete(DeleteType type
, Direction dir
)
834 deleteFromCursor(type
, dir
);
837 void TextEdit::actionToggleOverwrite()
842 void TextEdit::declareBindables()
845 declareBindable("textentry", "cursor-right",
846 sigc::bind(sigc::mem_fun(this, &TextEdit::actionMoveCursor
),
847 MOVE_LOGICAL_POSITIONS
, DIR_FORWARD
),
848 InputProcessor::BINDABLE_NORMAL
);
850 declareBindable("textentry", "cursor-left",
851 sigc::bind(sigc::mem_fun(this, &TextEdit::actionMoveCursor
),
852 MOVE_LOGICAL_POSITIONS
, DIR_BACK
),
853 InputProcessor::BINDABLE_NORMAL
);
855 declareBindable("textentry", "cursor-down",
856 sigc::bind(sigc::mem_fun(this, &TextEdit::actionMoveCursor
),
857 MOVE_DISPLAY_LINES
, DIR_FORWARD
),
858 InputProcessor::BINDABLE_NORMAL
);
860 declareBindable("textentry", "cursor-up",
861 sigc::bind(sigc::mem_fun(this, &TextEdit::actionMoveCursor
),
862 MOVE_DISPLAY_LINES
, DIR_BACK
),
863 InputProcessor::BINDABLE_NORMAL
);
865 declareBindable("textentry", "cursor-right-word",
866 sigc::bind(sigc::mem_fun(this, &TextEdit::actionMoveCursor
), MOVE_WORDS
,
868 InputProcessor::BINDABLE_NORMAL
);
870 declareBindable("textentry", "cursor-left-word",
871 sigc::bind(sigc::mem_fun(this, &TextEdit::actionMoveCursor
), MOVE_WORDS
,
873 InputProcessor::BINDABLE_NORMAL
);
875 declareBindable("textentry", "cursor-end",
876 sigc::bind(sigc::mem_fun(this, &TextEdit::actionMoveCursor
),
877 MOVE_DISPLAY_LINE_ENDS
, DIR_FORWARD
),
878 InputProcessor::BINDABLE_NORMAL
);
880 declareBindable("textentry", "cursor-begin",
881 sigc::bind(sigc::mem_fun(this, &TextEdit::actionMoveCursor
),
882 MOVE_DISPLAY_LINE_ENDS
, DIR_BACK
),
883 InputProcessor::BINDABLE_NORMAL
);
886 declareBindable("textentry", "delete-char",
887 sigc::bind(sigc::mem_fun(this, &TextEdit::actionDelete
), DELETE_CHARS
,
889 InputProcessor::BINDABLE_NORMAL
);
891 declareBindable("textentry", "backspace",
892 sigc::bind(sigc::mem_fun(this, &TextEdit::actionDelete
), DELETE_CHARS
,
894 InputProcessor::BINDABLE_NORMAL
);
896 declareBindable("textentry", "delete-word-end",
897 sigc::bind(sigc::mem_fun(this, &TextEdit::actionDelete
), DELETE_WORD_ENDS
,
899 InputProcessor::BINDABLE_NORMAL
);
901 declareBindable("textentry", "delete-word-begin",
902 sigc::bind(sigc::mem_fun(this, &TextEdit::actionDelete
), DELETE_WORD_ENDS
,
904 InputProcessor::BINDABLE_NORMAL
);
906 declareBindable("textentry", "newline",
907 sigc::bind(sigc::mem_fun(
908 this, static_cast<void (TextEdit::*)(const char *)>(
909 &TextEdit::insertTextAtCursor
)),
911 InputProcessor::BINDABLE_NORMAL
);
915 declareBindable("textentry", "toggle-overwrite", sigc::mem_fun(this,
916 &TextEdit::actionToggleOverwrite), InputProcessor::BINDABLE_NORMAL);
920 } // namespace CppConsUI
922 // vim: set tabstop=2 shiftwidth=2 textwidth=80 expandtab: