/* PIKA - Photo and Image Kooker Application * a rebranding of The GNU Image Manipulation Program (created with heckimp) * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio * * Original copyright, applying to most contents (license remains unchanged): * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * PikaTextTool * Copyright (C) 2002-2010 Sven Neumann * Daniel Eddeland * Michael Natterer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "config.h" #include #include #include "libpikabase/pikabase.h" #include "libpikaconfig/pikaconfig.h" #include "libpikawidgets/pikawidgets.h" #include "tools-types.h" #include "core/pika.h" #include "core/pikaasyncset.h" #include "core/pikacontext.h" #include "core/pikadatafactory.h" #include "core/pikaerror.h" #include "core/pikaimage.h" #include "core/pika-palettes.h" #include "core/pikaimage-pick-item.h" #include "core/pikaimage-undo.h" #include "core/pikaimage-undo-push.h" #include "core/pikalayer-floating-selection.h" #include "core/pikatoolinfo.h" #include "core/pikaundostack.h" #include "menus/menus.h" #include "text/pikatext.h" #include "text/pikatext-vectors.h" #include "text/pikatextlayer.h" #include "text/pikatextlayout.h" #include "text/pikatextundo.h" #include "vectors/pikastroke.h" #include "vectors/pikavectors.h" #include "vectors/pikavectors-warp.h" #include "widgets/pikadialogfactory.h" #include "widgets/pikadockcontainer.h" #include "widgets/pikahelp-ids.h" #include "widgets/pikamenufactory.h" #include "widgets/pikatextbuffer.h" #include "widgets/pikauimanager.h" #include "widgets/pikaviewabledialog.h" #include "display/pikacanvasgroup.h" #include "display/pikadisplay.h" #include "display/pikadisplayshell.h" #include "display/pikatoolrectangle.h" #include "pikatextoptions.h" #include "pikatexttool.h" #include "pikatexttool-editor.h" #include "pikatoolcontrol.h" #include "pika-intl.h" #define TEXT_UNDO_TIMEOUT 3 /* local function prototypes */ static void pika_text_tool_constructed (GObject *object); static void pika_text_tool_finalize (GObject *object); static void pika_text_tool_control (PikaTool *tool, PikaToolAction action, PikaDisplay *display); static void pika_text_tool_button_press (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaButtonPressType press_type, PikaDisplay *display); static void pika_text_tool_button_release (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaButtonReleaseType release_type, PikaDisplay *display); static void pika_text_tool_motion (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaDisplay *display); static gboolean pika_text_tool_key_press (PikaTool *tool, GdkEventKey *kevent, PikaDisplay *display); static gboolean pika_text_tool_key_release (PikaTool *tool, GdkEventKey *kevent, PikaDisplay *display); static void pika_text_tool_oper_update (PikaTool *tool, const PikaCoords *coords, GdkModifierType state, gboolean proximity, PikaDisplay *display); static void pika_text_tool_cursor_update (PikaTool *tool, const PikaCoords *coords, GdkModifierType state, PikaDisplay *display); static PikaUIManager * pika_text_tool_get_popup (PikaTool *tool, const PikaCoords *coords, GdkModifierType state, PikaDisplay *display, const gchar **ui_path); static void pika_text_tool_draw (PikaDrawTool *draw_tool); static void pika_text_tool_draw_selection (PikaDrawTool *draw_tool); static gboolean pika_text_tool_start (PikaTextTool *text_tool, PikaDisplay *display, PikaLayer *layer, GError **error); static void pika_text_tool_halt (PikaTextTool *text_tool); static void pika_text_tool_frame_item (PikaTextTool *text_tool); static void pika_text_tool_rectangle_response (PikaToolRectangle *rectangle, gint response_id, PikaTextTool *text_tool); static void pika_text_tool_rectangle_change_complete (PikaToolRectangle *rectangle, PikaTextTool *text_tool); static void pika_text_tool_connect (PikaTextTool *text_tool, PikaTextLayer *layer, PikaText *text); static void pika_text_tool_layer_notify (PikaTextLayer *layer, const GParamSpec *pspec, PikaTextTool *text_tool); static void pika_text_tool_proxy_notify (PikaText *text, const GParamSpec *pspec, PikaTextTool *text_tool); static void pika_text_tool_text_notify (PikaText *text, const GParamSpec *pspec, PikaTextTool *text_tool); static void pika_text_tool_text_changed (PikaText *text, PikaTextTool *text_tool); static void pika_text_tool_fonts_async_set_empty_notify (PikaAsyncSet *async_set, GParamSpec *pspec, PikaTextTool *text_tool); static void pika_text_tool_apply_list (PikaTextTool *text_tool, GList *pspecs); static void pika_text_tool_create_layer (PikaTextTool *text_tool, PikaText *text); static void pika_text_tool_layer_changed (PikaImage *image, PikaTextTool *text_tool); static void pika_text_tool_set_image (PikaTextTool *text_tool, PikaImage *image); static gboolean pika_text_tool_set_drawable (PikaTextTool *text_tool, PikaDrawable *drawable, gboolean confirm); static void pika_text_tool_block_drawing (PikaTextTool *text_tool); static void pika_text_tool_unblock_drawing (PikaTextTool *text_tool); static void pika_text_tool_buffer_begin_edit (PikaTextBuffer *buffer, PikaTextTool *text_tool); static void pika_text_tool_buffer_end_edit (PikaTextBuffer *buffer, PikaTextTool *text_tool); static void pika_text_tool_buffer_color_applied (PikaTextBuffer *buffer, const PikaRGB *color, PikaTextTool *text_tool); G_DEFINE_TYPE (PikaTextTool, pika_text_tool, PIKA_TYPE_DRAW_TOOL) #define parent_class pika_text_tool_parent_class void pika_text_tool_register (PikaToolRegisterCallback callback, gpointer data) { (* callback) (PIKA_TYPE_TEXT_TOOL, PIKA_TYPE_TEXT_OPTIONS, pika_text_options_gui, PIKA_CONTEXT_PROP_MASK_FOREGROUND | PIKA_CONTEXT_PROP_MASK_FONT | PIKA_CONTEXT_PROP_MASK_PALETTE /* for the color popup's palette tab */, "pika-text-tool", _("Text"), _("Text Tool: Create or edit text layers"), N_("Te_xt"), "T", NULL, PIKA_HELP_TOOL_TEXT, PIKA_ICON_TOOL_TEXT, data); } static void pika_text_tool_class_init (PikaTextToolClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); PikaToolClass *tool_class = PIKA_TOOL_CLASS (klass); PikaDrawToolClass *draw_tool_class = PIKA_DRAW_TOOL_CLASS (klass); object_class->constructed = pika_text_tool_constructed; object_class->finalize = pika_text_tool_finalize; tool_class->control = pika_text_tool_control; tool_class->button_press = pika_text_tool_button_press; tool_class->motion = pika_text_tool_motion; tool_class->button_release = pika_text_tool_button_release; tool_class->key_press = pika_text_tool_key_press; tool_class->key_release = pika_text_tool_key_release; tool_class->oper_update = pika_text_tool_oper_update; tool_class->cursor_update = pika_text_tool_cursor_update; tool_class->get_popup = pika_text_tool_get_popup; draw_tool_class->draw = pika_text_tool_draw; } static void pika_text_tool_init (PikaTextTool *text_tool) { PikaTool *tool = PIKA_TOOL (text_tool); text_tool->buffer = pika_text_buffer_new (); g_signal_connect (text_tool->buffer, "begin-user-action", G_CALLBACK (pika_text_tool_buffer_begin_edit), text_tool); g_signal_connect (text_tool->buffer, "end-user-action", G_CALLBACK (pika_text_tool_buffer_end_edit), text_tool); g_signal_connect (text_tool->buffer, "color-applied", G_CALLBACK (pika_text_tool_buffer_color_applied), text_tool); text_tool->handle_rectangle_change_complete = TRUE; pika_text_tool_editor_init (text_tool); pika_tool_control_set_scroll_lock (tool->control, TRUE); pika_tool_control_set_handle_empty_image (tool->control, TRUE); pika_tool_control_set_wants_click (tool->control, TRUE); pika_tool_control_set_wants_double_click (tool->control, TRUE); pika_tool_control_set_wants_triple_click (tool->control, TRUE); pika_tool_control_set_wants_all_key_events (tool->control, TRUE); pika_tool_control_set_active_modifiers (tool->control, PIKA_TOOL_ACTIVE_MODIFIERS_SEPARATE); pika_tool_control_set_precision (tool->control, PIKA_CURSOR_PRECISION_PIXEL_BORDER); pika_tool_control_set_tool_cursor (tool->control, PIKA_TOOL_CURSOR_TEXT); pika_tool_control_set_action_object_1 (tool->control, "context-font-select-set"); } static void pika_text_tool_constructed (GObject *object) { PikaTextTool *text_tool = PIKA_TEXT_TOOL (object); PikaTextOptions *options = PIKA_TEXT_TOOL_GET_OPTIONS (text_tool); PikaTool *tool = PIKA_TOOL (text_tool); PikaAsyncSet *async_set; G_OBJECT_CLASS (parent_class)->constructed (object); text_tool->proxy = g_object_new (PIKA_TYPE_TEXT, "pika", tool->tool_info->pika, NULL); pika_text_options_connect_text (options, text_tool->proxy); g_signal_connect_object (text_tool->proxy, "notify", G_CALLBACK (pika_text_tool_proxy_notify), text_tool, 0); async_set = pika_data_factory_get_async_set (tool->tool_info->pika->font_factory); g_signal_connect_object (async_set, "notify::empty", G_CALLBACK (pika_text_tool_fonts_async_set_empty_notify), text_tool, 0); } static void pika_text_tool_finalize (GObject *object) { PikaTextTool *text_tool = PIKA_TEXT_TOOL (object); g_clear_object (&text_tool->proxy); g_clear_object (&text_tool->buffer); pika_text_tool_editor_finalize (text_tool); G_OBJECT_CLASS (parent_class)->finalize (object); } static void pika_text_tool_remove_empty_text_layer (PikaTextTool *text_tool) { PikaTextLayer *text_layer = text_tool->layer; if (text_layer && text_layer->auto_rename) { PikaText *text = pika_text_layer_get_text (text_layer); if (text && text->box_mode == PIKA_TEXT_BOX_DYNAMIC && (! text->text || text->text[0] == '\0') && (! text->markup || text->markup[0] == '\0')) { PikaImage *image = pika_item_get_image (PIKA_ITEM (text_layer)); if (text_tool->image == image) g_signal_handlers_block_by_func (image, pika_text_tool_layer_changed, text_tool); pika_image_remove_layer (image, PIKA_LAYER (text_layer), TRUE, NULL); pika_image_flush (image); if (text_tool->image == image) g_signal_handlers_unblock_by_func (image, pika_text_tool_layer_changed, text_tool); } } } static void pika_text_tool_control (PikaTool *tool, PikaToolAction action, PikaDisplay *display) { PikaTextTool *text_tool = PIKA_TEXT_TOOL (tool); switch (action) { case PIKA_TOOL_ACTION_PAUSE: case PIKA_TOOL_ACTION_RESUME: break; case PIKA_TOOL_ACTION_HALT: pika_text_tool_halt (text_tool); break; case PIKA_TOOL_ACTION_COMMIT: break; } PIKA_TOOL_CLASS (parent_class)->control (tool, action, display); } static void pika_text_tool_button_press (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaButtonPressType press_type, PikaDisplay *display) { PikaTextTool *text_tool = PIKA_TEXT_TOOL (tool); PikaImage *image = pika_display_get_image (display); PikaText *text = text_tool->text; PikaToolRectangle *rectangle; pika_draw_tool_pause (PIKA_DRAW_TOOL (tool)); if (tool->display && tool->display != display) pika_tool_control (tool, PIKA_TOOL_ACTION_HALT, display); if (! text_tool->widget) { GError *error = NULL; if (! pika_text_tool_start (text_tool, display, NULL, &error)) { pika_draw_tool_resume (PIKA_DRAW_TOOL (tool)); pika_tool_message_literal (tool, display, error->message); g_clear_error (&error); return; } pika_tool_widget_hover (text_tool->widget, coords, state, TRUE); /* HACK: force CREATING on a newly created rectangle; otherwise, * the above binding of properties would cause the rectangle to * start with the size from tool options. */ pika_tool_rectangle_set_function (PIKA_TOOL_RECTANGLE (text_tool->widget), PIKA_TOOL_RECTANGLE_CREATING); } rectangle = PIKA_TOOL_RECTANGLE (text_tool->widget); if (press_type == PIKA_BUTTON_PRESS_NORMAL) { pika_tool_control_activate (tool->control); /* clicking anywhere while a preedit is going on aborts the * preedit, this is ugly but at least leaves everything in * a consistent state */ if (text_tool->preedit_active) pika_text_tool_abort_im_context (text_tool); else pika_text_tool_reset_im_context (text_tool); text_tool->selecting = FALSE; if (pika_tool_rectangle_point_in_rectangle (rectangle, coords->x, coords->y) && ! text_tool->moving) { pika_tool_rectangle_set_function (rectangle, PIKA_TOOL_RECTANGLE_DEAD); } else if (pika_tool_widget_button_press (text_tool->widget, coords, time, state, press_type)) { text_tool->grab_widget = text_tool->widget; } /* bail out now if the user user clicked on a handle of an * existing rectangle, but not inside an existing framed layer */ if (pika_tool_rectangle_get_function (rectangle) != PIKA_TOOL_RECTANGLE_CREATING) { if (text_tool->layer) { PikaItem *item = PIKA_ITEM (text_tool->layer); gdouble x = coords->x - pika_item_get_offset_x (item); gdouble y = coords->y - pika_item_get_offset_y (item); if (x < 0 || x >= pika_item_get_width (item) || y < 0 || y >= pika_item_get_height (item)) { pika_draw_tool_resume (PIKA_DRAW_TOOL (tool)); return; } } else { pika_draw_tool_resume (PIKA_DRAW_TOOL (tool)); return; } } /* if the the click is not related to the currently edited text * layer in any way, try to pick a text layer */ if (! text_tool->moving && pika_tool_rectangle_get_function (rectangle) == PIKA_TOOL_RECTANGLE_CREATING) { PikaTextLayer *text_layer; text_layer = pika_image_pick_text_layer (image, coords->x, coords->y); if (text_layer && text_layer != text_tool->layer) { GList *selection = g_list_prepend (NULL, text_layer); if (text_tool->image == image) g_signal_handlers_block_by_func (image, pika_text_tool_layer_changed, text_tool); pika_image_set_selected_layers (image, selection); g_list_free (selection); if (text_tool->image == image) g_signal_handlers_unblock_by_func (image, pika_text_tool_layer_changed, text_tool); } } } if (pika_image_coords_in_active_pickable (image, coords, FALSE, FALSE, FALSE)) { GList *drawables = pika_image_get_selected_drawables (image); PikaDrawable *drawable = NULL; gdouble x = coords->x; gdouble y = coords->y; if (g_list_length (drawables) == 1) { PikaItem *item = PIKA_ITEM (drawables->data); x = coords->x - pika_item_get_offset_x (item); y = coords->y - pika_item_get_offset_y (item); drawable = drawables->data; } g_list_free (drawables); /* did the user click on a text layer? */ if (drawable && pika_text_tool_set_drawable (text_tool, drawable, TRUE)) { if (press_type == PIKA_BUTTON_PRESS_NORMAL) { /* if we clicked on a text layer while the tool was idle * (didn't show a rectangle), frame the layer and switch to * selecting instead of drawing a new rectangle */ if (pika_tool_rectangle_get_function (rectangle) == PIKA_TOOL_RECTANGLE_CREATING) { pika_tool_rectangle_set_function (rectangle, PIKA_TOOL_RECTANGLE_DEAD); pika_text_tool_frame_item (text_tool); } if (text_tool->text && text_tool->text != text) { pika_text_tool_editor_start (text_tool); } } if (text_tool->text && ! text_tool->moving) { text_tool->selecting = TRUE; pika_text_tool_editor_button_press (text_tool, x, y, press_type); } else { text_tool->selecting = FALSE; } pika_draw_tool_resume (PIKA_DRAW_TOOL (tool)); return; } } if (press_type == PIKA_BUTTON_PRESS_NORMAL) { /* create a new text layer */ text_tool->text_box_fixed = FALSE; /* make sure the text tool has an image, even if the user didn't click * inside the active drawable, in particular, so that the text style * editor picks the correct resolution. */ pika_text_tool_set_image (text_tool, image); pika_text_tool_connect (text_tool, NULL, NULL); pika_text_tool_editor_start (text_tool); } pika_draw_tool_resume (PIKA_DRAW_TOOL (tool)); } static void pika_text_tool_button_release (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaButtonReleaseType release_type, PikaDisplay *display) { PikaTextTool *text_tool = PIKA_TEXT_TOOL (tool); PikaToolRectangle *rectangle = PIKA_TOOL_RECTANGLE (text_tool->widget); pika_tool_control_halt (tool->control); if (text_tool->selecting) { /* we are in a selection process (user has initially clicked on * an existing text layer), so finish the selection process and * ignore rectangle-change-complete. */ /* need to block "end-user-action" on the text buffer, because * GtkTextBuffer considers copying text to the clipboard an * undo-relevant user action, which is clearly a bug, but what * can we do... */ g_signal_handlers_block_by_func (text_tool->buffer, pika_text_tool_buffer_begin_edit, text_tool); g_signal_handlers_block_by_func (text_tool->buffer, pika_text_tool_buffer_end_edit, text_tool); pika_text_tool_editor_button_release (text_tool); g_signal_handlers_unblock_by_func (text_tool->buffer, pika_text_tool_buffer_end_edit, text_tool); g_signal_handlers_unblock_by_func (text_tool->buffer, pika_text_tool_buffer_begin_edit, text_tool); text_tool->selecting = FALSE; text_tool->handle_rectangle_change_complete = FALSE; /* there is no cancelling of selections yet */ if (release_type == PIKA_BUTTON_RELEASE_CANCEL) release_type = PIKA_BUTTON_RELEASE_NORMAL; } else if (text_tool->moving) { /* the user has moved the text layer with Alt-drag, fall * through and let rectangle-change-complete do its job of * setting text layer's new position. */ } else if (pika_tool_rectangle_get_function (rectangle) == PIKA_TOOL_RECTANGLE_DEAD) { /* the user clicked in dead space (like between the corner and * edge handles, so completely ignore that. */ text_tool->handle_rectangle_change_complete = FALSE; } else if (release_type == PIKA_BUTTON_RELEASE_CANCEL) { /* user has canceled the rectangle resizing, fall through * and let the rectangle handle restoring the previous size */ } else { gdouble x1, y1; gdouble x2, y2; /* otherwise the user has clicked outside of any text layer in * order to create a new text, fall through and let * rectangle-change-complete do its job of setting the new text * layer's size. */ g_object_get (rectangle, "x1", &x1, "y1", &y1, "x2", &x2, "y2", &y2, NULL); if (release_type == PIKA_BUTTON_RELEASE_CLICK || (x2 - x1) < 3 || (y2 - y1) < 3) { /* unless the rectangle is unreasonably small to hold any * real text (the user has eitherjust clicked or just made * a rectangle of a few pixels), so set the text box to * dynamic and ignore rectangle-change-complete. */ g_object_set (text_tool->proxy, "box-mode", PIKA_TEXT_BOX_DYNAMIC, NULL); text_tool->handle_rectangle_change_complete = FALSE; } } if (text_tool->grab_widget) { pika_tool_widget_button_release (text_tool->grab_widget, coords, time, state, release_type); text_tool->grab_widget = NULL; } text_tool->handle_rectangle_change_complete = TRUE; } static void pika_text_tool_motion (PikaTool *tool, const PikaCoords *coords, guint32 time, GdkModifierType state, PikaDisplay *display) { PikaTextTool *text_tool = PIKA_TEXT_TOOL (tool); if (! text_tool->selecting) { if (text_tool->grab_widget) { pika_tool_widget_motion (text_tool->grab_widget, coords, time, state); } } else { PikaItem *item = PIKA_ITEM (text_tool->layer); gdouble x = coords->x - pika_item_get_offset_x (item); gdouble y = coords->y - pika_item_get_offset_y (item); pika_text_tool_editor_motion (text_tool, x, y); } } static gboolean pika_text_tool_key_press (PikaTool *tool, GdkEventKey *kevent, PikaDisplay *display) { PikaTextTool *text_tool = PIKA_TEXT_TOOL (tool); if (display == tool->display) return pika_text_tool_editor_key_press (text_tool, kevent); return FALSE; } static gboolean pika_text_tool_key_release (PikaTool *tool, GdkEventKey *kevent, PikaDisplay *display) { PikaTextTool *text_tool = PIKA_TEXT_TOOL (tool); if (display == tool->display) return pika_text_tool_editor_key_release (text_tool, kevent); return FALSE; } static void pika_text_tool_oper_update (PikaTool *tool, const PikaCoords *coords, GdkModifierType state, gboolean proximity, PikaDisplay *display) { PikaTextTool *text_tool = PIKA_TEXT_TOOL (tool); PikaToolRectangle *rectangle = PIKA_TOOL_RECTANGLE (text_tool->widget); PIKA_TOOL_CLASS (parent_class)->oper_update (tool, coords, state, proximity, display); text_tool->moving = (text_tool->widget && pika_tool_rectangle_get_function (rectangle) == PIKA_TOOL_RECTANGLE_MOVING && (state & GDK_MOD1_MASK)); } static void pika_text_tool_cursor_update (PikaTool *tool, const PikaCoords *coords, GdkModifierType state, PikaDisplay *display) { PikaTextTool *text_tool = PIKA_TEXT_TOOL (tool); PikaToolRectangle *rectangle = PIKA_TOOL_RECTANGLE (text_tool->widget); if (rectangle && tool->display == display) { if (pika_tool_rectangle_point_in_rectangle (rectangle, coords->x, coords->y) && ! text_tool->moving) { pika_tool_set_cursor (tool, display, (PikaCursorType) GDK_XTERM, pika_tool_control_get_tool_cursor (tool->control), PIKA_CURSOR_MODIFIER_NONE); } else { PIKA_TOOL_CLASS (parent_class)->cursor_update (tool, coords, state, display); } } else { PikaAsyncSet *async_set; async_set = pika_data_factory_get_async_set (tool->tool_info->pika->font_factory); if (pika_async_set_is_empty (async_set)) { PIKA_TOOL_CLASS (parent_class)->cursor_update (tool, coords, state, display); } else { pika_tool_set_cursor (tool, display, pika_tool_control_get_cursor (tool->control), pika_tool_control_get_tool_cursor (tool->control), PIKA_CURSOR_MODIFIER_BAD); } } } static PikaUIManager * pika_text_tool_get_popup (PikaTool *tool, const PikaCoords *coords, GdkModifierType state, PikaDisplay *display, const gchar **ui_path) { PikaTextTool *text_tool = PIKA_TEXT_TOOL (tool); PikaToolRectangle *rectangle = PIKA_TOOL_RECTANGLE (text_tool->widget); if (rectangle && pika_tool_rectangle_point_in_rectangle (rectangle, coords->x, coords->y)) { PikaMenuFactory *menu_factory; PikaUIManager *ui_manager; menu_factory = menus_get_global_menu_factory (tool->tool_info->pika); ui_manager = pika_menu_factory_get_manager (menu_factory, "", text_tool); pika_ui_manager_update (ui_manager, text_tool); *ui_path = "/text-tool-popup"; return ui_manager; } return NULL; } static void pika_text_tool_draw (PikaDrawTool *draw_tool) { PikaTextTool *text_tool = PIKA_TEXT_TOOL (draw_tool); PIKA_DRAW_TOOL_CLASS (parent_class)->draw (draw_tool); if (! text_tool->text || ! text_tool->layer || ! text_tool->layer->text) { pika_text_tool_editor_update_im_cursor (text_tool); return; } pika_text_tool_ensure_layout (text_tool); if (gtk_text_buffer_get_has_selection (GTK_TEXT_BUFFER (text_tool->buffer))) { /* If the text buffer has a selection, highlight the selected letters */ pika_text_tool_draw_selection (draw_tool); } else { /* If the text buffer has no selection, draw the text cursor */ PikaCanvasItem *item; PangoRectangle cursor_rect; gint off_x, off_y; gboolean overwrite; PikaTextDirection direction; pika_text_tool_editor_get_cursor_rect (text_tool, text_tool->overwrite_mode, &cursor_rect); pika_item_get_offset (PIKA_ITEM (text_tool->layer), &off_x, &off_y); cursor_rect.x += off_x; cursor_rect.y += off_y; overwrite = text_tool->overwrite_mode && cursor_rect.width != 0; direction = pika_text_tool_get_direction (text_tool); item = pika_draw_tool_add_text_cursor (draw_tool, &cursor_rect, overwrite, direction); pika_canvas_item_set_highlight (item, TRUE); } pika_text_tool_editor_update_im_cursor (text_tool); } static void pika_text_tool_draw_selection (PikaDrawTool *draw_tool) { PikaTextTool *text_tool = PIKA_TEXT_TOOL (draw_tool); GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer); PikaCanvasGroup *group; PangoLayout *layout; gint offset_x; gint offset_y; gint width; gint height; gint off_x, off_y; PangoLayoutIter *iter; GtkTextIter sel_start, sel_end; gint min, max; gint i; PikaTextDirection direction; group = pika_draw_tool_add_stroke_group (draw_tool); pika_canvas_item_set_highlight (PIKA_CANVAS_ITEM (group), TRUE); gtk_text_buffer_get_selection_bounds (buffer, &sel_start, &sel_end); min = pika_text_buffer_get_iter_index (text_tool->buffer, &sel_start, TRUE); max = pika_text_buffer_get_iter_index (text_tool->buffer, &sel_end, TRUE); layout = pika_text_layout_get_pango_layout (text_tool->layout); pika_text_layout_get_offsets (text_tool->layout, &offset_x, &offset_y); pika_text_layout_get_size (text_tool->layout, &width, &height); pika_item_get_offset (PIKA_ITEM (text_tool->layer), &off_x, &off_y); offset_x += off_x; offset_y += off_y; direction = pika_text_tool_get_direction (text_tool); iter = pango_layout_get_iter (layout); pika_draw_tool_push_group (draw_tool, group); do { if (! pango_layout_iter_get_run (iter)) continue; i = pango_layout_iter_get_index (iter); if (i >= min && i < max) { PangoRectangle rect; gint ytop, ybottom; pango_layout_iter_get_char_extents (iter, &rect); pango_layout_iter_get_line_yrange (iter, &ytop, &ybottom); rect.y = ytop; rect.height = ybottom - ytop; pango_extents_to_pixels (&rect, NULL); pika_text_layout_transform_rect (text_tool->layout, &rect); switch (direction) { case PIKA_TEXT_DIRECTION_LTR: case PIKA_TEXT_DIRECTION_RTL: rect.x += offset_x; rect.y += offset_y; pika_draw_tool_add_rectangle (draw_tool, FALSE, rect.x, rect.y, rect.width, rect.height); break; case PIKA_TEXT_DIRECTION_TTB_RTL: case PIKA_TEXT_DIRECTION_TTB_RTL_UPRIGHT: rect.y = offset_x - rect.y + width; rect.x = offset_y + rect.x; pika_draw_tool_add_rectangle (draw_tool, FALSE, rect.y, rect.x, -rect.height, rect.width); break; case PIKA_TEXT_DIRECTION_TTB_LTR: case PIKA_TEXT_DIRECTION_TTB_LTR_UPRIGHT: rect.y = offset_x + rect.y; rect.x = offset_y - rect.x + height; pika_draw_tool_add_rectangle (draw_tool, FALSE, rect.y, rect.x, rect.height, -rect.width); break; } } } while (pango_layout_iter_next_char (iter)); pika_draw_tool_pop_group (draw_tool); pango_layout_iter_free (iter); } static gboolean pika_text_tool_start (PikaTextTool *text_tool, PikaDisplay *display, PikaLayer *layer, GError **error) { PikaTool *tool = PIKA_TOOL (text_tool); PikaDisplayShell *shell = pika_display_get_shell (display); PikaToolWidget *widget; PikaAsyncSet *async_set; async_set = pika_data_factory_get_async_set (tool->tool_info->pika->font_factory); if (! pika_async_set_is_empty (async_set)) { g_set_error_literal (error, PIKA_ERROR, PIKA_FAILED, _("Fonts are still loading")); return FALSE; } tool->display = display; text_tool->widget = widget = pika_tool_rectangle_new (shell); g_object_set (widget, "force-narrow-mode", TRUE, "status-title", _("Text box: "), NULL); pika_draw_tool_set_widget (PIKA_DRAW_TOOL (tool), widget); g_signal_connect (widget, "response", G_CALLBACK (pika_text_tool_rectangle_response), text_tool); g_signal_connect (widget, "change-complete", G_CALLBACK (pika_text_tool_rectangle_change_complete), text_tool); pika_draw_tool_start (PIKA_DRAW_TOOL (tool), display); if (layer) { pika_text_tool_frame_item (text_tool); pika_text_tool_editor_start (text_tool); pika_text_tool_editor_position (text_tool); } return TRUE; } static void pika_text_tool_halt (PikaTextTool *text_tool) { PikaTool *tool = PIKA_TOOL (text_tool); pika_text_tool_editor_halt (text_tool); pika_text_tool_clear_layout (text_tool); pika_text_tool_set_drawable (text_tool, NULL, FALSE); if (pika_draw_tool_is_active (PIKA_DRAW_TOOL (tool))) pika_draw_tool_stop (PIKA_DRAW_TOOL (tool)); pika_draw_tool_set_widget (PIKA_DRAW_TOOL (tool), NULL); g_clear_object (&text_tool->widget); tool->display = NULL; g_list_free (tool->drawables); tool->drawables = NULL; } static void pika_text_tool_frame_item (PikaTextTool *text_tool) { g_return_if_fail (PIKA_IS_LAYER (text_tool->layer)); text_tool->handle_rectangle_change_complete = FALSE; pika_tool_rectangle_frame_item (PIKA_TOOL_RECTANGLE (text_tool->widget), PIKA_ITEM (text_tool->layer)); text_tool->handle_rectangle_change_complete = TRUE; } static void pika_text_tool_rectangle_response (PikaToolRectangle *rectangle, gint response_id, PikaTextTool *text_tool) { PikaTool *tool = PIKA_TOOL (text_tool); /* this happens when a newly created rectangle gets canceled, * we have to shut down the tool */ if (response_id == PIKA_TOOL_WIDGET_RESPONSE_CANCEL) pika_tool_control (tool, PIKA_TOOL_ACTION_HALT, tool->display); } static void pika_text_tool_rectangle_change_complete (PikaToolRectangle *rectangle, PikaTextTool *text_tool) { pika_text_tool_editor_position (text_tool); if (text_tool->handle_rectangle_change_complete) { PikaItem *item = PIKA_ITEM (text_tool->layer); gdouble x1, y1; gdouble x2, y2; if (! item) { /* we can't set properties for the text layer, because it * isn't created until some text has been inserted, so we * need to make a special note that will remind us what to * do when we actually create the layer */ text_tool->text_box_fixed = TRUE; return; } g_object_get (rectangle, "x1", &x1, "y1", &y1, "x2", &x2, "y2", &y2, NULL); if ((x2 - x1) != pika_item_get_width (item) || (y2 - y1) != pika_item_get_height (item)) { PikaUnit box_unit = text_tool->proxy->box_unit; gdouble xres, yres; gboolean push_undo = TRUE; PikaUndo *undo; pika_image_get_resolution (text_tool->image, &xres, &yres); g_object_set (text_tool->proxy, "box-mode", PIKA_TEXT_BOX_FIXED, "box-width", pika_pixels_to_units (x2 - x1, box_unit, xres), "box-height", pika_pixels_to_units (y2 - y1, box_unit, yres), NULL); undo = pika_image_undo_can_compress (text_tool->image, PIKA_TYPE_UNDO_STACK, PIKA_UNDO_GROUP_TEXT); if (undo && pika_undo_get_age (undo) <= TEXT_UNDO_TIMEOUT && g_object_get_data (G_OBJECT (undo), "reshape-text-layer") == (gpointer) item) push_undo = FALSE; if (push_undo) { pika_image_undo_group_start (text_tool->image, PIKA_UNDO_GROUP_TEXT, _("Reshape Text Layer")); undo = pika_image_undo_can_compress (text_tool->image, PIKA_TYPE_UNDO_STACK, PIKA_UNDO_GROUP_TEXT); if (undo) g_object_set_data (G_OBJECT (undo), "reshape-text-layer", (gpointer) item); } pika_text_tool_block_drawing (text_tool); pika_item_translate (item, x1 - pika_item_get_offset_x (item), y1 - pika_item_get_offset_y (item), push_undo); pika_text_tool_apply (text_tool, push_undo); pika_text_tool_unblock_drawing (text_tool); if (push_undo) pika_image_undo_group_end (text_tool->image); } else if (x1 != pika_item_get_offset_x (item) || y1 != pika_item_get_offset_y (item)) { pika_text_tool_block_drawing (text_tool); pika_text_tool_apply (text_tool, TRUE); pika_item_translate (item, x1 - pika_item_get_offset_x (item), y1 - pika_item_get_offset_y (item), TRUE); pika_text_tool_unblock_drawing (text_tool); pika_image_flush (text_tool->image); } } } static void pika_text_tool_connect (PikaTextTool *text_tool, PikaTextLayer *layer, PikaText *text) { PikaTool *tool = PIKA_TOOL (text_tool); g_return_if_fail (text == NULL || (layer != NULL && layer->text == text)); if (text_tool->text != text) { PikaTextOptions *options = PIKA_TEXT_TOOL_GET_OPTIONS (tool); g_signal_handlers_block_by_func (text_tool->buffer, pika_text_tool_buffer_begin_edit, text_tool); g_signal_handlers_block_by_func (text_tool->buffer, pika_text_tool_buffer_end_edit, text_tool); if (text_tool->text) { g_signal_handlers_disconnect_by_func (text_tool->text, pika_text_tool_text_notify, text_tool); g_signal_handlers_disconnect_by_func (text_tool->text, pika_text_tool_text_changed, text_tool); if (text_tool->pending) pika_text_tool_apply (text_tool, TRUE); g_clear_object (&text_tool->text); g_object_set (text_tool->proxy, "text", NULL, "markup", NULL, NULL); pika_text_buffer_set_text (text_tool->buffer, NULL); pika_text_tool_clear_layout (text_tool); } pika_context_define_property (PIKA_CONTEXT (options), PIKA_CONTEXT_PROP_FOREGROUND, text != NULL); if (text) { if (text->unit != text_tool->proxy->unit) pika_size_entry_set_unit (PIKA_SIZE_ENTRY (options->size_entry), text->unit); pika_config_sync (G_OBJECT (text), G_OBJECT (text_tool->proxy), 0); if (text->markup) pika_text_buffer_set_markup (text_tool->buffer, text->markup); else pika_text_buffer_set_text (text_tool->buffer, text->text); pika_text_tool_clear_layout (text_tool); text_tool->text = g_object_ref (text); g_signal_connect (text, "notify", G_CALLBACK (pika_text_tool_text_notify), text_tool); g_signal_connect (text, "changed", G_CALLBACK (pika_text_tool_text_changed), text_tool); } g_signal_handlers_unblock_by_func (text_tool->buffer, pika_text_tool_buffer_end_edit, text_tool); g_signal_handlers_unblock_by_func (text_tool->buffer, pika_text_tool_buffer_begin_edit, text_tool); } if (text_tool->layer != layer) { if (text_tool->layer) { g_signal_handlers_disconnect_by_func (text_tool->layer, pika_text_tool_layer_notify, text_tool); /* don't try to remove the layer if it is not attached, * which can happen if we got here because the layer was * somehow deleted from the image (like by the user in the * layers dialog). */ if (pika_item_is_attached (PIKA_ITEM (text_tool->layer))) pika_text_tool_remove_empty_text_layer (text_tool); } text_tool->layer = layer; if (layer) { g_signal_connect_object (text_tool->layer, "notify", G_CALLBACK (pika_text_tool_layer_notify), text_tool, 0); } } } static void pika_text_tool_layer_notify (PikaTextLayer *layer, const GParamSpec *pspec, PikaTextTool *text_tool) { PikaTool *tool = PIKA_TOOL (text_tool); if (! strcmp (pspec->name, "modified")) { if (layer->modified) pika_tool_control (tool, PIKA_TOOL_ACTION_HALT, tool->display); } else if (! strcmp (pspec->name, "text")) { if (! layer->text) pika_tool_control (tool, PIKA_TOOL_ACTION_HALT, tool->display); } else if (! strcmp (pspec->name, "offset-x") || ! strcmp (pspec->name, "offset-y")) { if (pika_item_is_attached (PIKA_ITEM (layer))) { pika_text_tool_block_drawing (text_tool); pika_text_tool_frame_item (text_tool); pika_text_tool_unblock_drawing (text_tool); } } } static gboolean pika_text_tool_apply_idle (PikaTextTool *text_tool) { text_tool->idle_id = 0; pika_text_tool_apply (text_tool, TRUE); pika_text_tool_unblock_drawing (text_tool); return G_SOURCE_REMOVE; } static void pika_text_tool_proxy_notify (PikaText *text, const GParamSpec *pspec, PikaTextTool *text_tool) { if (! text_tool->text) return; if ((pspec->flags & G_PARAM_READWRITE) == G_PARAM_READWRITE && pspec->owner_type == PIKA_TYPE_TEXT) { if (text_tool->preedit_active) { /* if there is a preedit going on, don't queue pending * changes to be idle-applied with undo; instead, flush the * pending queue (happens only when preedit starts), and * apply the changes to text_tool->text directly. Preedit * will *always* end by removing the preedit string, and if * the preedit was committed, it will insert the resulting * text, which will not trigger this if() any more. */ GList *list = NULL; /* if there are pending changes, apply them before applying * preedit stuff directly (bypassing undo) */ if (text_tool->pending) { pika_text_tool_block_drawing (text_tool); pika_text_tool_apply (text_tool, TRUE); pika_text_tool_unblock_drawing (text_tool); } pika_text_tool_block_drawing (text_tool); list = g_list_append (list, (gpointer) pspec); pika_text_tool_apply_list (text_tool, list); g_list_free (list); pika_text_tool_frame_item (text_tool); pika_image_flush (pika_item_get_image (PIKA_ITEM (text_tool->layer))); pika_text_tool_unblock_drawing (text_tool); } else { /* else queue the property change for normal processing, * including undo */ text_tool->pending = g_list_append (text_tool->pending, (gpointer) pspec); if (! text_tool->idle_id) { pika_text_tool_block_drawing (text_tool); text_tool->idle_id = g_idle_add_full (G_PRIORITY_LOW, (GSourceFunc) pika_text_tool_apply_idle, text_tool, NULL); } } } } static void pika_text_tool_text_notify (PikaText *text, const GParamSpec *pspec, PikaTextTool *text_tool) { g_return_if_fail (text == text_tool->text); /* an undo cancels all preedit operations */ if (text_tool->preedit_active) pika_text_tool_abort_im_context (text_tool); pika_text_tool_block_drawing (text_tool); if ((pspec->flags & G_PARAM_READWRITE) == G_PARAM_READWRITE) { GValue value = G_VALUE_INIT; g_value_init (&value, pspec->value_type); g_object_get_property (G_OBJECT (text), pspec->name, &value); g_signal_handlers_block_by_func (text_tool->proxy, pika_text_tool_proxy_notify, text_tool); g_object_set_property (G_OBJECT (text_tool->proxy), pspec->name, &value); g_signal_handlers_unblock_by_func (text_tool->proxy, pika_text_tool_proxy_notify, text_tool); g_value_unset (&value); } /* if the text has changed, (probably because of an undo), we put * the new text into the text buffer */ if (strcmp (pspec->name, "text") == 0 || strcmp (pspec->name, "markup") == 0) { g_signal_handlers_block_by_func (text_tool->buffer, pika_text_tool_buffer_begin_edit, text_tool); g_signal_handlers_block_by_func (text_tool->buffer, pika_text_tool_buffer_end_edit, text_tool); if (text->markup) pika_text_buffer_set_markup (text_tool->buffer, text->markup); else pika_text_buffer_set_text (text_tool->buffer, text->text); g_signal_handlers_unblock_by_func (text_tool->buffer, pika_text_tool_buffer_end_edit, text_tool); g_signal_handlers_unblock_by_func (text_tool->buffer, pika_text_tool_buffer_begin_edit, text_tool); } pika_text_tool_unblock_drawing (text_tool); } static void pika_text_tool_text_changed (PikaText *text, PikaTextTool *text_tool) { pika_text_tool_block_drawing (text_tool); /* we need to redraw the rectangle in any case because whatever * changes to the text can change its size */ pika_text_tool_frame_item (text_tool); pika_text_tool_unblock_drawing (text_tool); } static void pika_text_tool_fonts_async_set_empty_notify (PikaAsyncSet *async_set, GParamSpec *pspec, PikaTextTool *text_tool) { PikaTool *tool = PIKA_TOOL (text_tool); if (! pika_async_set_is_empty (async_set) && tool->display) pika_tool_control (tool, PIKA_TOOL_ACTION_HALT, tool->display); } static void pika_text_tool_apply_list (PikaTextTool *text_tool, GList *pspecs) { GObject *src = G_OBJECT (text_tool->proxy); GObject *dest = G_OBJECT (text_tool->text); GList *list; g_signal_handlers_block_by_func (dest, pika_text_tool_text_notify, text_tool); g_signal_handlers_block_by_func (dest, pika_text_tool_text_changed, text_tool); g_object_freeze_notify (dest); for (list = pspecs; list; list = g_list_next (list)) { const GParamSpec *pspec; GValue value = G_VALUE_INIT; /* look ahead and compress changes */ if (list->next && list->next->data == list->data) continue; pspec = list->data; g_value_init (&value, pspec->value_type); g_object_get_property (src, pspec->name, &value); g_object_set_property (dest, pspec->name, &value); g_value_unset (&value); } g_object_thaw_notify (dest); g_signal_handlers_unblock_by_func (dest, pika_text_tool_text_notify, text_tool); g_signal_handlers_unblock_by_func (dest, pika_text_tool_text_changed, text_tool); } static void pika_text_tool_create_layer (PikaTextTool *text_tool, PikaText *text) { PikaTool *tool = PIKA_TOOL (text_tool); PikaImage *image = pika_display_get_image (tool->display); PikaLayer *layer; gdouble x1, y1; gdouble x2, y2; pika_text_tool_block_drawing (text_tool); if (text) { text = pika_config_duplicate (PIKA_CONFIG (text)); } else { gchar *string; if (pika_text_buffer_has_markup (text_tool->buffer)) { string = pika_text_buffer_get_markup (text_tool->buffer); g_object_set (text_tool->proxy, "markup", string, "box-mode", PIKA_TEXT_BOX_DYNAMIC, NULL); } else { string = pika_text_buffer_get_text (text_tool->buffer); g_object_set (text_tool->proxy, "text", string, "box-mode", PIKA_TEXT_BOX_DYNAMIC, NULL); } g_free (string); text = pika_config_duplicate (PIKA_CONFIG (text_tool->proxy)); } layer = pika_text_layer_new (image, text); g_object_unref (text); if (! layer) { pika_text_tool_unblock_drawing (text_tool); return; } pika_text_tool_connect (text_tool, PIKA_TEXT_LAYER (layer), text); pika_image_undo_group_start (image, PIKA_UNDO_GROUP_TEXT, _("Add Text Layer")); if (pika_image_get_floating_selection (image)) { g_signal_handlers_block_by_func (image, pika_text_tool_layer_changed, text_tool); floating_sel_anchor (pika_image_get_floating_selection (image)); g_signal_handlers_unblock_by_func (image, pika_text_tool_layer_changed, text_tool); } g_object_get (text_tool->widget, "x1", &x1, "y1", &y1, "x2", &x2, "y2", &y2, NULL); if (text_tool->text_box_fixed == FALSE) { if (text_tool->text && (text_tool->text->base_dir == PIKA_TEXT_DIRECTION_TTB_RTL || text_tool->text->base_dir == PIKA_TEXT_DIRECTION_TTB_RTL_UPRIGHT)) { x1 -= pika_item_get_width (PIKA_ITEM (layer)); } } pika_item_set_offset (PIKA_ITEM (layer), x1, y1); pika_image_add_layer (image, layer, PIKA_IMAGE_ACTIVE_PARENT, -1, TRUE); if (text_tool->text_box_fixed) { PikaUnit box_unit = text_tool->proxy->box_unit; gdouble xres, yres; pika_image_get_resolution (image, &xres, &yres); g_object_set (text_tool->proxy, "box-mode", PIKA_TEXT_BOX_FIXED, "box-width", pika_pixels_to_units (x2 - x1, box_unit, xres), "box-height", pika_pixels_to_units (y2 - y1, box_unit, yres), NULL); pika_text_tool_apply (text_tool, TRUE); /* unblocks drawing */ } else { pika_text_tool_frame_item (text_tool); } pika_image_undo_group_end (image); pika_image_flush (image); pika_text_tool_set_drawable (text_tool, PIKA_DRAWABLE (layer), FALSE); pika_text_tool_unblock_drawing (text_tool); } #define RESPONSE_NEW 1 static void pika_text_tool_confirm_response (GtkWidget *widget, gint response_id, PikaTextTool *text_tool) { PikaTextLayer *layer = text_tool->layer; gtk_widget_destroy (widget); if (layer && layer->text) { switch (response_id) { case RESPONSE_NEW: pika_text_tool_create_layer (text_tool, layer->text); break; case GTK_RESPONSE_ACCEPT: pika_text_tool_connect (text_tool, layer, layer->text); /* cause the text layer to be rerendered */ g_object_notify (G_OBJECT (text_tool->proxy), "markup"); pika_text_tool_editor_start (text_tool); break; default: break; } } } static void pika_text_tool_confirm_dialog (PikaTextTool *text_tool) { PikaTool *tool = PIKA_TOOL (text_tool); PikaDisplayShell *shell = pika_display_get_shell (tool->display); GtkWidget *dialog; GtkWidget *vbox; GtkWidget *label; g_return_if_fail (text_tool->layer != NULL); if (text_tool->confirm_dialog) { gtk_window_present (GTK_WINDOW (text_tool->confirm_dialog)); return; } dialog = pika_viewable_dialog_new (g_list_prepend (NULL, text_tool->layer), PIKA_CONTEXT (pika_tool_get_options (tool)), _("Confirm Text Editing"), "pika-text-tool-confirm", PIKA_ICON_LAYER_TEXT_LAYER, _("Confirm Text Editing"), GTK_WIDGET (shell), pika_standard_help_func, NULL, _("Create _New Layer"), RESPONSE_NEW, _("_Cancel"), GTK_RESPONSE_CANCEL, _("_Edit"), GTK_RESPONSE_ACCEPT, NULL); pika_dialog_set_alternative_button_order (GTK_DIALOG (dialog), RESPONSE_NEW, GTK_RESPONSE_ACCEPT, GTK_RESPONSE_CANCEL, -1); gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE); g_signal_connect (dialog, "response", G_CALLBACK (pika_text_tool_confirm_response), text_tool); vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); gtk_container_set_border_width (GTK_CONTAINER (vbox), 12); gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), vbox, FALSE, FALSE, 0); gtk_widget_show (vbox); label = gtk_label_new (_("The layer you selected is a text layer but " "it has been modified using other tools. " "Editing the layer with the text tool will " "discard these modifications." "\n\n" "You can edit the layer or create a new " "text layer from its text attributes.")); gtk_label_set_xalign (GTK_LABEL (label), 0.0); gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 0); gtk_widget_show (label); gtk_widget_show (dialog); text_tool->confirm_dialog = dialog; g_signal_connect_swapped (dialog, "destroy", G_CALLBACK (g_nullify_pointer), &text_tool->confirm_dialog); } static void pika_text_tool_layer_changed (PikaImage *image, PikaTextTool *text_tool) { GList *layers = pika_image_get_selected_layers (image); if (g_list_length (layers) != 1 || layers->data != text_tool->layer) { PikaTool *tool = PIKA_TOOL (text_tool); PikaDisplay *display = tool->display; if (display) { PikaLayer *layer = NULL; pika_tool_control (tool, PIKA_TOOL_ACTION_HALT, display); if (g_list_length (layers) == 1) layer = layers->data; /* The tool can only be started when a single layer is * selected and this is a text layer. */ if (layer && pika_text_tool_set_drawable (text_tool, PIKA_DRAWABLE (layer), FALSE) && PIKA_LAYER (text_tool->layer) == layer) { GError *error = NULL; if (! pika_text_tool_start (text_tool, display, layer, &error)) { pika_text_tool_set_drawable (text_tool, NULL, FALSE); pika_tool_message_literal (tool, display, error->message); g_clear_error (&error); return; } } } } } static void pika_text_tool_set_image (PikaTextTool *text_tool, PikaImage *image) { if (text_tool->image == image) return; if (text_tool->image) { g_signal_handlers_disconnect_by_func (text_tool->image, pika_text_tool_layer_changed, text_tool); } g_set_weak_pointer (&text_tool->image, image); if (image) { PikaTextOptions *options = PIKA_TEXT_TOOL_GET_OPTIONS (text_tool); gdouble xres; gdouble yres; g_signal_connect_object (text_tool->image, "selected-layers-changed", G_CALLBACK (pika_text_tool_layer_changed), text_tool, 0); pika_image_get_resolution (image, &xres, &yres); pika_size_entry_set_resolution (PIKA_SIZE_ENTRY (options->size_entry), 0, yres, FALSE); } } static gboolean pika_text_tool_set_drawable (PikaTextTool *text_tool, PikaDrawable *drawable, gboolean confirm) { PikaImage *image = NULL; if (text_tool->confirm_dialog) gtk_widget_destroy (text_tool->confirm_dialog); if (drawable) image = pika_item_get_image (PIKA_ITEM (drawable)); pika_text_tool_set_image (text_tool, image); if (PIKA_IS_TEXT_LAYER (drawable) && PIKA_TEXT_LAYER (drawable)->text) { PikaTextLayer *layer = PIKA_TEXT_LAYER (drawable); if (layer == text_tool->layer && layer->text == text_tool->text) return TRUE; if (layer->modified) { if (confirm) { pika_text_tool_connect (text_tool, layer, NULL); pika_text_tool_confirm_dialog (text_tool); return TRUE; } } else { pika_text_tool_connect (text_tool, layer, layer->text); return TRUE; } } pika_text_tool_connect (text_tool, NULL, NULL); return FALSE; } static void pika_text_tool_block_drawing (PikaTextTool *text_tool) { if (text_tool->drawing_blocked == 0) { pika_draw_tool_pause (PIKA_DRAW_TOOL (text_tool)); pika_text_tool_clear_layout (text_tool); } text_tool->drawing_blocked++; } static void pika_text_tool_unblock_drawing (PikaTextTool *text_tool) { g_return_if_fail (text_tool->drawing_blocked > 0); text_tool->drawing_blocked--; if (text_tool->drawing_blocked == 0) pika_draw_tool_resume (PIKA_DRAW_TOOL (text_tool)); } static void pika_text_tool_buffer_begin_edit (PikaTextBuffer *buffer, PikaTextTool *text_tool) { pika_text_tool_block_drawing (text_tool); } static void pika_text_tool_buffer_end_edit (PikaTextBuffer *buffer, PikaTextTool *text_tool) { if (text_tool->text) { gchar *string; if (pika_text_buffer_has_markup (buffer)) { string = pika_text_buffer_get_markup (buffer); g_object_set (text_tool->proxy, "markup", string, NULL); } else { string = pika_text_buffer_get_text (buffer); g_object_set (text_tool->proxy, "text", string, NULL); } g_free (string); } else { pika_text_tool_create_layer (text_tool, NULL); } pika_text_tool_unblock_drawing (text_tool); } static void pika_text_tool_buffer_color_applied (PikaTextBuffer *buffer, const PikaRGB *color, PikaTextTool *text_tool) { pika_palettes_add_color_history (PIKA_TOOL (text_tool)->tool_info->pika, color); } /* public functions */ void pika_text_tool_clear_layout (PikaTextTool *text_tool) { g_clear_object (&text_tool->layout); } gboolean pika_text_tool_ensure_layout (PikaTextTool *text_tool) { if (! text_tool->layout && text_tool->text) { PikaImage *image = pika_item_get_image (PIKA_ITEM (text_tool->layer)); gdouble xres; gdouble yres; GError *error = NULL; pika_image_get_resolution (image, &xres, &yres); text_tool->layout = pika_text_layout_new (text_tool->layer->text, xres, yres, &error); if (error) { pika_message_literal (image->pika, NULL, PIKA_MESSAGE_ERROR, error->message); g_error_free (error); } } return text_tool->layout != NULL; } void pika_text_tool_apply (PikaTextTool *text_tool, gboolean push_undo) { const GParamSpec *pspec = NULL; PikaImage *image; PikaTextLayer *layer; GList *list; gboolean undo_group = FALSE; if (text_tool->idle_id) { g_source_remove (text_tool->idle_id); text_tool->idle_id = 0; pika_text_tool_unblock_drawing (text_tool); } g_return_if_fail (text_tool->text != NULL); g_return_if_fail (text_tool->layer != NULL); layer = text_tool->layer; image = pika_item_get_image (PIKA_ITEM (layer)); g_return_if_fail (layer->text == text_tool->text); /* Walk over the list of changes and figure out if we are changing * a single property or need to push a full text undo. */ for (list = text_tool->pending; list && list->next && list->next->data == list->data; list = list->next) /* do nothing */; if (g_list_length (list) == 1) pspec = list->data; /* If we are changing a single property, we don't need to push * an undo if all of the following is true: * - the redo stack is empty * - the last item on the undo stack is a text undo * - the last undo changed the same text property on the same layer * - the last undo happened less than TEXT_UNDO_TIMEOUT seconds ago */ if (pspec) { PikaUndo *undo = pika_image_undo_can_compress (image, PIKA_TYPE_TEXT_UNDO, PIKA_UNDO_TEXT_LAYER); if (undo && PIKA_ITEM_UNDO (undo)->item == PIKA_ITEM (layer)) { PikaTextUndo *text_undo = PIKA_TEXT_UNDO (undo); if (text_undo->pspec == pspec) { if (pika_undo_get_age (undo) < TEXT_UNDO_TIMEOUT) { PikaTool *tool = PIKA_TOOL (text_tool); PikaContext *context; context = PIKA_CONTEXT (pika_tool_get_options (tool)); push_undo = FALSE; pika_undo_reset_age (undo); pika_undo_refresh_preview (undo, context); } } } } if (push_undo) { if (layer->modified) { undo_group = TRUE; pika_image_undo_group_start (image, PIKA_UNDO_GROUP_TEXT, NULL); pika_image_undo_push_text_layer_modified (image, NULL, layer); /* see comment in pika_text_layer_set() */ pika_image_undo_push_drawable_mod (image, NULL, PIKA_DRAWABLE (layer), TRUE); } if (pspec) pika_image_undo_push_text_layer (image, NULL, layer, pspec); } pika_text_tool_apply_list (text_tool, list); g_list_free (text_tool->pending); text_tool->pending = NULL; if (push_undo) { g_object_set (layer, "modified", FALSE, NULL); if (undo_group) pika_image_undo_group_end (image); } pika_text_tool_frame_item (text_tool); pika_image_flush (image); } gboolean pika_text_tool_set_layer (PikaTextTool *text_tool, PikaLayer *layer) { g_return_val_if_fail (PIKA_IS_TEXT_TOOL (text_tool), FALSE); g_return_val_if_fail (layer == NULL || PIKA_IS_LAYER (layer), FALSE); if (layer == PIKA_LAYER (text_tool->layer)) return TRUE; /* FIXME this function works, and I have no clue why: first we set * the drawable, then we HALT the tool and start() it without * re-setting the drawable. Why this works perfectly anyway when * double clicking a text layer in the layers dialog... no idea. */ if (pika_text_tool_set_drawable (text_tool, PIKA_DRAWABLE (layer), TRUE)) { PikaTool *tool = PIKA_TOOL (text_tool); PikaItem *item = PIKA_ITEM (layer); PikaContext *context; PikaDisplay *display; context = pika_get_user_context (tool->tool_info->pika); display = pika_context_get_display (context); if (! display || pika_display_get_image (display) != pika_item_get_image (item)) { GList *list; display = NULL; for (list = pika_get_display_iter (tool->tool_info->pika); list; list = g_list_next (list)) { display = list->data; if (pika_display_get_image (display) == pika_item_get_image (item)) { pika_context_set_display (context, display); break; } display = NULL; } } if (tool->display) pika_tool_control (tool, PIKA_TOOL_ACTION_HALT, tool->display); if (display) { GError *error = NULL; if (! pika_text_tool_start (text_tool, display, layer, &error)) { pika_text_tool_set_drawable (text_tool, NULL, FALSE); pika_tool_message_literal (tool, display, error->message); g_clear_error (&error); return FALSE; } g_list_free (tool->drawables); tool->drawables = g_list_prepend (NULL, PIKA_DRAWABLE (layer)); } } return TRUE; } gboolean pika_text_tool_get_has_text_selection (PikaTextTool *text_tool) { GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer); return gtk_text_buffer_get_has_selection (buffer); } void pika_text_tool_delete_selection (PikaTextTool *text_tool) { GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer); if (gtk_text_buffer_get_has_selection (buffer)) { gtk_text_buffer_delete_selection (buffer, TRUE, TRUE); } } void pika_text_tool_cut_clipboard (PikaTextTool *text_tool) { PikaDisplayShell *shell; GtkClipboard *clipboard; g_return_if_fail (PIKA_IS_TEXT_TOOL (text_tool)); shell = pika_display_get_shell (PIKA_TOOL (text_tool)->display); clipboard = gtk_widget_get_clipboard (GTK_WIDGET (shell), GDK_SELECTION_CLIPBOARD); gtk_text_buffer_cut_clipboard (GTK_TEXT_BUFFER (text_tool->buffer), clipboard, TRUE); } void pika_text_tool_copy_clipboard (PikaTextTool *text_tool) { PikaDisplayShell *shell; GtkClipboard *clipboard; g_return_if_fail (PIKA_IS_TEXT_TOOL (text_tool)); shell = pika_display_get_shell (PIKA_TOOL (text_tool)->display); clipboard = gtk_widget_get_clipboard (GTK_WIDGET (shell), GDK_SELECTION_CLIPBOARD); /* need to block "end-user-action" on the text buffer, because * GtkTextBuffer considers copying text to the clipboard an * undo-relevant user action, which is clearly a bug, but what * can we do... */ g_signal_handlers_block_by_func (text_tool->buffer, pika_text_tool_buffer_begin_edit, text_tool); g_signal_handlers_block_by_func (text_tool->buffer, pika_text_tool_buffer_end_edit, text_tool); gtk_text_buffer_copy_clipboard (GTK_TEXT_BUFFER (text_tool->buffer), clipboard); g_signal_handlers_unblock_by_func (text_tool->buffer, pika_text_tool_buffer_end_edit, text_tool); g_signal_handlers_unblock_by_func (text_tool->buffer, pika_text_tool_buffer_begin_edit, text_tool); } void pika_text_tool_paste_clipboard (PikaTextTool *text_tool) { PikaDisplayShell *shell; GtkClipboard *clipboard; g_return_if_fail (PIKA_IS_TEXT_TOOL (text_tool)); shell = pika_display_get_shell (PIKA_TOOL (text_tool)->display); clipboard = gtk_widget_get_clipboard (GTK_WIDGET (shell), GDK_SELECTION_CLIPBOARD); gtk_text_buffer_paste_clipboard (GTK_TEXT_BUFFER (text_tool->buffer), clipboard, NULL, TRUE); } void pika_text_tool_create_vectors (PikaTextTool *text_tool) { PikaVectors *vectors; g_return_if_fail (PIKA_IS_TEXT_TOOL (text_tool)); if (! text_tool->text || ! text_tool->image) return; vectors = pika_text_vectors_new (text_tool->image, text_tool->text); if (text_tool->layer) { gint x, y; pika_item_get_offset (PIKA_ITEM (text_tool->layer), &x, &y); pika_item_translate (PIKA_ITEM (vectors), x, y, FALSE); } pika_image_add_vectors (text_tool->image, vectors, PIKA_IMAGE_ACTIVE_PARENT, -1, TRUE); pika_image_flush (text_tool->image); } gboolean pika_text_tool_create_vectors_warped (PikaTextTool *text_tool, GError **error) { GList *vectors0; PikaVectors *vectors; gdouble box_width; gdouble box_height; PikaTextDirection dir; gdouble offset = 0.0; g_return_val_if_fail (PIKA_IS_TEXT_TOOL (text_tool), FALSE); if (! text_tool->text || ! text_tool->image || ! text_tool->layer) { if (! text_tool->text) g_set_error_literal (error, PIKA_ERROR, PIKA_FAILED, _("Text is required.")); if (! text_tool->image) g_set_error_literal (error, PIKA_ERROR, PIKA_FAILED, _("No image.")); if (! text_tool->layer) g_set_error_literal (error, PIKA_ERROR, PIKA_FAILED, _("No layer.")); return FALSE; } box_width = pika_item_get_width (PIKA_ITEM (text_tool->layer)); box_height = pika_item_get_height (PIKA_ITEM (text_tool->layer)); vectors0 = pika_image_get_selected_vectors (text_tool->image); if (g_list_length (vectors0) != 1) { g_set_error_literal (error, PIKA_ERROR, PIKA_FAILED, _("Exactly one path must be selected.")); return FALSE; } vectors = pika_text_vectors_new (text_tool->image, text_tool->text); offset = 0; dir = pika_text_tool_get_direction (text_tool); switch (dir) { case PIKA_TEXT_DIRECTION_LTR: case PIKA_TEXT_DIRECTION_RTL: offset = 0.5 * box_height; break; case PIKA_TEXT_DIRECTION_TTB_RTL: case PIKA_TEXT_DIRECTION_TTB_RTL_UPRIGHT: case PIKA_TEXT_DIRECTION_TTB_LTR: case PIKA_TEXT_DIRECTION_TTB_LTR_UPRIGHT: { PikaStroke *stroke = NULL; while ((stroke = pika_vectors_stroke_get_next (vectors, stroke))) { pika_stroke_rotate (stroke, 0, 0, 270); pika_stroke_translate (stroke, 0, box_width); } } offset = 0.5 * box_width; break; } pika_vectors_warp_vectors (vectors0->data, vectors, offset); pika_item_set_visible (PIKA_ITEM (vectors), TRUE, FALSE); pika_image_add_vectors (text_tool->image, vectors, PIKA_IMAGE_ACTIVE_PARENT, -1, TRUE); pika_image_flush (text_tool->image); return TRUE; } PikaTextDirection pika_text_tool_get_direction (PikaTextTool *text_tool) { PikaTextOptions *options = PIKA_TEXT_TOOL_GET_OPTIONS (text_tool); return options->base_dir; }