diff --git a/src/core/control/ToolHandler.cpp b/src/core/control/ToolHandler.cpp index 06f1416f6ff8..69afcf58f9a9 100644 --- a/src/core/control/ToolHandler.cpp +++ b/src/core/control/ToolHandler.cpp @@ -552,17 +552,18 @@ auto ToolHandler::isSinglePageTool() -> bool { ToolType toolType = this->getToolType(); DrawingType drawingType = this->getDrawingType(); - return toolType == (TOOL_PEN && (drawingType == DRAWING_TYPE_ARROW || drawingType == DRAWING_TYPE_ELLIPSE || - drawingType == DRAWING_TYPE_COORDINATE_SYSTEM || - drawingType == DRAWING_TYPE_LINE || drawingType == DRAWING_TYPE_RECTANGLE)) || - drawingType == DRAWING_TYPE_SPLINE || toolType == TOOL_SELECT_REGION || toolType == TOOL_SELECT_RECT || - toolType == TOOL_SELECT_OBJECT || toolType == TOOL_DRAW_RECT || toolType == TOOL_DRAW_ELLIPSE || - toolType == TOOL_DRAW_COORDINATE_SYSTEM || toolType == TOOL_DRAW_ARROW || - toolType == TOOL_DRAW_DOUBLE_ARROW || toolType == TOOL_FLOATING_TOOLBOX || toolType == TOOL_DRAW_SPLINE || - toolType == TOOL_SELECT_PDF_TEXT_LINEAR || toolType == TOOL_SELECT_PDF_TEXT_RECT; + return ((toolType == TOOL_PEN || toolType == TOOL_HIGHLIGHTER) && + (drawingType == DRAWING_TYPE_ARROW || drawingType == DRAWING_TYPE_DOUBLE_ARROW || + drawingType == DRAWING_TYPE_ELLIPSE || drawingType == DRAWING_TYPE_COORDINATE_SYSTEM || + drawingType == DRAWING_TYPE_LINE || drawingType == DRAWING_TYPE_RECTANGLE || + drawingType == DRAWING_TYPE_SPLINE)) || + toolType == TOOL_SELECT_REGION || toolType == TOOL_SELECT_RECT || toolType == TOOL_SELECT_OBJECT || + toolType == TOOL_DRAW_RECT || toolType == TOOL_DRAW_ELLIPSE || toolType == TOOL_DRAW_COORDINATE_SYSTEM || + toolType == TOOL_DRAW_ARROW || toolType == TOOL_DRAW_DOUBLE_ARROW || toolType == TOOL_FLOATING_TOOLBOX || + toolType == TOOL_DRAW_SPLINE || toolType == TOOL_SELECT_PDF_TEXT_LINEAR || + toolType == TOOL_SELECT_PDF_TEXT_RECT; } - auto ToolHandler::getSelectedTool(SelectedTool selectedTool) -> Tool* { switch (selectedTool) { case SelectedTool::active: diff --git a/src/core/gui/PageView.cpp b/src/core/gui/PageView.cpp index cc9868ba68b4..ce419147c023 100644 --- a/src/core/gui/PageView.cpp +++ b/src/core/gui/PageView.cpp @@ -272,7 +272,7 @@ void XojPageView::startText(double x, double y) { this->textEditor->mousePressed(x - text->getX(), y - text->getY()); } - this->rerenderPage(); + this->rerenderElement(text); } } diff --git a/src/core/gui/TextEditor.cpp b/src/core/gui/TextEditor.cpp index 1a196a7965f8..5d645a92b27c 100644 --- a/src/core/gui/TextEditor.cpp +++ b/src/core/gui/TextEditor.cpp @@ -34,6 +34,8 @@ TextEditor::TextEditor(XojPageView* gui, GtkWidget* widget, Text* text, bool own this->textWidget = gtk_xoj_int_txt_new(this); this->lastText = text->getText(); + this->previousBoundingBox = text->boundingRect(); + this->buffer = gtk_text_buffer_new(nullptr); string txt = this->text->getText(); gtk_text_buffer_set_text(this->buffer, txt.c_str(), -1); @@ -180,8 +182,8 @@ void TextEditor::iMCommitCallback(GtkIMContext* context, const gchar* str, TextE } gtk_text_buffer_end_user_action(te->buffer); - te->repaintEditor(); te->contentsChanged(); + te->repaintEditor(); } void TextEditor::iMPreeditChangedCallback(GtkIMContext* context, TextEditor* te) { @@ -218,8 +220,8 @@ void TextEditor::iMPreeditChangedCallback(GtkIMContext* context, TextEditor* te) te->preeditString = ""; } te->preeditCursor = cursor_pos; - te->repaintEditor(); te->contentsChanged(); + te->repaintEditor(); out: @@ -241,8 +243,8 @@ auto TextEditor::iMRetrieveSurroundingCallback(GtkIMContext* context, TextEditor gtk_im_context_set_surrounding(context, text, -1, pos); g_free(text); - te->repaintEditor(); te->contentsChanged(); + te->repaintEditor(); return true; } @@ -258,8 +260,8 @@ auto TextEditor::imDeleteSurroundingCallback(GtkIMContext* context, gint offset, gtk_text_buffer_delete_interactive(te->buffer, &start, &end, true); - te->repaintEditor(); te->contentsChanged(); + te->repaintEditor(); return true; } @@ -829,16 +831,16 @@ void TextEditor::backspace() { // Backspace deletes the selection, if one exists if (gtk_text_buffer_delete_selection(this->buffer, true, true)) { - this->repaintEditor(); this->contentsChanged(); + this->repaintEditor(); return; } gtk_text_buffer_get_iter_at_mark(this->buffer, &insert, gtk_text_buffer_get_insert(this->buffer)); if (gtk_text_buffer_backspace(this->buffer, &insert, true, true)) { - this->repaintEditor(); this->contentsChanged(); + this->repaintEditor(); } else { gtk_widget_error_bell(this->widget); } @@ -866,8 +868,8 @@ void TextEditor::cutToClipboard() { GtkClipboard* clipboard = gtk_widget_get_clipboard(this->widget, GDK_SELECTION_CLIPBOARD); gtk_text_buffer_cut_clipboard(this->buffer, clipboard, true); - this->repaintEditor(); this->contentsChanged(true); + this->repaintEditor(); } void TextEditor::pasteFromClipboard() { @@ -876,8 +878,8 @@ void TextEditor::pasteFromClipboard() { } void TextEditor::bufferPasteDoneCallback(GtkTextBuffer* buffer, GtkClipboard* clipboard, TextEditor* te) { - te->repaintEditor(); te->contentsChanged(true); + te->repaintEditor(); } void TextEditor::resetImContext() { @@ -887,11 +889,7 @@ void TextEditor::resetImContext() { } } -void TextEditor::repaintCursor() { - double x = this->text->getX(); - double y = this->text->getY(); - this->gui->repaintArea(x, y, x + this->text->getElementWidth(), y + this->text->getElementHeight()); -} +void TextEditor::repaintCursor() { this->gui->repaintElement(this->text); } #define CURSOR_ON_MULTIPLIER 2 #define CURSOR_OFF_MULTIPLIER 1 @@ -918,9 +916,78 @@ auto TextEditor::blinkCallback(TextEditor* te) -> gint { return false; } +void TextEditor::setTextToPangoLayout(PangoLayout* pl) const { + std::string txt = this->text->getText(); + + if (!this->preeditString.empty()) { + // When using an Input Method, we need to insert the preeditString into the text at the cursor location + + // Get the byte position of the cursor in the string, so we can insert at the right place + int pos = 0; + { + // Get an iterator at the cursor location + GtkTextIter it = {nullptr}; + GtkTextMark* cursor = gtk_text_buffer_get_insert(this->buffer); + gtk_text_buffer_get_iter_at_mark(this->buffer, &it, cursor); + // Bytes from beginning of line to iterator + pos = gtk_text_iter_get_line_index(&it); + gtk_text_iter_set_line_index(&it, 0); + // Count bytes of previous lines + while (gtk_text_iter_backward_line(&it)) { + pos += gtk_text_iter_get_bytes_in_line(&it); + } + } + txt.insert(static_cast(pos), this->preeditString); + + PangoAttrList* attrlist = pango_attr_list_new(); + PangoAttrList* preedit_attrlist = this->preeditAttrList; + pango_attr_list_splice(attrlist, preedit_attrlist, pos, static_cast(preeditString.length())); + pango_layout_set_attributes(pl, attrlist); + pango_attr_list_unref(attrlist); + attrlist = nullptr; + } + pango_layout_set_text(pl, txt.c_str(), static_cast(txt.length())); +} + +auto TextEditor::computeBoundingRect() -> xoj::util::Rectangle { + /* + * We draw on a fake surface to get the size of the printed text + * See also TextView::calcSize + * + * NB: we cannot rely on TextView::calcSize directly, since it would not take the size changes due to the IM + * preeditString into account. + */ + cairo_surface_t* surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1); + cairo_t* cr = cairo_create(surface); + auto* textElement = this->getText(); + + PangoLayout* pl = xoj::view::TextView::initPango(cr, textElement); + + setTextToPangoLayout(pl); + + int w = 0; + int h = 0; + pango_layout_get_size(pl, &w, &h); + double width = (static_cast(w)) / PANGO_SCALE; + double height = (static_cast(h)) / PANGO_SCALE; + g_object_unref(pl); + + cairo_destroy(cr); + cairo_surface_destroy(surface); + + return xoj::util::Rectangle(textElement->getX(), textElement->getY(), width, height); +} + + void TextEditor::repaintEditor() { - auto rect = text->boundingRect(); - this->gui->rerenderRect(rect.x, rect.y, rect.width, rect.height); + auto rect = this->computeBoundingRect(); + this->previousBoundingBox.unite(rect); + const double zoom = this->gui->getXournal()->getZoom(); + const double padding = (BORDER_WIDTH_IN_PIXELS + PADDING_IN_PIXELS) / zoom; + this->gui->repaintRect(this->previousBoundingBox.x - padding, this->previousBoundingBox.y - padding, + this->previousBoundingBox.width + 2.0 * padding, + this->previousBoundingBox.height + 2.0 * padding); + this->previousBoundingBox = rect; } /** @@ -971,10 +1038,6 @@ void TextEditor::paint(cairo_t* cr, double zoom) { Util::cairo_set_source_rgbi(cr, this->text->getColor()); - GtkTextIter cursorIter = {nullptr}; - GtkTextMark* cursor = gtk_text_buffer_get_insert(this->buffer); - gtk_text_buffer_get_iter_at_mark(this->buffer, &cursorIter, cursor); - double x0 = this->text->getX(); double y0 = this->text->getY(); cairo_translate(cr, x0, y0); @@ -985,28 +1048,9 @@ void TextEditor::paint(cairo_t* cr, double zoom) { this->layout = xoj::view::TextView::initPango(cr, this->text); } - if (!this->preeditString.empty()) { - string text = this->text->getText(); - int offset = gtk_text_iter_get_offset(&cursorIter); - int pos = gtk_text_iter_get_line_index(&cursorIter); - - for (gtk_text_iter_set_line_index(&cursorIter, 0); gtk_text_iter_backward_line(&cursorIter);) { - pos += gtk_text_iter_get_bytes_in_line(&cursorIter); - } - gtk_text_iter_set_offset(&cursorIter, offset); - string txt = text.substr(0, pos) + preeditString + text.substr(pos); - - PangoAttrList* attrlist = pango_attr_list_new(); - PangoAttrList* preedit_attrlist = this->preeditAttrList; - pango_attr_list_splice(attrlist, preedit_attrlist, pos, preeditString.length()); - pango_layout_set_attributes(this->layout, attrlist); - pango_attr_list_unref(attrlist); - attrlist = nullptr; - pango_layout_set_text(this->layout, txt.c_str(), txt.length()); - } else { - string txt = this->text->getText(); - pango_layout_set_text(this->layout, txt.c_str(), txt.length()); + this->setTextToPangoLayout(this->layout); + if (this->preeditString.empty()) { GtkTextIter start; GtkTextIter end; bool hasSelection = gtk_text_buffer_get_selection_bounds(this->buffer, &start, &end); @@ -1039,6 +1083,11 @@ void TextEditor::paint(cairo_t* cr, double zoom) { double width = (static_cast(w)) / PANGO_SCALE; double height = (static_cast(h)) / PANGO_SCALE; + + GtkTextIter cursorIter = {nullptr}; + GtkTextMark* cursor = gtk_text_buffer_get_insert(this->buffer); + gtk_text_buffer_get_iter_at_mark(this->buffer, &cursorIter, cursor); + int offset = gtk_text_iter_get_offset(&cursorIter); PangoRectangle rect = {0}; int pcursInd = 0; @@ -1057,10 +1106,11 @@ void TextEditor::paint(cairo_t* cr, double zoom) { cairo_restore(cr); // set the line always the same size on display - cairo_set_line_width(cr, 1 / zoom); + cairo_set_line_width(cr, BORDER_WIDTH_IN_PIXELS / zoom); gdk_cairo_set_source_rgba(cr, &selectionColor); - cairo_rectangle(cr, x0 - 5 / zoom, y0 - 5 / zoom, width + 10 / zoom, height + 10 / zoom); + cairo_rectangle(cr, x0 - PADDING_IN_PIXELS / zoom, y0 - PADDING_IN_PIXELS / zoom, + width + 2 * PADDING_IN_PIXELS / zoom, height + 2 * PADDING_IN_PIXELS / zoom); cairo_stroke(cr); // Notify the IM of the app's window and cursor position. diff --git a/src/core/gui/TextEditor.h b/src/core/gui/TextEditor.h index 90ee51967d68..1a039a40c368 100644 --- a/src/core/gui/TextEditor.h +++ b/src/core/gui/TextEditor.h @@ -21,6 +21,7 @@ #include // for cairo_t, PangoAttrList, PangoLayout #include "util/Color.h" // for Color +#include "util/Rectangle.h" class XojPageView; class Text; @@ -68,6 +69,14 @@ class TextEditor { UndoAction* setColor(Color color); private: + /** + * @brief Add the text to the providedd Pango layout. + * The added text contains both this->text, and the preedit string of the Input Method (this->preeditstring) + * This function also sets up the attributes of the preedit string (typically underlined) + */ + void setTextToPangoLayout(PangoLayout* pl) const; + + xoj::util::Rectangle computeBoundingRect(); void repaintEditor(); void drawCursor(cairo_t* cr, double x, double y, double height, double zoom); void repaintCursor(); @@ -115,6 +124,14 @@ class TextEditor { double markPosX = 0; double markPosY = 0; + /** + * Tracks the bounding box of the editor from the last render. + * + * Because adding or deleting lines may cause the size of the bounding box to change, + * we need to rerender the union of the current and previous bboxes. + */ + xoj::util::Rectangle previousBoundingBox; + bool cursorBlink = true; int cursorBlinkTime = 0; int cursorBlinkTimeout = 0; @@ -127,4 +144,9 @@ class TextEditor { bool mouseDown = false; bool cursorOverwrite = false; bool cursorVisible = false; + + // Padding between the text logical box and the frame + static constexpr int PADDING_IN_PIXELS = 5; + // Width of the lines making the frame + static constexpr int BORDER_WIDTH_IN_PIXELS = 1; }; diff --git a/src/core/gui/inputdevices/PenInputHandler.cpp b/src/core/gui/inputdevices/PenInputHandler.cpp index 8803949e663e..8d36fe45ea40 100644 --- a/src/core/gui/inputdevices/PenInputHandler.cpp +++ b/src/core/gui/inputdevices/PenInputHandler.cpp @@ -291,8 +291,6 @@ auto PenInputHandler::actionMotion(InputEvent const& event) -> bool { } } - bool result = false; - // Update the cursor xournal->view->getCursor()->setInsidePage(currentPage != nullptr); @@ -309,7 +307,10 @@ auto PenInputHandler::actionMotion(InputEvent const& event) -> bool { pos.pressure = this->filterPressure(pos, sequenceStartPage); - result = sequenceStartPage->onMotionNotifyEvent(pos); + bool result = sequenceStartPage->onMotionNotifyEvent(pos); + + this->updateLastEvent(event); // Update the last position of the input device + return result; } if (currentPage && this->penInWidget) { @@ -317,13 +318,13 @@ auto PenInputHandler::actionMotion(InputEvent const& event) -> bool { PositionInputData pos = getInputDataRelativeToCurrentPage(currentPage, event); pos.pressure = this->filterPressure(pos, currentPage); - result = currentPage->onMotionNotifyEvent(pos); - } + bool result = currentPage->onMotionNotifyEvent(pos); - // Update the last position of the input device - this->updateLastEvent(event); + this->updateLastEvent(event); // Update the last position of the input device + return result; + } - return result; + return false; } auto PenInputHandler::actionEnd(InputEvent const& event) -> bool {