diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 03:13:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 03:13:10 +0000 |
commit | 3c57dd931145d43f2b0aef96c4d178135956bf91 (patch) | |
tree | 3de698981e9f0cc2c4f9569b19a5f3595e741f6b /app/tools/gimptexttool.c | |
parent | Initial commit. (diff) | |
download | gimp-3c57dd931145d43f2b0aef96c4d178135956bf91.tar.xz gimp-3c57dd931145d43f2b0aef96c4d178135956bf91.zip |
Adding upstream version 2.10.36.upstream/2.10.36
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'app/tools/gimptexttool.c')
-rw-r--r-- | app/tools/gimptexttool.c | 2388 |
1 files changed, 2388 insertions, 0 deletions
diff --git a/app/tools/gimptexttool.c b/app/tools/gimptexttool.c new file mode 100644 index 0000000..45023ab --- /dev/null +++ b/app/tools/gimptexttool.c @@ -0,0 +1,2388 @@ +/* GIMP - The GNU Image Manipulation Program + * Copyright (C) 1995 Spencer Kimball and Peter Mattis + * + * GimpTextTool + * Copyright (C) 2002-2010 Sven Neumann <sven@gimp.org> + * Daniel Eddeland <danedde@svn.gnome.org> + * Michael Natterer <mitch@gimp.org> + * + * 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 <https://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include <gegl.h> +#include <gtk/gtk.h> + +#include "libgimpbase/gimpbase.h" +#include "libgimpconfig/gimpconfig.h" +#include "libgimpwidgets/gimpwidgets.h" + +#include "tools-types.h" + +#include "core/gimp.h" +#include "core/gimpasyncset.h" +#include "core/gimpcontext.h" +#include "core/gimpdatafactory.h" +#include "core/gimperror.h" +#include "core/gimpimage.h" +#include "core/gimp-palettes.h" +#include "core/gimpimage-pick-item.h" +#include "core/gimpimage-undo.h" +#include "core/gimpimage-undo-push.h" +#include "core/gimplayer-floating-selection.h" +#include "core/gimpmarshal.h" +#include "core/gimptoolinfo.h" +#include "core/gimpundostack.h" + +#include "text/gimptext.h" +#include "text/gimptext-vectors.h" +#include "text/gimptextlayer.h" +#include "text/gimptextlayout.h" +#include "text/gimptextundo.h" + +#include "vectors/gimpstroke.h" +#include "vectors/gimpvectors.h" +#include "vectors/gimpvectors-warp.h" + +#include "widgets/gimpdialogfactory.h" +#include "widgets/gimpdockcontainer.h" +#include "widgets/gimphelp-ids.h" +#include "widgets/gimpmenufactory.h" +#include "widgets/gimptextbuffer.h" +#include "widgets/gimpuimanager.h" +#include "widgets/gimpviewabledialog.h" + +#include "display/gimpcanvasgroup.h" +#include "display/gimpdisplay.h" +#include "display/gimpdisplayshell.h" +#include "display/gimptoolrectangle.h" + +#include "gimptextoptions.h" +#include "gimptexttool.h" +#include "gimptexttool-editor.h" +#include "gimptoolcontrol.h" + +#include "gimp-intl.h" + + +#define TEXT_UNDO_TIMEOUT 3 + + +/* local function prototypes */ + +static void gimp_text_tool_constructed (GObject *object); +static void gimp_text_tool_finalize (GObject *object); + +static void gimp_text_tool_control (GimpTool *tool, + GimpToolAction action, + GimpDisplay *display); +static void gimp_text_tool_button_press (GimpTool *tool, + const GimpCoords *coords, + guint32 time, + GdkModifierType state, + GimpButtonPressType press_type, + GimpDisplay *display); +static void gimp_text_tool_button_release (GimpTool *tool, + const GimpCoords *coords, + guint32 time, + GdkModifierType state, + GimpButtonReleaseType release_type, + GimpDisplay *display); +static void gimp_text_tool_motion (GimpTool *tool, + const GimpCoords *coords, + guint32 time, + GdkModifierType state, + GimpDisplay *display); +static gboolean gimp_text_tool_key_press (GimpTool *tool, + GdkEventKey *kevent, + GimpDisplay *display); +static gboolean gimp_text_tool_key_release (GimpTool *tool, + GdkEventKey *kevent, + GimpDisplay *display); +static void gimp_text_tool_oper_update (GimpTool *tool, + const GimpCoords *coords, + GdkModifierType state, + gboolean proximity, + GimpDisplay *display); +static void gimp_text_tool_cursor_update (GimpTool *tool, + const GimpCoords *coords, + GdkModifierType state, + GimpDisplay *display); +static GimpUIManager * gimp_text_tool_get_popup (GimpTool *tool, + const GimpCoords *coords, + GdkModifierType state, + GimpDisplay *display, + const gchar **ui_path); + +static void gimp_text_tool_draw (GimpDrawTool *draw_tool); +static void gimp_text_tool_draw_selection (GimpDrawTool *draw_tool); + +static gboolean gimp_text_tool_start (GimpTextTool *text_tool, + GimpDisplay *display, + GimpLayer *layer, + GError **error); +static void gimp_text_tool_halt (GimpTextTool *text_tool); + +static void gimp_text_tool_frame_item (GimpTextTool *text_tool); + +static void gimp_text_tool_rectangle_response + (GimpToolRectangle *rectangle, + gint response_id, + GimpTextTool *text_tool); +static void gimp_text_tool_rectangle_change_complete + (GimpToolRectangle *rectangle, + GimpTextTool *text_tool); + +static void gimp_text_tool_connect (GimpTextTool *text_tool, + GimpTextLayer *layer, + GimpText *text); + +static void gimp_text_tool_layer_notify (GimpTextLayer *layer, + const GParamSpec *pspec, + GimpTextTool *text_tool); +static void gimp_text_tool_proxy_notify (GimpText *text, + const GParamSpec *pspec, + GimpTextTool *text_tool); + +static void gimp_text_tool_text_notify (GimpText *text, + const GParamSpec *pspec, + GimpTextTool *text_tool); +static void gimp_text_tool_text_changed (GimpText *text, + GimpTextTool *text_tool); + +static void + gimp_text_tool_fonts_async_set_empty_notify (GimpAsyncSet *async_set, + GParamSpec *pspec, + GimpTextTool *text_tool); + +static void gimp_text_tool_apply_list (GimpTextTool *text_tool, + GList *pspecs); + +static void gimp_text_tool_create_layer (GimpTextTool *text_tool, + GimpText *text); + +static void gimp_text_tool_layer_changed (GimpImage *image, + GimpTextTool *text_tool); +static void gimp_text_tool_set_image (GimpTextTool *text_tool, + GimpImage *image); +static gboolean gimp_text_tool_set_drawable (GimpTextTool *text_tool, + GimpDrawable *drawable, + gboolean confirm); + +static void gimp_text_tool_block_drawing (GimpTextTool *text_tool); +static void gimp_text_tool_unblock_drawing (GimpTextTool *text_tool); + +static void gimp_text_tool_buffer_begin_edit (GimpTextBuffer *buffer, + GimpTextTool *text_tool); +static void gimp_text_tool_buffer_end_edit (GimpTextBuffer *buffer, + GimpTextTool *text_tool); + +static void gimp_text_tool_buffer_color_applied + (GimpTextBuffer *buffer, + const GimpRGB *color, + GimpTextTool *text_tool); + + +G_DEFINE_TYPE (GimpTextTool, gimp_text_tool, GIMP_TYPE_DRAW_TOOL) + +#define parent_class gimp_text_tool_parent_class + + +void +gimp_text_tool_register (GimpToolRegisterCallback callback, + gpointer data) +{ + (* callback) (GIMP_TYPE_TEXT_TOOL, + GIMP_TYPE_TEXT_OPTIONS, + gimp_text_options_gui, + GIMP_CONTEXT_PROP_MASK_FOREGROUND | + GIMP_CONTEXT_PROP_MASK_FONT | + GIMP_CONTEXT_PROP_MASK_PALETTE /* for the color popup's palette tab */, + "gimp-text-tool", + _("Text"), + _("Text Tool: Create or edit text layers"), + N_("Te_xt"), "T", + NULL, GIMP_HELP_TOOL_TEXT, + GIMP_ICON_TOOL_TEXT, + data); +} + +static void +gimp_text_tool_class_init (GimpTextToolClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GimpToolClass *tool_class = GIMP_TOOL_CLASS (klass); + GimpDrawToolClass *draw_tool_class = GIMP_DRAW_TOOL_CLASS (klass); + + object_class->constructed = gimp_text_tool_constructed; + object_class->finalize = gimp_text_tool_finalize; + + tool_class->control = gimp_text_tool_control; + tool_class->button_press = gimp_text_tool_button_press; + tool_class->motion = gimp_text_tool_motion; + tool_class->button_release = gimp_text_tool_button_release; + tool_class->key_press = gimp_text_tool_key_press; + tool_class->key_release = gimp_text_tool_key_release; + tool_class->oper_update = gimp_text_tool_oper_update; + tool_class->cursor_update = gimp_text_tool_cursor_update; + tool_class->get_popup = gimp_text_tool_get_popup; + + draw_tool_class->draw = gimp_text_tool_draw; +} + +static void +gimp_text_tool_init (GimpTextTool *text_tool) +{ + GimpTool *tool = GIMP_TOOL (text_tool); + + text_tool->buffer = gimp_text_buffer_new (); + + g_signal_connect (text_tool->buffer, "begin-user-action", + G_CALLBACK (gimp_text_tool_buffer_begin_edit), + text_tool); + g_signal_connect (text_tool->buffer, "end-user-action", + G_CALLBACK (gimp_text_tool_buffer_end_edit), + text_tool); + g_signal_connect (text_tool->buffer, "color-applied", + G_CALLBACK (gimp_text_tool_buffer_color_applied), + text_tool); + + text_tool->handle_rectangle_change_complete = TRUE; + + gimp_text_tool_editor_init (text_tool); + + gimp_tool_control_set_scroll_lock (tool->control, TRUE); + gimp_tool_control_set_handle_empty_image (tool->control, TRUE); + gimp_tool_control_set_wants_click (tool->control, TRUE); + gimp_tool_control_set_wants_double_click (tool->control, TRUE); + gimp_tool_control_set_wants_triple_click (tool->control, TRUE); + gimp_tool_control_set_wants_all_key_events (tool->control, TRUE); + gimp_tool_control_set_active_modifiers (tool->control, + GIMP_TOOL_ACTIVE_MODIFIERS_SEPARATE); + gimp_tool_control_set_precision (tool->control, + GIMP_CURSOR_PRECISION_PIXEL_BORDER); + gimp_tool_control_set_tool_cursor (tool->control, + GIMP_TOOL_CURSOR_TEXT); + gimp_tool_control_set_action_object_1 (tool->control, + "context/context-font-select-set"); +} + +static void +gimp_text_tool_constructed (GObject *object) +{ + GimpTextTool *text_tool = GIMP_TEXT_TOOL (object); + GimpTextOptions *options = GIMP_TEXT_TOOL_GET_OPTIONS (text_tool); + GimpTool *tool = GIMP_TOOL (text_tool); + GimpAsyncSet *async_set; + + G_OBJECT_CLASS (parent_class)->constructed (object); + + text_tool->proxy = g_object_new (GIMP_TYPE_TEXT, NULL); + + gimp_text_options_connect_text (options, text_tool->proxy); + + g_signal_connect_object (text_tool->proxy, "notify", + G_CALLBACK (gimp_text_tool_proxy_notify), + text_tool, 0); + + async_set = + gimp_data_factory_get_async_set (tool->tool_info->gimp->font_factory); + + g_signal_connect_object (async_set, + "notify::empty", + G_CALLBACK (gimp_text_tool_fonts_async_set_empty_notify), + text_tool, 0); +} + +static void +gimp_text_tool_finalize (GObject *object) +{ + GimpTextTool *text_tool = GIMP_TEXT_TOOL (object); + + g_clear_object (&text_tool->proxy); + g_clear_object (&text_tool->buffer); + g_clear_object (&text_tool->ui_manager); + + gimp_text_tool_editor_finalize (text_tool); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +gimp_text_tool_remove_empty_text_layer (GimpTextTool *text_tool) +{ + GimpTextLayer *text_layer = text_tool->layer; + + if (text_layer && text_layer->auto_rename) + { + GimpText *text = gimp_text_layer_get_text (text_layer); + + if (text && text->box_mode == GIMP_TEXT_BOX_DYNAMIC && + (! text->text || text->text[0] == '\0') && + (! text->markup || text->markup[0] == '\0')) + { + GimpImage *image = gimp_item_get_image (GIMP_ITEM (text_layer)); + + if (text_tool->image == image) + g_signal_handlers_block_by_func (image, + gimp_text_tool_layer_changed, + text_tool); + + gimp_image_remove_layer (image, GIMP_LAYER (text_layer), TRUE, NULL); + gimp_image_flush (image); + + if (text_tool->image == image) + g_signal_handlers_unblock_by_func (image, + gimp_text_tool_layer_changed, + text_tool); + } + } +} + +static void +gimp_text_tool_control (GimpTool *tool, + GimpToolAction action, + GimpDisplay *display) +{ + GimpTextTool *text_tool = GIMP_TEXT_TOOL (tool); + + switch (action) + { + case GIMP_TOOL_ACTION_PAUSE: + case GIMP_TOOL_ACTION_RESUME: + break; + + case GIMP_TOOL_ACTION_HALT: + gimp_text_tool_halt (text_tool); + break; + + case GIMP_TOOL_ACTION_COMMIT: + break; + } + + GIMP_TOOL_CLASS (parent_class)->control (tool, action, display); +} + +static void +gimp_text_tool_button_press (GimpTool *tool, + const GimpCoords *coords, + guint32 time, + GdkModifierType state, + GimpButtonPressType press_type, + GimpDisplay *display) +{ + GimpTextTool *text_tool = GIMP_TEXT_TOOL (tool); + GimpImage *image = gimp_display_get_image (display); + GimpText *text = text_tool->text; + GimpToolRectangle *rectangle; + + gimp_draw_tool_pause (GIMP_DRAW_TOOL (tool)); + + if (tool->display && tool->display != display) + gimp_tool_control (tool, GIMP_TOOL_ACTION_HALT, display); + + if (! text_tool->widget) + { + GError *error = NULL; + + if (! gimp_text_tool_start (text_tool, display, NULL, &error)) + { + gimp_draw_tool_resume (GIMP_DRAW_TOOL (tool)); + + gimp_tool_message_literal (tool, display, error->message); + + g_clear_error (&error); + + return; + } + + gimp_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. + */ + gimp_tool_rectangle_set_function (GIMP_TOOL_RECTANGLE (text_tool->widget), + GIMP_TOOL_RECTANGLE_CREATING); + } + + rectangle = GIMP_TOOL_RECTANGLE (text_tool->widget); + + if (press_type == GIMP_BUTTON_PRESS_NORMAL) + { + gimp_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) + gimp_text_tool_abort_im_context (text_tool); + else + gimp_text_tool_reset_im_context (text_tool); + + text_tool->selecting = FALSE; + + if (gimp_tool_rectangle_point_in_rectangle (rectangle, + coords->x, + coords->y) && + ! text_tool->moving) + { + gimp_tool_rectangle_set_function (rectangle, + GIMP_TOOL_RECTANGLE_DEAD); + } + else if (gimp_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 (gimp_tool_rectangle_get_function (rectangle) != + GIMP_TOOL_RECTANGLE_CREATING) + { + if (text_tool->layer) + { + GimpItem *item = GIMP_ITEM (text_tool->layer); + gdouble x = coords->x - gimp_item_get_offset_x (item); + gdouble y = coords->y - gimp_item_get_offset_y (item); + + if (x < 0 || x >= gimp_item_get_width (item) || + y < 0 || y >= gimp_item_get_height (item)) + { + gimp_draw_tool_resume (GIMP_DRAW_TOOL (tool)); + return; + } + } + else + { + gimp_draw_tool_resume (GIMP_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 && + gimp_tool_rectangle_get_function (rectangle) == + GIMP_TOOL_RECTANGLE_CREATING) + { + GimpTextLayer *text_layer; + + text_layer = gimp_image_pick_text_layer (image, coords->x, coords->y); + + if (text_layer && text_layer != text_tool->layer) + { + if (text_tool->image == image) + g_signal_handlers_block_by_func (image, + gimp_text_tool_layer_changed, + text_tool); + + gimp_image_set_active_layer (image, GIMP_LAYER (text_layer)); + + if (text_tool->image == image) + g_signal_handlers_unblock_by_func (image, + gimp_text_tool_layer_changed, + text_tool); + } + } + } + + if (gimp_image_coords_in_active_pickable (image, coords, FALSE, FALSE, FALSE)) + { + GimpDrawable *drawable = gimp_image_get_active_drawable (image); + GimpItem *item = GIMP_ITEM (drawable); + gdouble x = coords->x - gimp_item_get_offset_x (item); + gdouble y = coords->y - gimp_item_get_offset_y (item); + + /* did the user click on a text layer? */ + if (gimp_text_tool_set_drawable (text_tool, drawable, TRUE)) + { + if (press_type == GIMP_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 (gimp_tool_rectangle_get_function (rectangle) == + GIMP_TOOL_RECTANGLE_CREATING) + { + gimp_tool_rectangle_set_function (rectangle, + GIMP_TOOL_RECTANGLE_DEAD); + + gimp_text_tool_frame_item (text_tool); + } + + if (text_tool->text && text_tool->text != text) + { + gimp_text_tool_editor_start (text_tool); + } + } + + if (text_tool->text && ! text_tool->moving) + { + text_tool->selecting = TRUE; + + gimp_text_tool_editor_button_press (text_tool, x, y, press_type); + } + else + { + text_tool->selecting = FALSE; + } + + gimp_draw_tool_resume (GIMP_DRAW_TOOL (tool)); + + return; + } + } + + if (press_type == GIMP_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. + */ + gimp_text_tool_set_image (text_tool, image); + + gimp_text_tool_connect (text_tool, NULL, NULL); + gimp_text_tool_editor_start (text_tool); + } + + gimp_draw_tool_resume (GIMP_DRAW_TOOL (tool)); +} + +static void +gimp_text_tool_button_release (GimpTool *tool, + const GimpCoords *coords, + guint32 time, + GdkModifierType state, + GimpButtonReleaseType release_type, + GimpDisplay *display) +{ + GimpTextTool *text_tool = GIMP_TEXT_TOOL (tool); + GimpToolRectangle *rectangle = GIMP_TOOL_RECTANGLE (text_tool->widget); + + gimp_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, + gimp_text_tool_buffer_begin_edit, + text_tool); + g_signal_handlers_block_by_func (text_tool->buffer, + gimp_text_tool_buffer_end_edit, + text_tool); + + gimp_text_tool_editor_button_release (text_tool); + + g_signal_handlers_unblock_by_func (text_tool->buffer, + gimp_text_tool_buffer_end_edit, + text_tool); + g_signal_handlers_unblock_by_func (text_tool->buffer, + gimp_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 == GIMP_BUTTON_RELEASE_CANCEL) + release_type = GIMP_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 (gimp_tool_rectangle_get_function (rectangle) == + GIMP_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 == GIMP_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 == GIMP_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", GIMP_TEXT_BOX_DYNAMIC, + NULL); + + text_tool->handle_rectangle_change_complete = FALSE; + } + } + + if (text_tool->grab_widget) + { + gimp_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 +gimp_text_tool_motion (GimpTool *tool, + const GimpCoords *coords, + guint32 time, + GdkModifierType state, + GimpDisplay *display) +{ + GimpTextTool *text_tool = GIMP_TEXT_TOOL (tool); + + if (! text_tool->selecting) + { + if (text_tool->grab_widget) + { + gimp_tool_widget_motion (text_tool->grab_widget, + coords, time, state); + } + } + else + { + GimpItem *item = GIMP_ITEM (text_tool->layer); + gdouble x = coords->x - gimp_item_get_offset_x (item); + gdouble y = coords->y - gimp_item_get_offset_y (item); + + gimp_text_tool_editor_motion (text_tool, x, y); + } +} + +static gboolean +gimp_text_tool_key_press (GimpTool *tool, + GdkEventKey *kevent, + GimpDisplay *display) +{ + GimpTextTool *text_tool = GIMP_TEXT_TOOL (tool); + + if (display == tool->display) + return gimp_text_tool_editor_key_press (text_tool, kevent); + + return FALSE; +} + +static gboolean +gimp_text_tool_key_release (GimpTool *tool, + GdkEventKey *kevent, + GimpDisplay *display) +{ + GimpTextTool *text_tool = GIMP_TEXT_TOOL (tool); + + if (display == tool->display) + return gimp_text_tool_editor_key_release (text_tool, kevent); + + return FALSE; +} + +static void +gimp_text_tool_oper_update (GimpTool *tool, + const GimpCoords *coords, + GdkModifierType state, + gboolean proximity, + GimpDisplay *display) +{ + GimpTextTool *text_tool = GIMP_TEXT_TOOL (tool); + GimpToolRectangle *rectangle = GIMP_TOOL_RECTANGLE (text_tool->widget); + + GIMP_TOOL_CLASS (parent_class)->oper_update (tool, coords, state, + proximity, display); + + text_tool->moving = (text_tool->widget && + gimp_tool_rectangle_get_function (rectangle) == + GIMP_TOOL_RECTANGLE_MOVING && + (state & GDK_MOD1_MASK)); +} + +static void +gimp_text_tool_cursor_update (GimpTool *tool, + const GimpCoords *coords, + GdkModifierType state, + GimpDisplay *display) +{ + GimpTextTool *text_tool = GIMP_TEXT_TOOL (tool); + GimpToolRectangle *rectangle = GIMP_TOOL_RECTANGLE (text_tool->widget); + + if (rectangle && tool->display == display) + { + if (gimp_tool_rectangle_point_in_rectangle (rectangle, + coords->x, + coords->y) && + ! text_tool->moving) + { + gimp_tool_set_cursor (tool, display, + (GimpCursorType) GDK_XTERM, + gimp_tool_control_get_tool_cursor (tool->control), + GIMP_CURSOR_MODIFIER_NONE); + } + else + { + GIMP_TOOL_CLASS (parent_class)->cursor_update (tool, coords, state, + display); + } + } + else + { + GimpAsyncSet *async_set; + + async_set = + gimp_data_factory_get_async_set (tool->tool_info->gimp->font_factory); + + if (gimp_async_set_is_empty (async_set)) + { + GIMP_TOOL_CLASS (parent_class)->cursor_update (tool, coords, state, + display); + } + else + { + gimp_tool_set_cursor (tool, display, + gimp_tool_control_get_cursor (tool->control), + gimp_tool_control_get_tool_cursor (tool->control), + GIMP_CURSOR_MODIFIER_BAD); + } + } +} + +static GimpUIManager * +gimp_text_tool_get_popup (GimpTool *tool, + const GimpCoords *coords, + GdkModifierType state, + GimpDisplay *display, + const gchar **ui_path) +{ + GimpTextTool *text_tool = GIMP_TEXT_TOOL (tool); + GimpToolRectangle *rectangle = GIMP_TOOL_RECTANGLE (text_tool->widget); + + if (rectangle && + gimp_tool_rectangle_point_in_rectangle (rectangle, + coords->x, + coords->y)) + { + if (! text_tool->ui_manager) + { + GimpDisplayShell *shell = gimp_display_get_shell (tool->display); + GimpImageWindow *image_window; + GimpDialogFactory *dialog_factory; + GtkWidget *im_menu; + GList *children; + + image_window = gimp_display_shell_get_window (shell); + dialog_factory = gimp_dock_container_get_dialog_factory (GIMP_DOCK_CONTAINER (image_window)); + + text_tool->ui_manager = + gimp_menu_factory_manager_new (gimp_dialog_factory_get_menu_factory (dialog_factory), + "<TextTool>", + text_tool, FALSE); + + im_menu = gimp_ui_manager_get_widget (text_tool->ui_manager, + "/text-tool-popup/text-tool-input-methods-menu"); + + if (GTK_IS_MENU_ITEM (im_menu)) + im_menu = gtk_menu_item_get_submenu (GTK_MENU_ITEM (im_menu)); + + /* hide the generated "empty" item */ + children = gtk_container_get_children (GTK_CONTAINER (im_menu)); + while (children) + { + gtk_widget_hide (children->data); + children = g_list_remove (children, children->data); + } + + gtk_im_multicontext_append_menuitems (GTK_IM_MULTICONTEXT (text_tool->im_context), + GTK_MENU_SHELL (im_menu)); + } + + gimp_ui_manager_update (text_tool->ui_manager, text_tool); + + *ui_path = "/text-tool-popup"; + + return text_tool->ui_manager; + } + + return NULL; +} + +static void +gimp_text_tool_draw (GimpDrawTool *draw_tool) +{ + GimpTextTool *text_tool = GIMP_TEXT_TOOL (draw_tool); + + GIMP_DRAW_TOOL_CLASS (parent_class)->draw (draw_tool); + + if (! text_tool->text || + ! text_tool->layer || + ! text_tool->layer->text) + { + gimp_text_tool_editor_update_im_cursor (text_tool); + + return; + } + + gimp_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 */ + + gimp_text_tool_draw_selection (draw_tool); + } + else + { + /* If the text buffer has no selection, draw the text cursor */ + + GimpCanvasItem *item; + PangoRectangle cursor_rect; + gint off_x, off_y; + gboolean overwrite; + GimpTextDirection direction; + + gimp_text_tool_editor_get_cursor_rect (text_tool, + text_tool->overwrite_mode, + &cursor_rect); + + gimp_item_get_offset (GIMP_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 = gimp_text_tool_get_direction (text_tool); + + item = gimp_draw_tool_add_text_cursor (draw_tool, &cursor_rect, + overwrite, direction); + gimp_canvas_item_set_highlight (item, TRUE); + } + + gimp_text_tool_editor_update_im_cursor (text_tool); +} + +static void +gimp_text_tool_draw_selection (GimpDrawTool *draw_tool) +{ + GimpTextTool *text_tool = GIMP_TEXT_TOOL (draw_tool); + GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer); + GimpCanvasGroup *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; + GimpTextDirection direction; + + group = gimp_draw_tool_add_stroke_group (draw_tool); + gimp_canvas_item_set_highlight (GIMP_CANVAS_ITEM (group), TRUE); + + gtk_text_buffer_get_selection_bounds (buffer, &sel_start, &sel_end); + + min = gimp_text_buffer_get_iter_index (text_tool->buffer, &sel_start, TRUE); + max = gimp_text_buffer_get_iter_index (text_tool->buffer, &sel_end, TRUE); + + layout = gimp_text_layout_get_pango_layout (text_tool->layout); + + gimp_text_layout_get_offsets (text_tool->layout, &offset_x, &offset_y); + + gimp_text_layout_get_size (text_tool->layout, &width, &height); + + gimp_item_get_offset (GIMP_ITEM (text_tool->layer), &off_x, &off_y); + offset_x += off_x; + offset_y += off_y; + + direction = gimp_text_tool_get_direction (text_tool); + + iter = pango_layout_get_iter (layout); + + gimp_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); + + gimp_text_layout_transform_rect (text_tool->layout, &rect); + + switch (direction) + { + case GIMP_TEXT_DIRECTION_LTR: + case GIMP_TEXT_DIRECTION_RTL: + rect.x += offset_x; + rect.y += offset_y; + gimp_draw_tool_add_rectangle (draw_tool, FALSE, + rect.x, rect.y, + rect.width, rect.height); + break; + case GIMP_TEXT_DIRECTION_TTB_RTL: + case GIMP_TEXT_DIRECTION_TTB_RTL_UPRIGHT: + rect.y = offset_x - rect.y + width; + rect.x = offset_y + rect.x; + gimp_draw_tool_add_rectangle (draw_tool, FALSE, + rect.y, rect.x, + -rect.height, rect.width); + break; + case GIMP_TEXT_DIRECTION_TTB_LTR: + case GIMP_TEXT_DIRECTION_TTB_LTR_UPRIGHT: + rect.y = offset_x + rect.y; + rect.x = offset_y - rect.x + height; + gimp_draw_tool_add_rectangle (draw_tool, FALSE, + rect.y, rect.x, + rect.height, -rect.width); + break; + } + } + } + while (pango_layout_iter_next_char (iter)); + + gimp_draw_tool_pop_group (draw_tool); + + pango_layout_iter_free (iter); +} + +static gboolean +gimp_text_tool_start (GimpTextTool *text_tool, + GimpDisplay *display, + GimpLayer *layer, + GError **error) +{ + GimpTool *tool = GIMP_TOOL (text_tool); + GimpDisplayShell *shell = gimp_display_get_shell (display); + GimpToolWidget *widget; + GimpAsyncSet *async_set; + + async_set = + gimp_data_factory_get_async_set (tool->tool_info->gimp->font_factory); + + if (! gimp_async_set_is_empty (async_set)) + { + g_set_error_literal (error, GIMP_ERROR, GIMP_FAILED, + _("Fonts are still loading")); + + return FALSE; + } + + tool->display = display; + + text_tool->widget = widget = gimp_tool_rectangle_new (shell); + + g_object_set (widget, + "force-narrow-mode", TRUE, + "status-title", _("Text box: "), + NULL); + + gimp_draw_tool_set_widget (GIMP_DRAW_TOOL (tool), widget); + + g_signal_connect (widget, "response", + G_CALLBACK (gimp_text_tool_rectangle_response), + text_tool); + g_signal_connect (widget, "change-complete", + G_CALLBACK (gimp_text_tool_rectangle_change_complete), + text_tool); + + gimp_draw_tool_start (GIMP_DRAW_TOOL (tool), display); + + if (layer) + { + gimp_text_tool_frame_item (text_tool); + gimp_text_tool_editor_start (text_tool); + gimp_text_tool_editor_position (text_tool); + } + + return TRUE; +} + +static void +gimp_text_tool_halt (GimpTextTool *text_tool) +{ + GimpTool *tool = GIMP_TOOL (text_tool); + + gimp_text_tool_editor_halt (text_tool); + gimp_text_tool_clear_layout (text_tool); + gimp_text_tool_set_drawable (text_tool, NULL, FALSE); + + if (gimp_draw_tool_is_active (GIMP_DRAW_TOOL (tool))) + gimp_draw_tool_stop (GIMP_DRAW_TOOL (tool)); + + gimp_draw_tool_set_widget (GIMP_DRAW_TOOL (tool), NULL); + g_clear_object (&text_tool->widget); + + tool->display = NULL; + tool->drawable = NULL; +} + +static void +gimp_text_tool_frame_item (GimpTextTool *text_tool) +{ + g_return_if_fail (GIMP_IS_LAYER (text_tool->layer)); + + text_tool->handle_rectangle_change_complete = FALSE; + + gimp_tool_rectangle_frame_item (GIMP_TOOL_RECTANGLE (text_tool->widget), + GIMP_ITEM (text_tool->layer)); + + text_tool->handle_rectangle_change_complete = TRUE; +} + +static void +gimp_text_tool_rectangle_response (GimpToolRectangle *rectangle, + gint response_id, + GimpTextTool *text_tool) +{ + GimpTool *tool = GIMP_TOOL (text_tool); + + /* this happens when a newly created rectangle gets canceled, + * we have to shut down the tool + */ + if (response_id == GIMP_TOOL_WIDGET_RESPONSE_CANCEL) + gimp_tool_control (tool, GIMP_TOOL_ACTION_HALT, tool->display); +} + +static void +gimp_text_tool_rectangle_change_complete (GimpToolRectangle *rectangle, + GimpTextTool *text_tool) +{ + gimp_text_tool_editor_position (text_tool); + + if (text_tool->handle_rectangle_change_complete) + { + GimpItem *item = GIMP_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) != gimp_item_get_width (item) || + (y2 - y1) != gimp_item_get_height (item)) + { + GimpUnit box_unit = text_tool->proxy->box_unit; + gdouble xres, yres; + gboolean push_undo = TRUE; + GimpUndo *undo; + + gimp_image_get_resolution (text_tool->image, &xres, &yres); + + g_object_set (text_tool->proxy, + "box-mode", GIMP_TEXT_BOX_FIXED, + "box-width", gimp_pixels_to_units (x2 - x1, + box_unit, xres), + "box-height", gimp_pixels_to_units (y2 - y1, + box_unit, yres), + NULL); + + undo = gimp_image_undo_can_compress (text_tool->image, + GIMP_TYPE_UNDO_STACK, + GIMP_UNDO_GROUP_TEXT); + + if (undo && + gimp_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) + { + gimp_image_undo_group_start (text_tool->image, GIMP_UNDO_GROUP_TEXT, + _("Reshape Text Layer")); + + undo = gimp_image_undo_can_compress (text_tool->image, GIMP_TYPE_UNDO_STACK, + GIMP_UNDO_GROUP_TEXT); + + if (undo) + g_object_set_data (G_OBJECT (undo), "reshape-text-layer", + (gpointer) item); + } + + gimp_text_tool_block_drawing (text_tool); + + gimp_item_translate (item, + x1 - gimp_item_get_offset_x (item), + y1 - gimp_item_get_offset_y (item), + push_undo); + gimp_text_tool_apply (text_tool, push_undo); + + gimp_text_tool_unblock_drawing (text_tool); + + if (push_undo) + gimp_image_undo_group_end (text_tool->image); + } + else if (x1 != gimp_item_get_offset_x (item) || + y1 != gimp_item_get_offset_y (item)) + { + gimp_text_tool_block_drawing (text_tool); + + gimp_text_tool_apply (text_tool, TRUE); + + gimp_item_translate (item, + x1 - gimp_item_get_offset_x (item), + y1 - gimp_item_get_offset_y (item), + TRUE); + + gimp_text_tool_unblock_drawing (text_tool); + + gimp_image_flush (text_tool->image); + } + } +} + +static void +gimp_text_tool_connect (GimpTextTool *text_tool, + GimpTextLayer *layer, + GimpText *text) +{ + GimpTool *tool = GIMP_TOOL (text_tool); + + g_return_if_fail (text == NULL || (layer != NULL && layer->text == text)); + + if (text_tool->text != text) + { + GimpTextOptions *options = GIMP_TEXT_TOOL_GET_OPTIONS (tool); + + g_signal_handlers_block_by_func (text_tool->buffer, + gimp_text_tool_buffer_begin_edit, + text_tool); + g_signal_handlers_block_by_func (text_tool->buffer, + gimp_text_tool_buffer_end_edit, + text_tool); + + if (text_tool->text) + { + g_signal_handlers_disconnect_by_func (text_tool->text, + gimp_text_tool_text_notify, + text_tool); + g_signal_handlers_disconnect_by_func (text_tool->text, + gimp_text_tool_text_changed, + text_tool); + + if (text_tool->pending) + gimp_text_tool_apply (text_tool, TRUE); + + g_clear_object (&text_tool->text); + + g_object_set (text_tool->proxy, + "text", NULL, + "markup", NULL, + NULL); + gimp_text_buffer_set_text (text_tool->buffer, NULL); + + gimp_text_tool_clear_layout (text_tool); + } + + gimp_context_define_property (GIMP_CONTEXT (options), + GIMP_CONTEXT_PROP_FOREGROUND, + text != NULL); + + if (text) + { + if (text->unit != text_tool->proxy->unit) + gimp_size_entry_set_unit (GIMP_SIZE_ENTRY (options->size_entry), + text->unit); + + gimp_config_sync (G_OBJECT (text), G_OBJECT (text_tool->proxy), 0); + + if (text->markup) + gimp_text_buffer_set_markup (text_tool->buffer, text->markup); + else + gimp_text_buffer_set_text (text_tool->buffer, text->text); + + gimp_text_tool_clear_layout (text_tool); + + text_tool->text = g_object_ref (text); + + g_signal_connect (text, "notify", + G_CALLBACK (gimp_text_tool_text_notify), + text_tool); + g_signal_connect (text, "changed", + G_CALLBACK (gimp_text_tool_text_changed), + text_tool); + } + + g_signal_handlers_unblock_by_func (text_tool->buffer, + gimp_text_tool_buffer_end_edit, + text_tool); + g_signal_handlers_unblock_by_func (text_tool->buffer, + gimp_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, + gimp_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 (gimp_item_is_attached (GIMP_ITEM (text_tool->layer))) + gimp_text_tool_remove_empty_text_layer (text_tool); + } + + text_tool->layer = layer; + + if (layer) + { + g_signal_connect_object (text_tool->layer, "notify", + G_CALLBACK (gimp_text_tool_layer_notify), + text_tool, 0); + } + } +} + +static void +gimp_text_tool_layer_notify (GimpTextLayer *layer, + const GParamSpec *pspec, + GimpTextTool *text_tool) +{ + GimpTool *tool = GIMP_TOOL (text_tool); + + if (! strcmp (pspec->name, "modified")) + { + if (layer->modified) + gimp_tool_control (tool, GIMP_TOOL_ACTION_HALT, tool->display); + } + else if (! strcmp (pspec->name, "text")) + { + if (! layer->text) + gimp_tool_control (tool, GIMP_TOOL_ACTION_HALT, tool->display); + } + else if (! strcmp (pspec->name, "offset-x") || + ! strcmp (pspec->name, "offset-y")) + { + if (gimp_item_is_attached (GIMP_ITEM (layer))) + { + gimp_text_tool_block_drawing (text_tool); + + gimp_text_tool_frame_item (text_tool); + + gimp_text_tool_unblock_drawing (text_tool); + } + } +} + +static gboolean +gimp_text_tool_apply_idle (GimpTextTool *text_tool) +{ + text_tool->idle_id = 0; + + gimp_text_tool_apply (text_tool, TRUE); + + gimp_text_tool_unblock_drawing (text_tool); + + return G_SOURCE_REMOVE; +} + +static void +gimp_text_tool_proxy_notify (GimpText *text, + const GParamSpec *pspec, + GimpTextTool *text_tool) +{ + if (! text_tool->text) + return; + + if ((pspec->flags & G_PARAM_READWRITE) == G_PARAM_READWRITE && + pspec->owner_type == GIMP_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) + { + gimp_text_tool_block_drawing (text_tool); + gimp_text_tool_apply (text_tool, TRUE); + gimp_text_tool_unblock_drawing (text_tool); + } + + gimp_text_tool_block_drawing (text_tool); + + list = g_list_append (list, (gpointer) pspec); + gimp_text_tool_apply_list (text_tool, list); + g_list_free (list); + + gimp_text_tool_frame_item (text_tool); + + gimp_image_flush (gimp_item_get_image (GIMP_ITEM (text_tool->layer))); + + gimp_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) + { + gimp_text_tool_block_drawing (text_tool); + + text_tool->idle_id = + g_idle_add_full (G_PRIORITY_LOW, + (GSourceFunc) gimp_text_tool_apply_idle, + text_tool, + NULL); + } + } + } +} + +static void +gimp_text_tool_text_notify (GimpText *text, + const GParamSpec *pspec, + GimpTextTool *text_tool) +{ + g_return_if_fail (text == text_tool->text); + + /* an undo cancels all preedit operations */ + if (text_tool->preedit_active) + gimp_text_tool_abort_im_context (text_tool); + + gimp_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, + gimp_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, + gimp_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, + gimp_text_tool_buffer_begin_edit, + text_tool); + g_signal_handlers_block_by_func (text_tool->buffer, + gimp_text_tool_buffer_end_edit, + text_tool); + + if (text->markup) + gimp_text_buffer_set_markup (text_tool->buffer, text->markup); + else + gimp_text_buffer_set_text (text_tool->buffer, text->text); + + g_signal_handlers_unblock_by_func (text_tool->buffer, + gimp_text_tool_buffer_end_edit, + text_tool); + g_signal_handlers_unblock_by_func (text_tool->buffer, + gimp_text_tool_buffer_begin_edit, + text_tool); + } + + gimp_text_tool_unblock_drawing (text_tool); +} + +static void +gimp_text_tool_text_changed (GimpText *text, + GimpTextTool *text_tool) +{ + gimp_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 + */ + gimp_text_tool_frame_item (text_tool); + + gimp_text_tool_unblock_drawing (text_tool); +} + +static void +gimp_text_tool_fonts_async_set_empty_notify (GimpAsyncSet *async_set, + GParamSpec *pspec, + GimpTextTool *text_tool) +{ + GimpTool *tool = GIMP_TOOL (text_tool); + + if (! gimp_async_set_is_empty (async_set) && tool->display) + gimp_tool_control (tool, GIMP_TOOL_ACTION_HALT, tool->display); +} + +static void +gimp_text_tool_apply_list (GimpTextTool *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, + gimp_text_tool_text_notify, + text_tool); + g_signal_handlers_block_by_func (dest, + gimp_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, + gimp_text_tool_text_notify, + text_tool); + g_signal_handlers_unblock_by_func (dest, + gimp_text_tool_text_changed, + text_tool); +} + +static void +gimp_text_tool_create_layer (GimpTextTool *text_tool, + GimpText *text) +{ + GimpTool *tool = GIMP_TOOL (text_tool); + GimpImage *image = gimp_display_get_image (tool->display); + GimpLayer *layer; + gdouble x1, y1; + gdouble x2, y2; + + gimp_text_tool_block_drawing (text_tool); + + if (text) + { + text = gimp_config_duplicate (GIMP_CONFIG (text)); + } + else + { + gchar *string; + + if (gimp_text_buffer_has_markup (text_tool->buffer)) + { + string = gimp_text_buffer_get_markup (text_tool->buffer); + + g_object_set (text_tool->proxy, + "markup", string, + "box-mode", GIMP_TEXT_BOX_DYNAMIC, + NULL); + } + else + { + string = gimp_text_buffer_get_text (text_tool->buffer); + + g_object_set (text_tool->proxy, + "text", string, + "box-mode", GIMP_TEXT_BOX_DYNAMIC, + NULL); + } + + g_free (string); + + text = gimp_config_duplicate (GIMP_CONFIG (text_tool->proxy)); + } + + layer = gimp_text_layer_new (image, text); + + g_object_unref (text); + + if (! layer) + { + gimp_text_tool_unblock_drawing (text_tool); + return; + } + + gimp_text_tool_connect (text_tool, GIMP_TEXT_LAYER (layer), text); + + gimp_image_undo_group_start (image, GIMP_UNDO_GROUP_TEXT, + _("Add Text Layer")); + + if (gimp_image_get_floating_selection (image)) + { + g_signal_handlers_block_by_func (image, + gimp_text_tool_layer_changed, + text_tool); + + floating_sel_anchor (gimp_image_get_floating_selection (image)); + + g_signal_handlers_unblock_by_func (image, + gimp_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 == GIMP_TEXT_DIRECTION_TTB_RTL || + text_tool->text->base_dir == GIMP_TEXT_DIRECTION_TTB_RTL_UPRIGHT)) + { + x1 -= gimp_item_get_width (GIMP_ITEM (layer)); + } + } + gimp_item_set_offset (GIMP_ITEM (layer), x1, y1); + + gimp_image_add_layer (image, layer, + GIMP_IMAGE_ACTIVE_PARENT, -1, TRUE); + + if (text_tool->text_box_fixed) + { + GimpUnit box_unit = text_tool->proxy->box_unit; + gdouble xres, yres; + + gimp_image_get_resolution (image, &xres, &yres); + + g_object_set (text_tool->proxy, + "box-mode", GIMP_TEXT_BOX_FIXED, + "box-width", gimp_pixels_to_units (x2 - x1, + box_unit, xres), + "box-height", gimp_pixels_to_units (y2 - y1, + box_unit, yres), + NULL); + + gimp_text_tool_apply (text_tool, TRUE); /* unblocks drawing */ + } + else + { + gimp_text_tool_frame_item (text_tool); + } + + gimp_image_undo_group_end (image); + + gimp_image_flush (image); + + gimp_text_tool_set_drawable (text_tool, GIMP_DRAWABLE (layer), FALSE); + + gimp_text_tool_unblock_drawing (text_tool); +} + +#define RESPONSE_NEW 1 + +static void +gimp_text_tool_confirm_response (GtkWidget *widget, + gint response_id, + GimpTextTool *text_tool) +{ + GimpTextLayer *layer = text_tool->layer; + + gtk_widget_destroy (widget); + + if (layer && layer->text) + { + switch (response_id) + { + case RESPONSE_NEW: + gimp_text_tool_create_layer (text_tool, layer->text); + break; + + case GTK_RESPONSE_ACCEPT: + gimp_text_tool_connect (text_tool, layer, layer->text); + + /* cause the text layer to be rerendered */ + g_object_notify (G_OBJECT (text_tool->proxy), "markup"); + + gimp_text_tool_editor_start (text_tool); + break; + + default: + break; + } + } +} + +static void +gimp_text_tool_confirm_dialog (GimpTextTool *text_tool) +{ + GimpTool *tool = GIMP_TOOL (text_tool); + GimpDisplayShell *shell = gimp_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 = gimp_viewable_dialog_new (GIMP_VIEWABLE (text_tool->layer), + GIMP_CONTEXT (gimp_tool_get_options (tool)), + _("Confirm Text Editing"), + "gimp-text-tool-confirm", + GIMP_ICON_LAYER_TEXT_LAYER, + _("Confirm Text Editing"), + GTK_WIDGET (shell), + gimp_standard_help_func, NULL, + + _("Create _New Layer"), RESPONSE_NEW, + _("_Cancel"), GTK_RESPONSE_CANCEL, + _("_Edit"), GTK_RESPONSE_ACCEPT, + + NULL); + + gtk_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 (gimp_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 +gimp_text_tool_layer_changed (GimpImage *image, + GimpTextTool *text_tool) +{ + GimpLayer *layer = gimp_image_get_active_layer (image); + + if (layer != GIMP_LAYER (text_tool->layer)) + { + GimpTool *tool = GIMP_TOOL (text_tool); + GimpDisplay *display = tool->display; + + if (display) + { + gimp_tool_control (tool, GIMP_TOOL_ACTION_HALT, display); + + if (gimp_text_tool_set_drawable (text_tool, GIMP_DRAWABLE (layer), + FALSE) && + GIMP_LAYER (text_tool->layer) == layer) + { + GError *error = NULL; + + if (! gimp_text_tool_start (text_tool, display, layer, &error)) + { + gimp_text_tool_set_drawable (text_tool, NULL, FALSE); + + gimp_tool_message_literal (tool, display, error->message); + + g_clear_error (&error); + + return; + } + } + } + } +} + +static void +gimp_text_tool_set_image (GimpTextTool *text_tool, + GimpImage *image) +{ + if (text_tool->image == image) + return; + + if (text_tool->image) + { + g_signal_handlers_disconnect_by_func (text_tool->image, + gimp_text_tool_layer_changed, + text_tool); + + g_object_remove_weak_pointer (G_OBJECT (text_tool->image), + (gpointer) &text_tool->image); + } + + text_tool->image = image; + + if (image) + { + GimpTextOptions *options = GIMP_TEXT_TOOL_GET_OPTIONS (text_tool); + gdouble xres; + gdouble yres; + + g_object_add_weak_pointer (G_OBJECT (text_tool->image), + (gpointer) &text_tool->image); + + g_signal_connect_object (text_tool->image, "active-layer-changed", + G_CALLBACK (gimp_text_tool_layer_changed), + text_tool, 0); + + gimp_image_get_resolution (image, &xres, &yres); + gimp_size_entry_set_resolution (GIMP_SIZE_ENTRY (options->size_entry), 0, + yres, FALSE); + } +} + +static gboolean +gimp_text_tool_set_drawable (GimpTextTool *text_tool, + GimpDrawable *drawable, + gboolean confirm) +{ + GimpImage *image = NULL; + + if (text_tool->confirm_dialog) + gtk_widget_destroy (text_tool->confirm_dialog); + + if (drawable) + image = gimp_item_get_image (GIMP_ITEM (drawable)); + + gimp_text_tool_set_image (text_tool, image); + + if (GIMP_IS_TEXT_LAYER (drawable) && GIMP_TEXT_LAYER (drawable)->text) + { + GimpTextLayer *layer = GIMP_TEXT_LAYER (drawable); + + if (layer == text_tool->layer && layer->text == text_tool->text) + return TRUE; + + if (layer->modified) + { + if (confirm) + { + gimp_text_tool_connect (text_tool, layer, NULL); + gimp_text_tool_confirm_dialog (text_tool); + return TRUE; + } + } + else + { + gimp_text_tool_connect (text_tool, layer, layer->text); + return TRUE; + } + } + + gimp_text_tool_connect (text_tool, NULL, NULL); + + return FALSE; +} + +static void +gimp_text_tool_block_drawing (GimpTextTool *text_tool) +{ + if (text_tool->drawing_blocked == 0) + { + gimp_draw_tool_pause (GIMP_DRAW_TOOL (text_tool)); + + gimp_text_tool_clear_layout (text_tool); + } + + text_tool->drawing_blocked++; +} + +static void +gimp_text_tool_unblock_drawing (GimpTextTool *text_tool) +{ + g_return_if_fail (text_tool->drawing_blocked > 0); + + text_tool->drawing_blocked--; + + if (text_tool->drawing_blocked == 0) + gimp_draw_tool_resume (GIMP_DRAW_TOOL (text_tool)); +} + +static void +gimp_text_tool_buffer_begin_edit (GimpTextBuffer *buffer, + GimpTextTool *text_tool) +{ + gimp_text_tool_block_drawing (text_tool); +} + +static void +gimp_text_tool_buffer_end_edit (GimpTextBuffer *buffer, + GimpTextTool *text_tool) +{ + if (text_tool->text) + { + gchar *string; + + if (gimp_text_buffer_has_markup (buffer)) + { + string = gimp_text_buffer_get_markup (buffer); + + g_object_set (text_tool->proxy, + "markup", string, + NULL); + } + else + { + string = gimp_text_buffer_get_text (buffer); + + g_object_set (text_tool->proxy, + "text", string, + NULL); + } + + g_free (string); + } + else + { + gimp_text_tool_create_layer (text_tool, NULL); + } + + gimp_text_tool_unblock_drawing (text_tool); +} + +static void +gimp_text_tool_buffer_color_applied (GimpTextBuffer *buffer, + const GimpRGB *color, + GimpTextTool *text_tool) +{ + gimp_palettes_add_color_history (GIMP_TOOL (text_tool)->tool_info->gimp, + color); +} + + +/* public functions */ + +void +gimp_text_tool_clear_layout (GimpTextTool *text_tool) +{ + g_clear_object (&text_tool->layout); +} + +gboolean +gimp_text_tool_ensure_layout (GimpTextTool *text_tool) +{ + if (! text_tool->layout && text_tool->text) + { + GimpImage *image = gimp_item_get_image (GIMP_ITEM (text_tool->layer)); + gdouble xres; + gdouble yres; + GError *error = NULL; + + gimp_image_get_resolution (image, &xres, &yres); + + text_tool->layout = gimp_text_layout_new (text_tool->layer->text, + xres, yres, &error); + if (error) + { + gimp_message_literal (image->gimp, NULL, GIMP_MESSAGE_ERROR, error->message); + g_error_free (error); + } + } + + return text_tool->layout != NULL; +} + +void +gimp_text_tool_apply (GimpTextTool *text_tool, + gboolean push_undo) +{ + const GParamSpec *pspec = NULL; + GimpImage *image; + GimpTextLayer *layer; + GList *list; + gboolean undo_group = FALSE; + + if (text_tool->idle_id) + { + g_source_remove (text_tool->idle_id); + text_tool->idle_id = 0; + + gimp_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 = gimp_item_get_image (GIMP_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) + { + GimpUndo *undo = gimp_image_undo_can_compress (image, GIMP_TYPE_TEXT_UNDO, + GIMP_UNDO_TEXT_LAYER); + + if (undo && GIMP_ITEM_UNDO (undo)->item == GIMP_ITEM (layer)) + { + GimpTextUndo *text_undo = GIMP_TEXT_UNDO (undo); + + if (text_undo->pspec == pspec) + { + if (gimp_undo_get_age (undo) < TEXT_UNDO_TIMEOUT) + { + GimpTool *tool = GIMP_TOOL (text_tool); + GimpContext *context; + + context = GIMP_CONTEXT (gimp_tool_get_options (tool)); + + push_undo = FALSE; + gimp_undo_reset_age (undo); + gimp_undo_refresh_preview (undo, context); + } + } + } + } + + if (push_undo) + { + if (layer->modified) + { + undo_group = TRUE; + gimp_image_undo_group_start (image, GIMP_UNDO_GROUP_TEXT, NULL); + + gimp_image_undo_push_text_layer_modified (image, NULL, layer); + + /* see comment in gimp_text_layer_set() */ + gimp_image_undo_push_drawable_mod (image, NULL, + GIMP_DRAWABLE (layer), TRUE); + } + + gimp_image_undo_push_text_layer (image, NULL, layer, pspec); + } + + gimp_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) + gimp_image_undo_group_end (image); + } + + gimp_text_tool_frame_item (text_tool); + + gimp_image_flush (image); +} + +gboolean +gimp_text_tool_set_layer (GimpTextTool *text_tool, + GimpLayer *layer) +{ + g_return_val_if_fail (GIMP_IS_TEXT_TOOL (text_tool), FALSE); + g_return_val_if_fail (layer == NULL || GIMP_IS_LAYER (layer), FALSE); + + if (layer == GIMP_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 (gimp_text_tool_set_drawable (text_tool, GIMP_DRAWABLE (layer), TRUE)) + { + GimpTool *tool = GIMP_TOOL (text_tool); + GimpItem *item = GIMP_ITEM (layer); + GimpContext *context; + GimpDisplay *display; + + context = gimp_get_user_context (tool->tool_info->gimp); + display = gimp_context_get_display (context); + + if (! display || + gimp_display_get_image (display) != gimp_item_get_image (item)) + { + GList *list; + + display = NULL; + + for (list = gimp_get_display_iter (tool->tool_info->gimp); + list; + list = g_list_next (list)) + { + display = list->data; + + if (gimp_display_get_image (display) == gimp_item_get_image (item)) + { + gimp_context_set_display (context, display); + break; + } + + display = NULL; + } + } + + if (tool->display) + gimp_tool_control (tool, GIMP_TOOL_ACTION_HALT, tool->display); + + if (display) + { + GError *error = NULL; + + if (! gimp_text_tool_start (text_tool, display, layer, &error)) + { + gimp_text_tool_set_drawable (text_tool, NULL, FALSE); + + gimp_tool_message_literal (tool, display, error->message); + + g_clear_error (&error); + + return FALSE; + } + + tool->drawable = GIMP_DRAWABLE (layer); + } + } + + return TRUE; +} + +gboolean +gimp_text_tool_get_has_text_selection (GimpTextTool *text_tool) +{ + GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer); + + return gtk_text_buffer_get_has_selection (buffer); +} + +void +gimp_text_tool_delete_selection (GimpTextTool *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 +gimp_text_tool_cut_clipboard (GimpTextTool *text_tool) +{ + GimpDisplayShell *shell; + GtkClipboard *clipboard; + + g_return_if_fail (GIMP_IS_TEXT_TOOL (text_tool)); + + shell = gimp_display_get_shell (GIMP_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 +gimp_text_tool_copy_clipboard (GimpTextTool *text_tool) +{ + GimpDisplayShell *shell; + GtkClipboard *clipboard; + + g_return_if_fail (GIMP_IS_TEXT_TOOL (text_tool)); + + shell = gimp_display_get_shell (GIMP_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, + gimp_text_tool_buffer_begin_edit, + text_tool); + g_signal_handlers_block_by_func (text_tool->buffer, + gimp_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, + gimp_text_tool_buffer_end_edit, + text_tool); + g_signal_handlers_unblock_by_func (text_tool->buffer, + gimp_text_tool_buffer_begin_edit, + text_tool); +} + +void +gimp_text_tool_paste_clipboard (GimpTextTool *text_tool) +{ + GimpDisplayShell *shell; + GtkClipboard *clipboard; + + g_return_if_fail (GIMP_IS_TEXT_TOOL (text_tool)); + + shell = gimp_display_get_shell (GIMP_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 +gimp_text_tool_create_vectors (GimpTextTool *text_tool) +{ + GimpVectors *vectors; + + g_return_if_fail (GIMP_IS_TEXT_TOOL (text_tool)); + + if (! text_tool->text || ! text_tool->image) + return; + + vectors = gimp_text_vectors_new (text_tool->image, text_tool->text); + + if (text_tool->layer) + { + gint x, y; + + gimp_item_get_offset (GIMP_ITEM (text_tool->layer), &x, &y); + gimp_item_translate (GIMP_ITEM (vectors), x, y, FALSE); + } + + gimp_image_add_vectors (text_tool->image, vectors, + GIMP_IMAGE_ACTIVE_PARENT, -1, TRUE); + + gimp_image_flush (text_tool->image); +} + +void +gimp_text_tool_create_vectors_warped (GimpTextTool *text_tool) +{ + GimpVectors *vectors0; + GimpVectors *vectors; + gdouble box_width; + gdouble box_height; + GimpTextDirection dir; + gdouble offset = 0.0; + + g_return_if_fail (GIMP_IS_TEXT_TOOL (text_tool)); + + if (! text_tool->text || ! text_tool->image || ! text_tool->layer) + return; + + box_width = gimp_item_get_width (GIMP_ITEM (text_tool->layer)); + box_height = gimp_item_get_height (GIMP_ITEM (text_tool->layer)); + + vectors0 = gimp_image_get_active_vectors (text_tool->image); + if (! vectors0) + return; + + vectors = gimp_text_vectors_new (text_tool->image, text_tool->text); + + offset = 0; + dir = gimp_text_tool_get_direction (text_tool); + switch (dir) + { + case GIMP_TEXT_DIRECTION_LTR: + case GIMP_TEXT_DIRECTION_RTL: + offset = 0.5 * box_height; + break; + case GIMP_TEXT_DIRECTION_TTB_RTL: + case GIMP_TEXT_DIRECTION_TTB_RTL_UPRIGHT: + case GIMP_TEXT_DIRECTION_TTB_LTR: + case GIMP_TEXT_DIRECTION_TTB_LTR_UPRIGHT: + { + GimpStroke *stroke = NULL; + + while ((stroke = gimp_vectors_stroke_get_next (vectors, stroke))) + { + gimp_stroke_rotate (stroke, 0, 0, 270); + gimp_stroke_translate (stroke, 0, box_width); + } + } + offset = 0.5 * box_width; + break; + } + + gimp_vectors_warp_vectors (vectors0, vectors, offset); + + gimp_item_set_visible (GIMP_ITEM (vectors), TRUE, FALSE); + + gimp_image_add_vectors (text_tool->image, vectors, + GIMP_IMAGE_ACTIVE_PARENT, -1, TRUE); + + gimp_image_flush (text_tool->image); +} + +GimpTextDirection +gimp_text_tool_get_direction (GimpTextTool *text_tool) +{ + GimpTextOptions *options = GIMP_TEXT_TOOL_GET_OPTIONS (text_tool); + return options->base_dir; +} |