1 #include "TextEditor.h"
2 
3 #include <memory>
4 
5 #include <gtk/gtkimmulticontext.h>
6 
7 #include "control/Control.h"
8 #include "undo/ColorUndoAction.h"
9 #include "view/DocumentView.h"
10 #include "view/TextView.h"
11 
12 #include "PageView.h"
13 #include "TextEditorWidget.h"
14 #include "XournalView.h"
15 #include "XournalppCursor.h"
16 
TextEditor(XojPageView * gui,GtkWidget * widget,Text * text,bool ownText)17 TextEditor::TextEditor(XojPageView* gui, GtkWidget* widget, Text* text, bool ownText):
18         gui(gui), widget(widget), text(text), ownText(ownText) {
19     this->text->setInEditing(true);
20     this->textWidget = gtk_xoj_int_txt_new(this);
21     this->lastText = text->getText();
22 
23     this->buffer = gtk_text_buffer_new(nullptr);
24     string txt = this->text->getText();
25     gtk_text_buffer_set_text(this->buffer, txt.c_str(), -1);
26 
27     g_signal_connect(this->buffer, "paste-done", G_CALLBACK(bufferPasteDoneCallback), this);
28 
29     GtkTextIter first = {nullptr};
30     gtk_text_buffer_get_iter_at_offset(this->buffer, &first, 0);
31     gtk_text_buffer_place_cursor(this->buffer, &first);
32 
33     GtkSettings* settings = gtk_widget_get_settings(this->widget);
34     g_object_get(settings, "gtk-cursor-blink", &this->cursorBlink, nullptr);
35     g_object_get(settings, "gtk-cursor-blink-time", &this->cursorBlinkTime, nullptr);
36     g_object_get(settings, "gtk-cursor-blink-timeout", &this->cursorBlinkTimeout, nullptr);
37 
38     this->imContext = gtk_im_multicontext_new();
39     gtk_im_context_focus_in(this->imContext);
40 
41     g_signal_connect(this->imContext, "commit", G_CALLBACK(iMCommitCallback), this);
42     g_signal_connect(this->imContext, "preedit-changed", G_CALLBACK(iMPreeditChangedCallback), this);
43     g_signal_connect(this->imContext, "retrieve-surrounding", G_CALLBACK(iMRetrieveSurroundingCallback), this);
44     g_signal_connect(this->imContext, "delete-surrounding", G_CALLBACK(imDeleteSurroundingCallback), this);
45 
46     if (this->cursorBlink) {
47         blinkCallback(this);
48     } else {
49         this->cursorVisible = true;
50     }
51 }
52 
~TextEditor()53 TextEditor::~TextEditor() {
54     this->text->setInEditing(false);
55     this->widget = nullptr;
56 
57     Control* control = gui->getXournal()->getControl();
58     control->setCopyPasteEnabled(false);
59 
60     this->contentsChanged(true);
61 
62     if (this->ownText) {
63         UndoRedoHandler* handler = gui->getXournal()->getControl()->getUndoRedoHandler();
64         for (TextUndoAction& undo: this->undoActions) { handler->removeUndoAction(&undo); }
65     } else {
66         for (TextUndoAction& undo: this->undoActions) { undo.textEditFinished(); }
67     }
68     this->undoActions.clear();
69 
70     if (this->ownText) {
71         delete this->text;
72         this->text = nullptr;
73     }
74 
75     g_object_unref(this->buffer);
76     gtk_widget_destroy(this->textWidget);
77 
78     if (this->blinkTimeout) {
79         g_source_remove(this->blinkTimeout);
80     }
81 
82     g_object_unref(this->imContext);
83 
84     this->text = nullptr;
85 
86     if (this->layout) {
87         g_object_unref(this->layout);
88         this->layout = nullptr;
89     }
90 
91     if (this->preeditAttrList) {
92         pango_attr_list_unref(this->preeditAttrList);
93     }
94 }
95 
getText()96 auto TextEditor::getText() -> Text* {
97     GtkTextIter start, end;
98 
99     gtk_text_buffer_get_bounds(this->buffer, &start, &end);
100     char* text = gtk_text_iter_get_text(&start, &end);
101     this->text->setText(text);
102     g_free(text);
103 
104     return this->text;
105 }
106 
setText(const string & text)107 void TextEditor::setText(const string& text) {
108     gtk_text_buffer_set_text(this->buffer, text.c_str(), -1);
109 
110     GtkTextIter first = {nullptr};
111     gtk_text_buffer_get_iter_at_offset(this->buffer, &first, 0);
112     gtk_text_buffer_place_cursor(this->buffer, &first);
113 }
114 
setColor(Color color)115 auto TextEditor::setColor(Color color) -> UndoAction* {
116     auto origColor = this->text->getColor();
117     this->text->setColor(color);
118 
119     repaintEditor();
120 
121     // This is a new text, so we don't need to create a undo action
122     if (this->ownText) {
123         return nullptr;
124     }
125 
126     auto* undo = new ColorUndoAction(gui->getPage(), gui->getPage()->getSelectedLayer());
127     undo->addStroke(this->text, origColor, color);
128 
129     return undo;
130 }
131 
setFont(XojFont font)132 void TextEditor::setFont(XojFont font) {
133     this->text->setFont(font);
134     TextView::updatePangoFont(this->layout, this->text);
135     this->repaintEditor();
136 }
137 
textCopyed()138 void TextEditor::textCopyed() { this->ownText = false; }
139 
iMCommitCallback(GtkIMContext * context,const gchar * str,TextEditor * te)140 void TextEditor::iMCommitCallback(GtkIMContext* context, const gchar* str, TextEditor* te) {
141     gtk_text_buffer_begin_user_action(te->buffer);
142     gboolean had_selection = gtk_text_buffer_get_selection_bounds(te->buffer, nullptr, nullptr);
143 
144     gtk_text_buffer_delete_selection(te->buffer, true, true);
145 
146     if (!strcmp(str, "\n")) {
147         if (!gtk_text_buffer_insert_interactive_at_cursor(te->buffer, "\n", 1, true)) {
148             gtk_widget_error_bell(te->widget);
149         } else {
150             te->contentsChanged(true);
151         }
152     } else {
153         if (!had_selection && te->cursorOverwrite) {
154             GtkTextIter insert;
155 
156             gtk_text_buffer_get_iter_at_mark(te->buffer, &insert, gtk_text_buffer_get_insert(te->buffer));
157             if (!gtk_text_iter_ends_line(&insert)) {
158                 te->deleteFromCursor(GTK_DELETE_CHARS, 1);
159             }
160         }
161 
162         if (!gtk_text_buffer_insert_interactive_at_cursor(te->buffer, str, -1, true)) {
163             gtk_widget_error_bell(te->widget);
164         }
165     }
166 
167     gtk_text_buffer_end_user_action(te->buffer);
168     te->repaintEditor();
169     te->contentsChanged();
170 }
171 
iMPreeditChangedCallback(GtkIMContext * context,TextEditor * te)172 void TextEditor::iMPreeditChangedCallback(GtkIMContext* context, TextEditor* te) {
173     gchar* str = nullptr;
174     PangoAttrList* attrs = nullptr;
175     gint cursor_pos = 0;
176     GtkTextIter iter;
177 
178     gtk_text_buffer_get_iter_at_mark(te->buffer, &iter, gtk_text_buffer_get_insert(te->buffer));
179 
180     /* Keypress events are passed to input method even if cursor position is
181      * not editable; so beep here if it's multi-key input sequence, input
182      * method will be reset in key-press-event handler.
183      */
184     gtk_im_context_get_preedit_string(context, &str, &attrs, &cursor_pos);
185 
186     if (attrs == nullptr) {
187         attrs = pango_attr_list_new();
188     }
189     if (te->preeditAttrList) {
190         pango_attr_list_unref(te->preeditAttrList);
191     }
192     te->preeditAttrList = attrs;
193     attrs = nullptr;
194 
195     if (str && str[0] && !gtk_text_iter_can_insert(&iter, true)) {
196         gtk_widget_error_bell(te->widget);
197         goto out;
198     }
199 
200     if (str != nullptr) {
201         te->preeditString = str;
202     } else {
203         te->preeditString = "";
204     }
205     te->preeditCursor = cursor_pos;
206     te->repaintEditor();
207     te->contentsChanged();
208 
209 out:
210 
211     g_free(str);
212 }
213 
iMRetrieveSurroundingCallback(GtkIMContext * context,TextEditor * te)214 auto TextEditor::iMRetrieveSurroundingCallback(GtkIMContext* context, TextEditor* te) -> bool {
215     GtkTextIter start;
216     GtkTextIter end;
217 
218     gtk_text_buffer_get_iter_at_mark(te->buffer, &start, gtk_text_buffer_get_insert(te->buffer));
219     end = start;
220 
221     gint pos = gtk_text_iter_get_line_index(&start);
222     gtk_text_iter_set_line_offset(&start, 0);
223     gtk_text_iter_forward_to_line_end(&end);
224 
225     gchar* text = gtk_text_iter_get_slice(&start, &end);
226     gtk_im_context_set_surrounding(context, text, -1, pos);
227     g_free(text);
228 
229     te->repaintEditor();
230     te->contentsChanged();
231     return true;
232 }
233 
imDeleteSurroundingCallback(GtkIMContext * context,gint offset,gint n_chars,TextEditor * te)234 auto TextEditor::imDeleteSurroundingCallback(GtkIMContext* context, gint offset, gint n_chars, TextEditor* te) -> bool {
235     GtkTextIter start;
236     GtkTextIter end;
237 
238     gtk_text_buffer_get_iter_at_mark(te->buffer, &start, gtk_text_buffer_get_insert(te->buffer));
239     end = start;
240 
241     gtk_text_iter_forward_chars(&start, offset);
242     gtk_text_iter_forward_chars(&end, offset + n_chars);
243 
244     gtk_text_buffer_delete_interactive(te->buffer, &start, &end, true);
245 
246     te->repaintEditor();
247     te->contentsChanged();
248 
249     return true;
250 }
251 
onKeyPressEvent(GdkEventKey * event)252 auto TextEditor::onKeyPressEvent(GdkEventKey* event) -> bool {
253     if (gtk_bindings_activate_event(G_OBJECT(this->textWidget), event)) {
254         return true;
255     }
256 
257     bool retval = false;
258     bool obscure = false;
259 
260     GtkTextIter iter;
261     GdkModifierType modifiers = gtk_accelerator_get_default_mod_mask();
262     GtkTextMark* insert = gtk_text_buffer_get_insert(this->buffer);
263     gtk_text_buffer_get_iter_at_mark(this->buffer, &iter, insert);
264     bool canInsert = gtk_text_iter_can_insert(&iter, true);
265     if (gtk_im_context_filter_keypress(this->imContext, event)) {
266         this->needImReset = true;
267         if (!canInsert) {
268             this->resetImContext();
269         }
270         obscure = canInsert;
271         retval = true;
272     } else if ((event->state & modifiers) == GDK_CONTROL_MASK) {
273         // Bold text
274         if (event->keyval == GDK_KEY_b || event->keyval == GDK_KEY_B) {
275             toggleBold();
276             return true;
277         }
278         // Increase text size
279         if (event->keyval == GDK_KEY_plus) {
280             incSize();
281             return true;
282         }
283         // Decrease text size
284         if (event->keyval == GDK_KEY_minus) {
285             decSize();
286             return true;
287         }
288     } else if (event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_ISO_Enter ||
289                event->keyval == GDK_KEY_KP_Enter) {
290         this->resetImContext();
291         iMCommitCallback(nullptr, "\n", this);
292 
293         obscure = true;
294         retval = true;
295     }
296     // Pass through Tab as literal tab, unless Control is held down
297     else if ((event->keyval == GDK_KEY_Tab || event->keyval == GDK_KEY_KP_Tab ||
298               event->keyval == GDK_KEY_ISO_Left_Tab) &&
299              !(event->state & GDK_CONTROL_MASK)) {
300         resetImContext();
301         iMCommitCallback(nullptr, "\t", this);
302         obscure = true;
303         retval = true;
304     } else {
305         retval = false;
306     }
307 
308     if (obscure) {
309         XournalppCursor* cursor = gui->getXournal()->getCursor();
310         cursor->setInvisible(true);
311     }
312 
313     return retval;
314 }
315 
onKeyReleaseEvent(GdkEventKey * event)316 auto TextEditor::onKeyReleaseEvent(GdkEventKey* event) -> bool {
317     GtkTextIter iter;
318 
319     GtkTextMark* insert = gtk_text_buffer_get_insert(this->buffer);
320     gtk_text_buffer_get_iter_at_mark(this->buffer, &iter, insert);
321     if (gtk_text_iter_can_insert(&iter, true) && gtk_im_context_filter_keypress(this->imContext, event)) {
322         this->needImReset = true;
323         return true;
324     }
325     return false;
326 }
327 
toggleOverwrite()328 void TextEditor::toggleOverwrite() {
329     this->cursorOverwrite = !this->cursorOverwrite;
330     repaintCursor();
331 }
332 
333 /**
334  * I know it's a bit rough and duplicated
335  * Improve that later on...
336  */
decSize()337 void TextEditor::decSize() {
338     XojFont& font = text->getFont();
339     double fontSize = font.getSize();
340     fontSize--;
341     font.setSize(fontSize);
342     setFont(font);
343 }
344 
incSize()345 void TextEditor::incSize() {
346     XojFont& font = text->getFont();
347     double fontSize = font.getSize();
348     fontSize++;
349     font.setSize(fontSize);
350     setFont(font);
351 }
352 
toggleBold()353 void TextEditor::toggleBold() {
354     // get the current/used font
355     XojFont& font = text->getFont();
356     string fontName = font.getName();
357 
358     std::size_t found = fontName.find("Bold");
359 
360     // toggle bold
361     if (found == string::npos) {
362         fontName = fontName + " Bold";
363     } else {
364         fontName = fontName.substr(0, found - 1);
365     }
366 
367     // commit changes
368     font.setName(fontName);
369     setFont(font);
370 
371     // this->repaintEditor();
372 }
373 
selectAtCursor(TextEditor::SelectType ty)374 void TextEditor::selectAtCursor(TextEditor::SelectType ty) {
375     GtkTextMark* mark = gtk_text_buffer_get_insert(this->buffer);
376     GtkTextIter startPos;
377     GtkTextIter endPos;
378     gtk_text_buffer_get_selection_bounds(this->buffer, &startPos, &endPos);
379     const auto searchFlag = GTK_TEXT_SEARCH_TEXT_ONLY;  // To be used to find double newlines
380 
381     switch (ty) {
382         case TextEditor::SelectType::word:
383             // Do nothing if cursor is over whitespace
384             GtkTextIter currentPos;
385             gtk_text_buffer_get_iter_at_mark(this->buffer, &currentPos, mark);
386             if (!gtk_text_iter_inside_word(&currentPos)) {
387                 return;
388             }
389 
390             if (!gtk_text_iter_starts_word(&currentPos)) {
391                 gtk_text_iter_backward_word_start(&startPos);
392             }
393             if (!gtk_text_iter_ends_word(&currentPos)) {
394                 gtk_text_iter_forward_word_end(&endPos);
395             }
396             break;
397         case TextEditor::SelectType::paragraph:
398             // Note that a GTK "paragraph" is a line, so there's no nice one-liner.
399             // We define a paragraph as text separated by double newlines.
400             while (!gtk_text_iter_is_start(&startPos)) {
401                 // There's no GTK function to go to line start, so do it manually.
402                 while (!gtk_text_iter_starts_line(&startPos)) {
403                     if (!gtk_text_iter_backward_word_start(&startPos)) {
404                         break;
405                     }
406                 }
407                 // Check for paragraph start
408                 GtkTextIter searchPos = startPos;
409                 gtk_text_iter_backward_chars(&searchPos, 2);
410                 if (gtk_text_iter_backward_search(&startPos, "\n\n", searchFlag, nullptr, nullptr, &searchPos)) {
411                     break;
412                 }
413                 gtk_text_iter_backward_line(&startPos);
414             }
415             while (!gtk_text_iter_ends_line(&endPos)) {
416                 gtk_text_iter_forward_to_line_end(&endPos);
417                 // Check for paragraph end
418                 GtkTextIter searchPos = endPos;
419                 gtk_text_iter_forward_chars(&searchPos, 2);
420                 if (gtk_text_iter_forward_search(&endPos, "\n\n", searchFlag, nullptr, nullptr, &searchPos)) {
421                     break;
422                 }
423                 gtk_text_iter_forward_line(&endPos);
424             }
425             break;
426         case TextEditor::SelectType::all:
427             gtk_text_buffer_get_bounds(this->buffer, &startPos, &endPos);
428             break;
429     }
430 
431     gtk_text_buffer_select_range(this->buffer, &startPos, &endPos);
432 
433     this->repaintEditor();
434 }
435 
moveCursor(GtkMovementStep step,int count,bool extendSelection)436 void TextEditor::moveCursor(GtkMovementStep step, int count, bool extendSelection) {
437     resetImContext();
438 
439     // Not possible, but we have to handle the events, else the page gets scrolled
440     //	if (step == GTK_MOVEMENT_PAGES) {
441     //		if (!gtk_text_view_scroll_pages(text_view, count, extend_selection))
442     //			gtk_widget_error_bell(GTK_WIDGET (text_view));
443     //
444     //		gtk_text_view_check_cursor_blink(text_view);
445     //		gtk_text_view_pend_cursor_blink(text_view);
446     //		return;
447     //	} else if (step == GTK_MOVEMENT_HORIZONTAL_PAGES) {
448     //		if (!gtk_text_view_scroll_hpages(text_view, count, extend_selection))
449     //			gtk_widget_error_bell(GTK_WIDGET (text_view));
450     //
451     //		gtk_text_view_check_cursor_blink(text_view);
452     //		gtk_text_view_pend_cursor_blink(text_view);
453     //		return;
454     //	}
455 
456     GtkTextIter insert;
457     gtk_text_buffer_get_iter_at_mark(this->buffer, &insert, gtk_text_buffer_get_insert(this->buffer));
458     GtkTextIter newplace = insert;
459 
460     bool updateVirtualCursor = true;
461 
462     switch (step) {
463         case GTK_MOVEMENT_LOGICAL_POSITIONS:  // not used!?
464             gtk_text_iter_forward_visible_cursor_positions(&newplace, count);
465             break;
466         case GTK_MOVEMENT_VISUAL_POSITIONS:
467             if (count < 0) {
468                 gtk_text_iter_backward_cursor_position(&newplace);
469             } else {
470                 gtk_text_iter_forward_cursor_position(&newplace);
471             }
472             break;
473 
474         case GTK_MOVEMENT_WORDS:
475             if (count < 0) {
476                 gtk_text_iter_backward_visible_word_starts(&newplace, -count);
477             } else if (count > 0) {
478                 if (!gtk_text_iter_forward_visible_word_ends(&newplace, count)) {
479                     gtk_text_iter_forward_to_line_end(&newplace);
480                 }
481             }
482             break;
483 
484         case GTK_MOVEMENT_DISPLAY_LINES:
485             updateVirtualCursor = false;
486             jumpALine(&newplace, count);
487             break;
488 
489         case GTK_MOVEMENT_PARAGRAPHS:
490             if (count > 0) {
491                 if (!gtk_text_iter_ends_line(&newplace)) {
492                     gtk_text_iter_forward_to_line_end(&newplace);
493                     --count;
494                 }
495                 gtk_text_iter_forward_visible_lines(&newplace, count);
496                 gtk_text_iter_forward_to_line_end(&newplace);
497             } else if (count < 0) {
498                 if (gtk_text_iter_get_line_offset(&newplace) > 0) {
499                     gtk_text_iter_set_line_offset(&newplace, 0);
500                 }
501                 gtk_text_iter_forward_visible_lines(&newplace, count);
502                 gtk_text_iter_set_line_offset(&newplace, 0);
503             }
504             break;
505 
506         case GTK_MOVEMENT_DISPLAY_LINE_ENDS:
507         case GTK_MOVEMENT_PARAGRAPH_ENDS:
508             if (count > 0) {
509                 if (!gtk_text_iter_ends_line(&newplace)) {
510                     gtk_text_iter_forward_to_line_end(&newplace);
511                 }
512             } else if (count < 0) {
513                 gtk_text_iter_set_line_offset(&newplace, 0);
514             }
515             break;
516 
517         case GTK_MOVEMENT_BUFFER_ENDS:
518             if (count > 0) {
519                 gtk_text_buffer_get_end_iter(this->buffer, &newplace);
520             } else if (count < 0) {
521                 gtk_text_buffer_get_iter_at_offset(this->buffer, &newplace, 0);
522             }
523             break;
524 
525         default:
526             break;
527     }
528 
529     // call moveCursor() even if the cursor hasn't moved, since it cancels the selection
530     moveCursor(&newplace, extendSelection);
531 
532     if (updateVirtualCursor) {
533         calcVirtualCursor();
534     }
535 
536     if (gtk_text_iter_equal(&insert, &newplace)) {
537         gtk_widget_error_bell(this->widget);
538     }
539 
540     if (this->cursorBlink) {
541         this->cursorVisible = false;
542         if (this->blinkTimeout) {
543             g_source_remove(this->blinkTimeout);
544         }
545         blinkCallback(this);
546     } else {
547         repaintCursor();
548     }
549 }
550 
findPos(GtkTextIter * iter,double xPos,double yPos)551 void TextEditor::findPos(GtkTextIter* iter, double xPos, double yPos) {
552     if (!this->layout) {
553         return;
554     }
555 
556     int index = 0;
557     if (!pango_layout_xy_to_index(this->layout, xPos * PANGO_SCALE, yPos * PANGO_SCALE, &index, nullptr)) {
558         index++;
559     }
560 
561     gtk_text_iter_set_offset(iter, getCharOffset(index));
562 }
563 
contentsChanged(bool forceCreateUndoAction)564 void TextEditor::contentsChanged(bool forceCreateUndoAction) {
565     string currentText = getText()->getText();
566 
567     // I know it's a little bit bulky, but ABS on subtracted size_t is a little bit unsafe
568     if (forceCreateUndoAction ||
569         ((lastText.length() >= currentText.length()) ? (lastText.length() - currentText.length()) :
570                                                        (currentText.length() - lastText.length())) > 100) {
571         if (!lastText.empty() && !this->undoActions.empty() &&
572             this->undoActions.front().get().getUndoText() != currentText) {
573             auto undo = std::make_unique<TextUndoAction>(gui->getPage(), gui->getPage()->getSelectedLayer(), this->text,
574                                                          lastText, this);
575             UndoRedoHandler* handler = gui->getXournal()->getControl()->getUndoRedoHandler();
576             this->undoActions.emplace_back(std::ref(*undo));
577             handler->addUndoAction(std::move(undo));
578         }
579         lastText = currentText;
580     }
581 }
582 
getFirstUndoAction()583 auto TextEditor::getFirstUndoAction() -> UndoAction* {
584     if (!this->undoActions.empty()) {
585         return &this->undoActions.front().get();
586     }
587     return nullptr;
588 }
589 
markPos(double x,double y,bool extendSelection)590 void TextEditor::markPos(double x, double y, bool extendSelection) {
591     if (this->layout == nullptr) {
592         this->markPosX = x;
593         this->markPosY = y;
594         this->markPosExtendSelection = extendSelection;
595         this->markPosQueue = true;
596         return;
597     }
598     GtkTextIter iter;
599     GtkTextMark* insert = gtk_text_buffer_get_insert(this->buffer);
600     gtk_text_buffer_get_iter_at_mark(this->buffer, &iter, insert);
601     GtkTextIter newplace = iter;
602 
603     findPos(&newplace, x, y);
604 
605     // Noting changed
606     if (gtk_text_iter_equal(&newplace, &iter)) {
607         return;
608     }
609     moveCursor(&newplace, extendSelection);
610     calcVirtualCursor();
611     repaintCursor();
612 }
613 
mousePressed(double x,double y)614 void TextEditor::mousePressed(double x, double y) {
615     this->mouseDown = true;
616     markPos(x, y, false);
617 }
618 
mouseMoved(double x,double y)619 void TextEditor::mouseMoved(double x, double y) {
620     if (this->mouseDown) {
621         markPos(x, y, true);
622     }
623 }
624 
mouseReleased()625 void TextEditor::mouseReleased() { this->mouseDown = false; }
626 
jumpALine(GtkTextIter * textIter,int count)627 void TextEditor::jumpALine(GtkTextIter* textIter, int count) {
628     int cursorLine = gtk_text_iter_get_line(textIter);
629 
630     if (cursorLine + count < 0) {
631         return;
632     }
633 
634     PangoLayoutLine* line = pango_layout_get_line(this->layout, cursorLine + count);
635     if (line == nullptr) {
636         return;
637     }
638 
639     int index = 0;
640     pango_layout_line_x_to_index(line, this->virtualCursor * PANGO_SCALE, &index, nullptr);
641 
642     index = getCharOffset(index);
643 
644     gtk_text_iter_set_offset(textIter, index);
645 }
646 
calcVirtualCursor()647 void TextEditor::calcVirtualCursor() {
648     this->virtualCursor = 0;
649     GtkTextIter cursorIter = {nullptr};
650     GtkTextMark* cursor = gtk_text_buffer_get_insert(this->buffer);
651     gtk_text_buffer_get_iter_at_mark(this->buffer, &cursorIter, cursor);
652 
653     int offset = getByteOffset(gtk_text_iter_get_offset(&cursorIter));
654 
655     PangoRectangle rect = {0};
656     pango_layout_index_to_pos(this->layout, offset, &rect);
657     this->virtualCursor = (static_cast<double>(rect.x)) / PANGO_SCALE;
658 }
659 
moveCursor(const GtkTextIter * newLocation,gboolean extendSelection)660 void TextEditor::moveCursor(const GtkTextIter* newLocation, gboolean extendSelection) {
661     Control* control = gui->getXournal()->getControl();
662 
663     if (extendSelection) {
664         gtk_text_buffer_move_mark_by_name(this->buffer, "insert", newLocation);
665         control->setCopyPasteEnabled(true);
666     } else {
667         gtk_text_buffer_place_cursor(this->buffer, newLocation);
668         control->setCopyPasteEnabled(false);
669     }
670 
671     this->repaintEditor();
672 }
673 
whitespace(gunichar ch,gpointer user_data)674 static auto whitespace(gunichar ch, gpointer user_data) -> gboolean { return (ch == ' ' || ch == '\t'); }
675 
not_whitespace(gunichar ch,gpointer user_data)676 static auto not_whitespace(gunichar ch, gpointer user_data) -> gboolean { return !whitespace(ch, user_data); }
677 
find_whitepace_region(const GtkTextIter * center,GtkTextIter * start,GtkTextIter * end)678 static auto find_whitepace_region(const GtkTextIter* center, GtkTextIter* start, GtkTextIter* end) -> gboolean {
679     *start = *center;
680     *end = *center;
681 
682     if (gtk_text_iter_backward_find_char(start, not_whitespace, nullptr, nullptr)) {
683         gtk_text_iter_forward_char(start); /* we want the first whitespace... */
684     }
685     if (whitespace(gtk_text_iter_get_char(end), nullptr)) {
686         gtk_text_iter_forward_find_char(end, not_whitespace, nullptr, nullptr);
687     }
688 
689     return !gtk_text_iter_equal(start, end);
690 }
691 
deleteFromCursor(GtkDeleteType type,int count)692 void TextEditor::deleteFromCursor(GtkDeleteType type, int count) {
693     GtkTextIter insert;
694     // gboolean leave_one = false; // not needed
695 
696     this->resetImContext();
697 
698     if (type == GTK_DELETE_CHARS) {
699         // Char delete deletes the selection, if one exists
700         if (gtk_text_buffer_delete_selection(this->buffer, true, true)) {
701             this->contentsChanged(true);
702             this->repaintEditor();
703             return;
704         }
705     }
706 
707     gtk_text_buffer_get_iter_at_mark(this->buffer, &insert, gtk_text_buffer_get_insert(this->buffer));
708 
709     GtkTextIter start = insert;
710     GtkTextIter end = insert;
711 
712     switch (type) {
713         case GTK_DELETE_CHARS:
714             gtk_text_iter_forward_cursor_positions(&end, count);
715             break;
716 
717         case GTK_DELETE_WORD_ENDS:
718             if (count > 0) {
719                 gtk_text_iter_forward_word_ends(&end, count);
720             } else if (count < 0) {
721                 gtk_text_iter_backward_word_starts(&start, 0 - count);
722             }
723             break;
724 
725         case GTK_DELETE_WORDS:
726             break;
727 
728         case GTK_DELETE_DISPLAY_LINE_ENDS:
729             break;
730 
731         case GTK_DELETE_DISPLAY_LINES:
732             break;
733 
734         case GTK_DELETE_PARAGRAPH_ENDS:
735             if (count > 0) {
736                 /* If we're already at a newline, we need to
737                  * simply delete that newline, instead of
738                  * moving to the next one.
739                  */
740                 if (gtk_text_iter_ends_line(&end)) {
741                     gtk_text_iter_forward_line(&end);
742                     --count;
743                 }
744 
745                 while (count > 0) {
746                     if (!gtk_text_iter_forward_to_line_end(&end)) {
747                         break;
748                     }
749 
750                     --count;
751                 }
752             } else if (count < 0) {
753                 if (gtk_text_iter_starts_line(&start)) {
754                     gtk_text_iter_backward_line(&start);
755                     if (!gtk_text_iter_ends_line(&end)) {
756                         gtk_text_iter_forward_to_line_end(&start);
757                     }
758                 } else {
759                     gtk_text_iter_set_line_offset(&start, 0);
760                 }
761                 ++count;
762 
763                 gtk_text_iter_backward_lines(&start, -count);
764             }
765             break;
766 
767         case GTK_DELETE_PARAGRAPHS:
768             if (count > 0) {
769                 gtk_text_iter_set_line_offset(&start, 0);
770                 gtk_text_iter_forward_to_line_end(&end);
771 
772                 /* Do the lines beyond the first. */
773                 while (count > 1) {
774                     gtk_text_iter_forward_to_line_end(&end);
775                     --count;
776                 }
777             }
778 
779             break;
780 
781         case GTK_DELETE_WHITESPACE: {
782             find_whitepace_region(&insert, &start, &end);
783         } break;
784 
785         default:
786             break;
787     }
788 
789     if (!gtk_text_iter_equal(&start, &end)) {
790         gtk_text_buffer_begin_user_action(this->buffer);
791 
792         if (gtk_text_buffer_delete_interactive(this->buffer, &start, &end, true)) {
793             /*if (leave_one) // leave_one is statically false
794             {
795                 gtk_text_buffer_insert_interactive_at_cursor(this->buffer, " ", 1, true);
796             }*/
797         } else {
798             gtk_widget_error_bell(this->widget);
799         }
800 
801         gtk_text_buffer_end_user_action(this->buffer);
802     } else {
803         gtk_widget_error_bell(this->widget);
804     }
805 
806     this->contentsChanged();
807     this->repaintEditor();
808 }
809 
backspace()810 void TextEditor::backspace() {
811     GtkTextIter insert;
812 
813     resetImContext();
814 
815     // Backspace deletes the selection, if one exists
816     if (gtk_text_buffer_delete_selection(this->buffer, true, true)) {
817         this->repaintEditor();
818         this->contentsChanged();
819         return;
820     }
821 
822     gtk_text_buffer_get_iter_at_mark(this->buffer, &insert, gtk_text_buffer_get_insert(this->buffer));
823 
824     if (gtk_text_buffer_backspace(this->buffer, &insert, true, true)) {
825         this->repaintEditor();
826         this->contentsChanged();
827     } else {
828         gtk_widget_error_bell(this->widget);
829     }
830 }
831 
getSelection()832 auto TextEditor::getSelection() -> string {
833     GtkTextIter start, end;
834     char* text = nullptr;
835     string s;
836 
837     if (gtk_text_buffer_get_selection_bounds(buffer, &start, &end)) {
838         text = gtk_text_iter_get_text(&start, &end);
839         s = text;
840         g_free(text);
841     }
842     return s;
843 }
844 
copyToCliboard()845 void TextEditor::copyToCliboard() {
846     GtkClipboard* clipboard = gtk_widget_get_clipboard(this->widget, GDK_SELECTION_CLIPBOARD);
847     gtk_text_buffer_copy_clipboard(this->buffer, clipboard);
848 }
849 
cutToClipboard()850 void TextEditor::cutToClipboard() {
851     GtkClipboard* clipboard = gtk_widget_get_clipboard(this->widget, GDK_SELECTION_CLIPBOARD);
852     gtk_text_buffer_cut_clipboard(this->buffer, clipboard, true);
853 
854     this->repaintEditor();
855     this->contentsChanged(true);
856 }
857 
pasteFromClipboard()858 void TextEditor::pasteFromClipboard() {
859     GtkClipboard* clipboard = gtk_widget_get_clipboard(this->widget, GDK_SELECTION_CLIPBOARD);
860     gtk_text_buffer_paste_clipboard(this->buffer, clipboard, nullptr, true);
861 }
862 
bufferPasteDoneCallback(GtkTextBuffer * buffer,GtkClipboard * clipboard,TextEditor * te)863 void TextEditor::bufferPasteDoneCallback(GtkTextBuffer* buffer, GtkClipboard* clipboard, TextEditor* te) {
864     te->repaintEditor();
865     te->contentsChanged(true);
866 }
867 
resetImContext()868 void TextEditor::resetImContext() {
869     if (this->needImReset) {
870         this->needImReset = false;
871         gtk_im_context_reset(this->imContext);
872     }
873 }
874 
repaintCursor()875 void TextEditor::repaintCursor() {
876     double x = this->text->getX();
877     double y = this->text->getY();
878     this->gui->repaintArea(x, y, x + this->text->getElementWidth(), y + this->text->getElementHeight());
879 }
880 
881 #define CURSOR_ON_MULTIPLIER 2
882 #define CURSOR_OFF_MULTIPLIER 1
883 #define CURSOR_PEND_MULTIPLIER 3
884 #define CURSOR_DIVIDER 3
885 
886 /*
887  * Blink!
888  */
blinkCallback(TextEditor * te)889 auto TextEditor::blinkCallback(TextEditor* te) -> gint {
890     if (te->cursorVisible) {
891         te->blinkTimeout = gdk_threads_add_timeout(te->cursorBlinkTime * CURSOR_OFF_MULTIPLIER / CURSOR_DIVIDER,
892                                                    reinterpret_cast<GSourceFunc>(blinkCallback), te);
893     } else {
894         te->blinkTimeout = gdk_threads_add_timeout(te->cursorBlinkTime * CURSOR_ON_MULTIPLIER / CURSOR_DIVIDER,
895                                                    reinterpret_cast<GSourceFunc>(blinkCallback), te);
896     }
897 
898     te->cursorVisible = !te->cursorVisible;
899 
900     te->repaintCursor();
901 
902     // Remove ourselves
903     return false;
904 }
905 
repaintEditor()906 void TextEditor::repaintEditor() { this->gui->repaintPage(); }
907 
908 /**
909  * Calculate the UTF-8 Char offset into a byte offset.
910  */
getByteOffset(int charOffset)911 auto TextEditor::getByteOffset(int charOffset) -> int {
912     const char* text = pango_layout_get_text(this->layout);
913     return g_utf8_offset_to_pointer(text, charOffset) - text;
914 }
915 
916 /**
917  * Calculate the UTF-8 Char byte offset into a char offset.
918  */
getCharOffset(int byteOffset)919 auto TextEditor::getCharOffset(int byteOffset) -> int {
920     const char* text = pango_layout_get_text(this->layout);
921 
922     return g_utf8_pointer_to_offset(text, text + byteOffset);
923 }
924 
drawCursor(cairo_t * cr,double x,double y,double height,double zoom)925 void TextEditor::drawCursor(cairo_t* cr, double x, double y, double height, double zoom) {
926     double cw = 2 / zoom;
927     double dX = 0;
928     if (this->cursorOverwrite) {
929         dX = -cw / 2;
930         cw *= 2;
931     }
932 
933     // Not draw cursor if a move is pending
934     if (!this->markPosQueue) {
935         if (this->cursorVisible) {
936             cairo_save(cr);
937 
938             cairo_set_operator(cr, CAIRO_OPERATOR_DIFFERENCE);
939             cairo_set_source_rgb(cr, 1, 1, 1);
940             cairo_rectangle(cr, x + dX, y, cw, height);
941             cairo_fill(cr);
942 
943             cairo_restore(cr);
944         }
945     }
946     DocumentView::applyColor(cr, this->text);
947 }
948 
paint(cairo_t * cr,GdkRectangle * repaintRect,double zoom)949 void TextEditor::paint(cairo_t* cr, GdkRectangle* repaintRect, double zoom) {
950     GdkRGBA selectionColor = this->gui->getSelectionColor();
951 
952     cairo_save(cr);
953 
954     DocumentView::applyColor(cr, this->text);
955 
956     GtkTextIter cursorIter = {nullptr};
957     GtkTextMark* cursor = gtk_text_buffer_get_insert(this->buffer);
958     gtk_text_buffer_get_iter_at_mark(this->buffer, &cursorIter, cursor);
959 
960     double x0 = this->text->getX();
961     double y0 = this->text->getY();
962     cairo_translate(cr, x0, y0);
963     double x1 = this->gui->getX();
964     double y1 = this->gui->getY();
965 
966     if (this->layout == nullptr) {
967         this->layout = TextView::initPango(cr, this->text);
968     }
969 
970     if (!this->preeditString.empty()) {
971         string text = this->text->getText();
972         int offset = gtk_text_iter_get_offset(&cursorIter);
973         int pos = gtk_text_iter_get_line_index(&cursorIter);
974 
975         for (gtk_text_iter_set_line_index(&cursorIter, 0); gtk_text_iter_backward_line(&cursorIter);) {
976             pos += gtk_text_iter_get_bytes_in_line(&cursorIter);
977         }
978         gtk_text_iter_set_offset(&cursorIter, offset);
979         string txt = text.substr(0, pos) + preeditString + text.substr(pos);
980 
981         PangoAttrList* attrlist = pango_attr_list_new();
982         PangoAttrList* preedit_attrlist = this->preeditAttrList;
983         pango_attr_list_splice(attrlist, preedit_attrlist, pos, preeditString.length());
984         pango_layout_set_attributes(this->layout, attrlist);
985         pango_attr_list_unref(attrlist);
986         attrlist = nullptr;
987         pango_layout_set_text(this->layout, txt.c_str(), txt.length());
988     } else {
989         string txt = this->text->getText();
990         pango_layout_set_text(this->layout, txt.c_str(), txt.length());
991 
992         GtkTextIter start;
993         GtkTextIter end;
994         bool hasSelection = gtk_text_buffer_get_selection_bounds(this->buffer, &start, &end);
995 
996         if (hasSelection) {
997             auto selectionColorU16 = Util::GdkRGBA_to_ColorU16(selectionColor);
998             PangoAttribute* attrib =
999                     pango_attr_background_new(selectionColorU16.red, selectionColorU16.green, selectionColorU16.blue);
1000             attrib->start_index = getByteOffset(gtk_text_iter_get_offset(&start));
1001             attrib->end_index = getByteOffset(gtk_text_iter_get_offset(&end));
1002 
1003             PangoAttrList* attrlist = pango_attr_list_new();
1004             pango_attr_list_insert(attrlist, attrib);
1005             pango_layout_set_attributes(this->layout, attrlist);
1006             pango_attr_list_unref(attrlist);
1007             attrlist = nullptr;
1008         } else {
1009             // remove all attributes
1010             PangoAttrList* attrlist = pango_attr_list_new();
1011             pango_layout_set_attributes(this->layout, attrlist);
1012             pango_attr_list_unref(attrlist);
1013             attrlist = nullptr;
1014         }
1015     }
1016 
1017     pango_cairo_show_layout(cr, this->layout);
1018     int w = 0;
1019     int h = 0;
1020     pango_layout_get_size(this->layout, &w, &h);
1021     double width = (static_cast<double>(w)) / PANGO_SCALE;
1022     double height = (static_cast<double>(h)) / PANGO_SCALE;
1023 
1024     int offset = gtk_text_iter_get_offset(&cursorIter);
1025     PangoRectangle rect = {0};
1026     int pcursInd = 0;
1027     if (!this->preeditString.empty() && this->preeditCursor != 0) {
1028         const gchar* preeditText = this->preeditString.c_str();
1029         pcursInd = g_utf8_offset_to_pointer(preeditText, preeditCursor) - preeditText;
1030     }
1031     int pangoOffset = getByteOffset(offset) + pcursInd;
1032     pango_layout_index_to_pos(this->layout, pangoOffset, &rect);
1033     double cX = (static_cast<double>(rect.x)) / PANGO_SCALE;
1034     double cY = (static_cast<double>(rect.y)) / PANGO_SCALE;
1035     double cHeight = (static_cast<double>(rect.height)) / PANGO_SCALE;
1036 
1037     drawCursor(cr, cX, cY, cHeight, zoom);
1038 
1039     cairo_restore(cr);
1040 
1041     // set the line always the same size on display
1042     cairo_set_line_width(cr, 1 / zoom);
1043     gdk_cairo_set_source_rgba(cr, &selectionColor);
1044 
1045     cairo_rectangle(cr, x0 - 5 / zoom, y0 - 5 / zoom, width + 10 / zoom, height + 10 / zoom);
1046     cairo_stroke(cr);
1047 
1048     // Notify the IM of the app's window and cursor position.
1049     gtk_im_context_set_client_window(this->imContext, gtk_widget_get_window(this->widget));
1050     GdkRectangle cursorRect;
1051     cursorRect.x = static_cast<int>(zoom * x0 + x1 + zoom * cX);
1052     cursorRect.y = static_cast<int>(zoom * y0 + y1 + zoom * cY);
1053     cursorRect.height = static_cast<int>(zoom * cHeight);
1054     // // The next setting treat the text box as if it were a cursor rectangle.
1055     // cursorRect.x = static_cast<int>(zoom * x0 + x1 - 10);
1056     // cursorRect.y = static_cast<int>(zoom * y0 + y1 - 10);
1057     // cursorRect.width = static_cast<int>(zoom * width + 20);
1058     // cursorRect.height = static_cast<int>(zoom * height + 20);
1059     // // This is also useful, so it is good to make it user's preference.
1060     gtk_im_context_set_cursor_location(this->imContext, &cursorRect);
1061 
1062     this->text->setWidth(width);
1063     this->text->setHeight(height);
1064 
1065     if (this->markPosQueue) {
1066         this->markPosQueue = false;
1067         markPos(this->markPosX, this->markPosY, this->markPosExtendSelection);
1068     }
1069 }
1070