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, ¤tPos, mark);
386 if (!gtk_text_iter_inside_word(¤tPos)) {
387 return;
388 }
389
390 if (!gtk_text_iter_starts_word(¤tPos)) {
391 gtk_text_iter_backward_word_start(&startPos);
392 }
393 if (!gtk_text_iter_ends_word(¤tPos)) {
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