/* 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 <gdk/gdkkeysyms.h>

#include "libgimpwidgets/gimpwidgets.h"

#include "tools-types.h"

#include "core/gimp.h"
#include "core/gimpdatafactory.h"
#include "core/gimpimage.h"
#include "core/gimptoolinfo.h"

#include "menus/menus.h"

#include "text/gimptext.h"
#include "text/gimptextlayout.h"

#include "widgets/gimpdialogfactory.h"
#include "widgets/gimpdockcontainer.h"
#include "widgets/gimpoverlaybox.h"
#include "widgets/gimpoverlayframe.h"
#include "widgets/gimptextbuffer.h"
#include "widgets/gimptexteditor.h"
#include "widgets/gimptextproxy.h"
#include "widgets/gimptextstyleeditor.h"
#include "widgets/gimpwidgets-utils.h"

#include "display/gimpdisplay.h"
#include "display/gimpdisplayshell.h"
#include "display/gimpdisplayshell-transform.h"

#include "gimptextoptions.h"
#include "gimptexttool.h"
#include "gimptexttool-editor.h"

#include "gimp-log.h"
#include "gimp-intl.h"


/*  local function prototypes  */

static void     gimp_text_tool_ensure_proxy       (GimpTextTool    *text_tool);
static void     gimp_text_tool_move_cursor        (GimpTextTool    *text_tool,
                                                   GtkMovementStep  step,
                                                   gint             count,
                                                   gboolean         extend_selection);
static void     gimp_text_tool_insert_at_cursor   (GimpTextTool    *text_tool,
                                                   const gchar     *str);
static void     gimp_text_tool_delete_from_cursor (GimpTextTool    *text_tool,
                                                   GtkDeleteType    type,
                                                   gint             count);
static void     gimp_text_tool_backspace          (GimpTextTool    *text_tool);
static void     gimp_text_tool_toggle_overwrite   (GimpTextTool    *text_tool);
static void     gimp_text_tool_select_all         (GimpTextTool    *text_tool,
                                                   gboolean         select);
static void     gimp_text_tool_change_size        (GimpTextTool    *text_tool,
                                                   gdouble          amount);
static void     gimp_text_tool_change_baseline    (GimpTextTool    *text_tool,
                                                   gdouble          amount);
static void     gimp_text_tool_change_kerning     (GimpTextTool    *text_tool,
                                                   gdouble          amount);

static void     gimp_text_tool_options_notify     (GimpTextOptions *options,
                                                   GParamSpec      *pspec,
                                                   GimpTextTool    *text_tool);
static void     gimp_text_tool_editor_dialog      (GimpTextTool    *text_tool);
static void     gimp_text_tool_editor_destroy     (GtkWidget       *dialog,
                                                   GimpTextTool    *text_tool);
static void     gimp_text_tool_enter_text         (GimpTextTool    *text_tool,
                                                   const gchar     *str);
static void     gimp_text_tool_xy_to_iter         (GimpTextTool    *text_tool,
                                                   gdouble          x,
                                                   gdouble          y,
                                                   GtkTextIter     *iter);

static void     gimp_text_tool_im_preedit_start   (GtkIMContext    *context,
                                                   GimpTextTool    *text_tool);
static void     gimp_text_tool_im_preedit_end     (GtkIMContext    *context,
                                                   GimpTextTool    *text_tool);
static void     gimp_text_tool_im_preedit_changed (GtkIMContext    *context,
                                                   GimpTextTool    *text_tool);
static void     gimp_text_tool_im_commit          (GtkIMContext    *context,
                                                   const gchar     *str,
                                                   GimpTextTool    *text_tool);
static gboolean gimp_text_tool_im_retrieve_surrounding
                                                  (GtkIMContext    *context,
                                                   GimpTextTool    *text_tool);
static gboolean gimp_text_tool_im_delete_surrounding
                                                  (GtkIMContext    *context,
                                                   gint             offset,
                                                   gint             n_chars,
                                                   GimpTextTool    *text_tool);

static void     gimp_text_tool_im_delete_preedit  (GimpTextTool    *text_tool);

static void     gimp_text_tool_editor_copy_selection_to_clipboard
                                                  (GimpTextTool    *text_tool);

static void   gimp_text_tool_fix_position         (GimpTextTool    *text_tool,
                                                   gdouble         *x,
                                                   gdouble         *y);

static void   gimp_text_tool_convert_gdkkeyevent  (GimpTextTool    *text_tool,
                                                   GdkEventKey     *kevent);


/*  public functions  */

void
gimp_text_tool_editor_init (GimpTextTool *text_tool)
{
  text_tool->im_context     = gtk_im_multicontext_new ();
  text_tool->needs_im_reset = FALSE;

  text_tool->preedit_string = NULL;
  text_tool->preedit_cursor = 0;
  text_tool->overwrite_mode = FALSE;
  text_tool->x_pos          = -1;

  g_signal_connect (text_tool->im_context, "preedit-start",
                    G_CALLBACK (gimp_text_tool_im_preedit_start),
                    text_tool);
  g_signal_connect (text_tool->im_context, "preedit-end",
                    G_CALLBACK (gimp_text_tool_im_preedit_end),
                    text_tool);
  g_signal_connect (text_tool->im_context, "preedit-changed",
                    G_CALLBACK (gimp_text_tool_im_preedit_changed),
                    text_tool);
  g_signal_connect (text_tool->im_context, "commit",
                    G_CALLBACK (gimp_text_tool_im_commit),
                    text_tool);
  g_signal_connect (text_tool->im_context, "retrieve-surrounding",
                    G_CALLBACK (gimp_text_tool_im_retrieve_surrounding),
                    text_tool);
  g_signal_connect (text_tool->im_context, "delete-surrounding",
                    G_CALLBACK (gimp_text_tool_im_delete_surrounding),
                    text_tool);
}

void
gimp_text_tool_editor_finalize (GimpTextTool *text_tool)
{
  if (text_tool->im_context)
    {
      g_object_unref (text_tool->im_context);
      text_tool->im_context = NULL;
    }
}

void
gimp_text_tool_editor_start (GimpTextTool *text_tool)
{
  GimpTool         *tool    = GIMP_TOOL (text_tool);
  GimpTextOptions  *options = GIMP_TEXT_TOOL_GET_OPTIONS (text_tool);
  GimpDisplayShell *shell   = gimp_display_get_shell (tool->display);

  gtk_im_context_set_client_window (text_tool->im_context,
                                    gtk_widget_get_window (shell->canvas));

  text_tool->needs_im_reset = TRUE;
  gimp_text_tool_reset_im_context (text_tool);

  gtk_im_context_focus_in (text_tool->im_context);

  if (options->use_editor)
    gimp_text_tool_editor_dialog (text_tool);

  g_signal_connect (options, "notify::use-editor",
                    G_CALLBACK (gimp_text_tool_options_notify),
                    text_tool);
  g_signal_connect (options, "notify::show-on-canvas",
                    G_CALLBACK (gimp_text_tool_options_notify),
                    text_tool);

  if (! text_tool->style_overlay)
    {
      Gimp          *gimp = GIMP_CONTEXT (options)->gimp;
      GimpContainer *fonts;
      gdouble        xres = 1.0;
      gdouble        yres = 1.0;

      text_tool->style_overlay = gimp_overlay_frame_new ();
      gtk_container_set_border_width (GTK_CONTAINER (text_tool->style_overlay),
                                      4);
      gimp_display_shell_add_overlay (shell,
                                      text_tool->style_overlay,
                                      0, 0,
                                      GIMP_HANDLE_ANCHOR_CENTER, 0, 0);
      gimp_overlay_box_set_child_opacity (GIMP_OVERLAY_BOX (shell->canvas),
                                          text_tool->style_overlay, 0.85);

      if (text_tool->image)
        gimp_image_get_resolution (text_tool->image, &xres, &yres);

      fonts = gimp_data_factory_get_container (gimp->font_factory);

      text_tool->style_editor = gimp_text_style_editor_new (gimp,
                                                            text_tool->proxy,
                                                            text_tool->buffer,
                                                            fonts,
                                                            xres, yres);
      gtk_container_add (GTK_CONTAINER (text_tool->style_overlay),
                         text_tool->style_editor);

      if (options->show_on_canvas)
        gtk_widget_show (text_tool->style_editor);
    }

  gimp_text_tool_editor_position (text_tool);
  if (options->show_on_canvas)
    gtk_widget_show (text_tool->style_overlay);
}

void
gimp_text_tool_editor_position (GimpTextTool *text_tool)
{
  if (text_tool->style_overlay)
    {
      GimpTool         *tool    = GIMP_TOOL (text_tool);
      GimpDisplayShell *shell   = gimp_display_get_shell (tool->display);
      GtkRequisition    requisition;
      gdouble           x, y;

      gtk_widget_get_preferred_size (text_tool->style_overlay,
                                     &requisition, NULL);

      g_object_get (text_tool->widget,
                    "x1", &x,
                    "y1", &y,
                    NULL);

      gimp_display_shell_move_overlay (shell,
                                       text_tool->style_overlay,
                                       x, y,
                                       GIMP_HANDLE_ANCHOR_SOUTH_WEST, 4, 12);

      if (text_tool->image)
        {
          gdouble xres, yres;

          gimp_image_get_resolution (text_tool->image, &xres, &yres);

          g_object_set (text_tool->style_editor,
                        "resolution-x", xres,
                        "resolution-y", yres,
                        NULL);
        }
    }
}

void
gimp_text_tool_editor_halt (GimpTextTool *text_tool)
{
  GimpTextOptions *options = GIMP_TEXT_TOOL_GET_OPTIONS (text_tool);

  if (text_tool->style_overlay)
    {
      gtk_widget_destroy (text_tool->style_overlay);
      text_tool->style_overlay = NULL;
      text_tool->style_editor  = NULL;
    }

  g_signal_handlers_disconnect_by_func (options,
                                        gimp_text_tool_options_notify,
                                        text_tool);

  if (text_tool->editor_dialog)
    {
      g_signal_handlers_disconnect_by_func (text_tool->editor_dialog,
                                            gimp_text_tool_editor_destroy,
                                            text_tool);
      gtk_widget_destroy (text_tool->editor_dialog);
    }

  if (text_tool->proxy_text_view)
    {
      gtk_widget_destroy (text_tool->offscreen_window);
      text_tool->offscreen_window = NULL;
      text_tool->proxy_text_view = NULL;
    }

  text_tool->needs_im_reset = TRUE;
  gimp_text_tool_reset_im_context (text_tool);

  gtk_im_context_focus_out (text_tool->im_context);

  gtk_im_context_set_client_window (text_tool->im_context, NULL);
}

void
gimp_text_tool_editor_button_press (GimpTextTool        *text_tool,
                                    gdouble              x,
                                    gdouble              y,
                                    GimpButtonPressType  press_type)
{
  GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer);
  GtkTextIter    cursor;
  GtkTextIter    selection;

  gimp_text_tool_xy_to_iter (text_tool, x, y, &cursor);

  selection = cursor;

  text_tool->select_start_iter = cursor;
  text_tool->select_words      = FALSE;
  text_tool->select_lines      = FALSE;

  switch (press_type)
    {
      GtkTextIter start, end;

    case GIMP_BUTTON_PRESS_NORMAL:
      if (gtk_text_buffer_get_selection_bounds (buffer, &start, &end) ||
          gtk_text_iter_compare (&start, &cursor))
        gtk_text_buffer_place_cursor (buffer, &cursor);
      break;

    case GIMP_BUTTON_PRESS_DOUBLE:
      text_tool->select_words = TRUE;

      if (! gtk_text_iter_starts_word (&cursor))
        gtk_text_iter_backward_visible_word_starts (&cursor, 1);

      if (! gtk_text_iter_ends_word (&selection) &&
          ! gtk_text_iter_forward_visible_word_ends (&selection, 1))
        gtk_text_iter_forward_to_line_end (&selection);

      gtk_text_buffer_select_range (buffer, &cursor, &selection);
      break;

    case GIMP_BUTTON_PRESS_TRIPLE:
      text_tool->select_lines = TRUE;

      gtk_text_iter_set_line_offset (&cursor, 0);
      gtk_text_iter_forward_to_line_end (&selection);

      gtk_text_buffer_select_range (buffer, &cursor, &selection);
      break;
    }
}

void
gimp_text_tool_editor_button_release (GimpTextTool *text_tool)
{
  gimp_text_tool_editor_copy_selection_to_clipboard (text_tool);
}

void
gimp_text_tool_editor_motion (GimpTextTool *text_tool,
                              gdouble       x,
                              gdouble       y)
{
  GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer);
  GtkTextIter    old_cursor;
  GtkTextIter    old_selection;
  GtkTextIter    cursor;
  GtkTextIter    selection;

  gtk_text_buffer_get_iter_at_mark (buffer, &old_cursor,
                                    gtk_text_buffer_get_insert (buffer));
  gtk_text_buffer_get_iter_at_mark (buffer, &old_selection,
                                    gtk_text_buffer_get_selection_bound (buffer));

  gimp_text_tool_xy_to_iter (text_tool, x, y, &cursor);
  selection = text_tool->select_start_iter;

  if (text_tool->select_words ||
      text_tool->select_lines)
    {
      GtkTextIter start;
      GtkTextIter end;

      if (gtk_text_iter_compare (&cursor, &selection) < 0)
        {
          start = cursor;
          end   = selection;
        }
      else
        {
          start = selection;
          end   = cursor;
        }

      if (text_tool->select_words)
        {
          if (! gtk_text_iter_starts_word (&start))
            gtk_text_iter_backward_visible_word_starts (&start, 1);

          if (! gtk_text_iter_ends_word (&end) &&
              ! gtk_text_iter_forward_visible_word_ends (&end, 1))
            gtk_text_iter_forward_to_line_end (&end);
        }
      else if (text_tool->select_lines)
        {
          gtk_text_iter_set_line_offset (&start, 0);
          gtk_text_iter_forward_to_line_end (&end);
        }

      if (gtk_text_iter_compare (&cursor, &selection) < 0)
        {
          cursor    = start;
          selection = end;
        }
      else
        {
          selection = start;
          cursor    = end;
        }
    }

  if (! gtk_text_iter_equal (&cursor,    &old_cursor) ||
      ! gtk_text_iter_equal (&selection, &old_selection))
    {
      gimp_draw_tool_pause (GIMP_DRAW_TOOL (text_tool));

      gtk_text_buffer_select_range (buffer, &cursor, &selection);

      gimp_draw_tool_resume (GIMP_DRAW_TOOL (text_tool));
    }
}

gboolean
gimp_text_tool_editor_key_press (GimpTextTool *text_tool,
                                 GdkEventKey  *kevent)
{
  GimpTool         *tool   = GIMP_TOOL (text_tool);
  GimpDisplayShell *shell  = gimp_display_get_shell (tool->display);
  GtkTextBuffer    *buffer = GTK_TEXT_BUFFER (text_tool->buffer);
  GtkTextIter       cursor;
  GtkTextIter       selection;
  gboolean          retval = TRUE;

  if (! gtk_widget_has_focus (shell->canvas))
    {
      /*  The focus is in the floating style editor, and the event
       *  was not handled there, focus the canvas.
       */
      switch (kevent->keyval)
        {
        case GDK_KEY_Tab:
        case GDK_KEY_KP_Tab:
        case GDK_KEY_ISO_Left_Tab:
        case GDK_KEY_Escape:
          gtk_widget_grab_focus (shell->canvas);
          return TRUE;

        default:
          break;
        }
    }

  if (gtk_im_context_filter_keypress (text_tool->im_context, kevent))
    {
      text_tool->needs_im_reset = TRUE;
      text_tool->x_pos          = -1;

      return TRUE;
    }

  gimp_text_tool_convert_gdkkeyevent (text_tool, kevent);

  gimp_text_tool_ensure_proxy (text_tool);

  if (gtk_bindings_activate_event (G_OBJECT (text_tool->proxy_text_view),
                                   kevent))
    {
      GIMP_LOG (TEXT_EDITING, "binding handled event");

      return TRUE;
    }

  gtk_text_buffer_get_iter_at_mark (buffer, &cursor,
                                    gtk_text_buffer_get_insert (buffer));
  gtk_text_buffer_get_iter_at_mark (buffer, &selection,
                                    gtk_text_buffer_get_selection_bound (buffer));

  switch (kevent->keyval)
    {
    case GDK_KEY_Return:
    case GDK_KEY_KP_Enter:
    case GDK_KEY_ISO_Enter:
      gimp_text_tool_reset_im_context (text_tool);
      gimp_text_tool_enter_text (text_tool, "\n");
      break;

    case GDK_KEY_Tab:
    case GDK_KEY_KP_Tab:
    case GDK_KEY_ISO_Left_Tab:
      gimp_text_tool_reset_im_context (text_tool);
      gimp_text_tool_enter_text (text_tool, "\t");
      break;

    case GDK_KEY_Escape:
      gimp_tool_control (GIMP_TOOL (text_tool), GIMP_TOOL_ACTION_HALT,
                         GIMP_TOOL (text_tool)->display);
      break;

    default:
      retval = FALSE;
    }

  text_tool->x_pos = -1;

  return retval;
}

gboolean
gimp_text_tool_editor_key_release (GimpTextTool *text_tool,
                                   GdkEventKey  *kevent)
{
  if (gtk_im_context_filter_keypress (text_tool->im_context, kevent))
    {
      text_tool->needs_im_reset = TRUE;

      return TRUE;
    }

  gimp_text_tool_convert_gdkkeyevent (text_tool, kevent);

  gimp_text_tool_ensure_proxy (text_tool);

  if (gtk_bindings_activate_event (G_OBJECT (text_tool->proxy_text_view),
                                   kevent))
    {
      GIMP_LOG (TEXT_EDITING, "binding handled event");

      return TRUE;
    }

  return FALSE;
}

void
gimp_text_tool_reset_im_context (GimpTextTool *text_tool)
{
  if (text_tool->needs_im_reset)
    {
      text_tool->needs_im_reset = FALSE;
      gtk_im_context_reset (text_tool->im_context);
    }
}

void
gimp_text_tool_abort_im_context (GimpTextTool *text_tool)
{
  GimpTool         *tool  = GIMP_TOOL (text_tool);
  GimpDisplayShell *shell = gimp_display_get_shell (tool->display);

  text_tool->needs_im_reset = TRUE;
  gimp_text_tool_reset_im_context (text_tool);

  /* Making sure preedit text is removed. */
  gimp_text_tool_im_delete_preedit (text_tool);

  /* the following lines seem to be the only way of really getting
   * rid of any ongoing preedit state, please somebody tell me
   * a clean way... mitch
   */

  gtk_im_context_focus_out (text_tool->im_context);
  gtk_im_context_set_client_window (text_tool->im_context, NULL);

  g_object_unref (text_tool->im_context);
  text_tool->im_context = gtk_im_multicontext_new ();
  gtk_im_context_set_client_window (text_tool->im_context,
                                    gtk_widget_get_window (shell->canvas));
  gtk_im_context_focus_in (text_tool->im_context);
  g_signal_connect (text_tool->im_context, "preedit-start",
                    G_CALLBACK (gimp_text_tool_im_preedit_start),
                    text_tool);
  g_signal_connect (text_tool->im_context, "preedit-end",
                    G_CALLBACK (gimp_text_tool_im_preedit_end),
                    text_tool);
  g_signal_connect (text_tool->im_context, "preedit-changed",
                    G_CALLBACK (gimp_text_tool_im_preedit_changed),
                    text_tool);
  g_signal_connect (text_tool->im_context, "commit",
                    G_CALLBACK (gimp_text_tool_im_commit),
                    text_tool);
  g_signal_connect (text_tool->im_context, "retrieve-surrounding",
                    G_CALLBACK (gimp_text_tool_im_retrieve_surrounding),
                    text_tool);
  g_signal_connect (text_tool->im_context, "delete-surrounding",
                    G_CALLBACK (gimp_text_tool_im_delete_surrounding),
                    text_tool);
}

void
gimp_text_tool_editor_get_cursor_rect (GimpTextTool   *text_tool,
                                       gboolean        overwrite,
                                       PangoRectangle *cursor_rect)
{
  GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer);
  PangoLayout   *layout;
  PangoContext  *context;
  gint           offset_x;
  gint           offset_y;
  GtkTextIter    cursor;
  gint           cursor_index;

  g_return_if_fail (GIMP_IS_TEXT_TOOL (text_tool));
  g_return_if_fail (cursor_rect != NULL);

  gtk_text_buffer_get_iter_at_mark (buffer, &cursor,
                                    gtk_text_buffer_get_insert (buffer));
  cursor_index = gimp_text_buffer_get_iter_index (text_tool->buffer, &cursor,
                                                  TRUE);

  gimp_text_tool_ensure_layout (text_tool);

  layout = gimp_text_layout_get_pango_layout (text_tool->layout);

  context = pango_layout_get_context (layout);

  gimp_text_layout_get_offsets (text_tool->layout, &offset_x, &offset_y);

  if (overwrite)
    {
      pango_layout_index_to_pos (layout, cursor_index, cursor_rect);

      /* pango_layout_index_to_pos() returns wrong position, if gravity is west
       * and cursor is at end of line. Avoid this behavior. (pango 1.42.1)
       */
      if (pango_context_get_base_gravity (context) == PANGO_GRAVITY_WEST &&
          cursor_rect->width == 0)
        pango_layout_get_cursor_pos (layout, cursor_index, cursor_rect, NULL);
    }
 else
    pango_layout_get_cursor_pos (layout, cursor_index, cursor_rect, NULL);

  gimp_text_layout_transform_rect (text_tool->layout, cursor_rect);

  switch (gimp_text_tool_get_direction (text_tool))
    {
    case GIMP_TEXT_DIRECTION_LTR:
    case GIMP_TEXT_DIRECTION_RTL:
      cursor_rect->x      = PANGO_PIXELS (cursor_rect->x) + offset_x;
      cursor_rect->y      = PANGO_PIXELS (cursor_rect->y) + offset_y;
      cursor_rect->width  = PANGO_PIXELS (cursor_rect->width);
      cursor_rect->height = PANGO_PIXELS (cursor_rect->height);
      break;
    case GIMP_TEXT_DIRECTION_TTB_RTL:
    case GIMP_TEXT_DIRECTION_TTB_RTL_UPRIGHT:
      {
      gint temp, width, height;

      gimp_text_layout_get_size (text_tool->layout, &width, &height);

      temp                = cursor_rect->x;
      cursor_rect->x      = width - PANGO_PIXELS (cursor_rect->y) + offset_x;
      cursor_rect->y      = PANGO_PIXELS (temp) + offset_y;

      temp                = cursor_rect->width;
      cursor_rect->width  = PANGO_PIXELS (cursor_rect->height);
      cursor_rect->height = PANGO_PIXELS (temp);
      }
      break;
    case GIMP_TEXT_DIRECTION_TTB_LTR:
    case GIMP_TEXT_DIRECTION_TTB_LTR_UPRIGHT:
      {
      gint temp, width, height;

      gimp_text_layout_get_size (text_tool->layout, &width, &height);

      temp                = cursor_rect->x;
      cursor_rect->x      = PANGO_PIXELS (cursor_rect->y) + offset_x;
      cursor_rect->y      = height - PANGO_PIXELS (temp) + offset_y;

      temp                = cursor_rect->width;
      cursor_rect->width  = PANGO_PIXELS (cursor_rect->height);
      cursor_rect->height = PANGO_PIXELS (temp);
      }
      break;
   }
}

void
gimp_text_tool_editor_update_im_cursor (GimpTextTool *text_tool)
{
  GimpDisplayShell *shell;
  PangoRectangle    rect = { 0, };
  gdouble           off_x, off_y;

  g_return_if_fail (GIMP_IS_TEXT_TOOL (text_tool));

  shell = gimp_display_get_shell (GIMP_TOOL (text_tool)->display);

  if (text_tool->text)
    gimp_text_tool_editor_get_cursor_rect (text_tool,
                                           text_tool->overwrite_mode,
                                           &rect);

  g_object_get (text_tool->widget,
                "x1", &off_x,
                "y1", &off_y,
                NULL);

  rect.x += off_x;
  rect.y += off_y;

  gimp_display_shell_transform_xy (shell, rect.x, rect.y, &rect.x, &rect.y);

  gtk_im_context_set_cursor_location (text_tool->im_context,
                                      (GdkRectangle *) &rect);
}


/*  private functions  */

static void
gimp_text_tool_ensure_proxy (GimpTextTool *text_tool)
{
  GimpTool         *tool  = GIMP_TOOL (text_tool);
  GimpDisplayShell *shell = gimp_display_get_shell (tool->display);

  if (text_tool->offscreen_window &&
      gtk_widget_get_screen (text_tool->offscreen_window) !=
      gtk_widget_get_screen (GTK_WIDGET (shell)))
    {
      gtk_window_set_screen (GTK_WINDOW (text_tool->offscreen_window),
                             gtk_widget_get_screen (GTK_WIDGET (shell)));
      gtk_window_move (GTK_WINDOW (text_tool->offscreen_window), -200, -200);
    }
  else if (! text_tool->offscreen_window)
    {
      text_tool->offscreen_window = gtk_window_new (GTK_WINDOW_POPUP);
      gtk_window_set_screen (GTK_WINDOW (text_tool->offscreen_window),
                             gtk_widget_get_screen (GTK_WIDGET (shell)));
      gtk_window_move (GTK_WINDOW (text_tool->offscreen_window), -200, -200);
      gtk_widget_show (text_tool->offscreen_window);

      text_tool->proxy_text_view = gimp_text_proxy_new ();
      gtk_container_add (GTK_CONTAINER (text_tool->offscreen_window),
                         text_tool->proxy_text_view);
      gtk_widget_show (text_tool->proxy_text_view);

      g_signal_connect_swapped (text_tool->proxy_text_view, "move-cursor",
                                G_CALLBACK (gimp_text_tool_move_cursor),
                                text_tool);
      g_signal_connect_swapped (text_tool->proxy_text_view, "insert-at-cursor",
                                G_CALLBACK (gimp_text_tool_insert_at_cursor),
                                text_tool);
      g_signal_connect_swapped (text_tool->proxy_text_view, "delete-from-cursor",
                                G_CALLBACK (gimp_text_tool_delete_from_cursor),
                                text_tool);
      g_signal_connect_swapped (text_tool->proxy_text_view, "backspace",
                                G_CALLBACK (gimp_text_tool_backspace),
                                text_tool);
      g_signal_connect_swapped (text_tool->proxy_text_view, "cut-clipboard",
                                G_CALLBACK (gimp_text_tool_cut_clipboard),
                                text_tool);
      g_signal_connect_swapped (text_tool->proxy_text_view, "copy-clipboard",
                                G_CALLBACK (gimp_text_tool_copy_clipboard),
                                text_tool);
      g_signal_connect_swapped (text_tool->proxy_text_view, "paste-clipboard",
                                G_CALLBACK (gimp_text_tool_paste_clipboard),
                                text_tool);
      g_signal_connect_swapped (text_tool->proxy_text_view, "toggle-overwrite",
                                G_CALLBACK (gimp_text_tool_toggle_overwrite),
                                text_tool);
      g_signal_connect_swapped (text_tool->proxy_text_view, "select-all",
                                G_CALLBACK (gimp_text_tool_select_all),
                                text_tool);
      g_signal_connect_swapped (text_tool->proxy_text_view, "change-size",
                                G_CALLBACK (gimp_text_tool_change_size),
                                text_tool);
      g_signal_connect_swapped (text_tool->proxy_text_view, "change-baseline",
                                G_CALLBACK (gimp_text_tool_change_baseline),
                                text_tool);
      g_signal_connect_swapped (text_tool->proxy_text_view, "change-kerning",
                                G_CALLBACK (gimp_text_tool_change_kerning),
                                text_tool);
    }
}

static void
gimp_text_tool_move_cursor (GimpTextTool    *text_tool,
                            GtkMovementStep  step,
                            gint             count,
                            gboolean         extend_selection)
{
  GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer);
  GtkTextIter    cursor;
  GtkTextIter    selection;
  GtkTextIter   *sel_start;
  gboolean       cancel_selection = FALSE;
  gint           x_pos  = -1;

  if (text_tool->pending)
    {
      /* If there are any pending text commits, there would be
       * inconsistencies between the text_tool->buffer and layout.
       * This could result in crashes. See bug 751333.
       * Therefore we apply them first.
       */
      gimp_text_tool_apply (text_tool, TRUE);
    }
  GIMP_LOG (TEXT_EDITING, "%s count = %d, select = %s",
            g_enum_get_value (g_type_class_ref (GTK_TYPE_MOVEMENT_STEP),
                              step)->value_name,
            count,
            extend_selection ? "TRUE" : "FALSE");

  gtk_text_buffer_get_iter_at_mark (buffer, &cursor,
                                    gtk_text_buffer_get_insert (buffer));
  gtk_text_buffer_get_iter_at_mark (buffer, &selection,
                                    gtk_text_buffer_get_selection_bound (buffer));

  if (extend_selection)
    {
      sel_start = &selection;
    }
  else
    {
      /*  when there is a selection, moving the cursor without
       *  extending it should move the cursor to the end of the
       *  selection that is in moving direction
       */
      if (count > 0)
        gtk_text_iter_order (&selection, &cursor);
      else
        gtk_text_iter_order (&cursor, &selection);

      sel_start = &cursor;

      /* if we actually have a selection, just move *to* the beginning/end
       * of the selection and not *from* there on LOGICAL_POSITIONS
       * and VISUAL_POSITIONS movement
       */
      if (! gtk_text_iter_equal (&cursor, &selection))
        cancel_selection = TRUE;
    }

  switch (step)
    {
    case GTK_MOVEMENT_LOGICAL_POSITIONS:
      if (! cancel_selection)
        gtk_text_iter_forward_visible_cursor_positions (&cursor, count);
      break;

    case GTK_MOVEMENT_VISUAL_POSITIONS:
      if (! cancel_selection)
        {
          PangoLayout *layout;
          const gchar *text;

          if (! gimp_text_tool_ensure_layout (text_tool))
            break;

          layout = gimp_text_layout_get_pango_layout (text_tool->layout);
          text = pango_layout_get_text (layout);

          while (count != 0)
            {
              const gunichar word_joiner = 8288; /*g_utf8_get_char(WORD_JOINER);*/
              gint index;
              gint trailing = 0;
              gint new_index;

              index = gimp_text_buffer_get_iter_index (text_tool->buffer,
                                                       &cursor, TRUE);

              if (count > 0)
                {
                  if (g_utf8_get_char (text + index) == word_joiner)
                    pango_layout_move_cursor_visually (layout, TRUE,
                                                       index, 0, 1,
                                                       &new_index, &trailing);
                  else
                    new_index = index;

                  pango_layout_move_cursor_visually (layout, TRUE,
                                                     new_index, trailing, 1,
                                                     &new_index, &trailing);
                  count--;
                }
              else
                {
                  pango_layout_move_cursor_visually (layout, TRUE,
                                                     index, 0, -1,
                                                     &new_index, &trailing);

                  if (new_index != -1 && new_index != G_MAXINT &&
                      g_utf8_get_char (text + new_index) == word_joiner)
                    {
                      pango_layout_move_cursor_visually (layout, TRUE,
                                                         new_index, trailing, -1,
                                                         &new_index, &trailing);
                    }

                  count++;
                }

              if (new_index != G_MAXINT && new_index != -1)
                index = new_index;
              else
                break;

              gimp_text_buffer_get_iter_at_index (text_tool->buffer,
                                                  &cursor, index, TRUE);
              gtk_text_iter_forward_chars (&cursor, trailing);
            }
        }
      break;

    case GTK_MOVEMENT_WORDS:
      if (count < 0)
        {
          gtk_text_iter_backward_visible_word_starts (&cursor, -count);
        }
      else if (count > 0)
        {
          if (! gtk_text_iter_forward_visible_word_ends (&cursor, count))
            gtk_text_iter_forward_to_line_end (&cursor);
        }
      break;

    case GTK_MOVEMENT_DISPLAY_LINES:
      {
        GtkTextIter      start;
        GtkTextIter      end;
        gint             cursor_index;
        PangoLayout     *layout;
        PangoLayoutLine *layout_line;
        PangoLayoutIter *layout_iter;
        PangoRectangle   logical;
        gint             line;
        gint             trailing;
        gint             i;

        gtk_text_buffer_get_bounds (buffer, &start, &end);

        cursor_index = gimp_text_buffer_get_iter_index (text_tool->buffer,
                                                        &cursor, TRUE);

        if (! gimp_text_tool_ensure_layout (text_tool))
          break;

        layout = gimp_text_layout_get_pango_layout (text_tool->layout);

        pango_layout_index_to_line_x (layout, cursor_index, FALSE,
                                      &line, &x_pos);

        layout_iter = pango_layout_get_iter (layout);
        for (i = 0; i < line; i++)
          pango_layout_iter_next_line (layout_iter);

        pango_layout_iter_get_line_extents (layout_iter, NULL, &logical);

        x_pos += logical.x;

        pango_layout_iter_free (layout_iter);

        /*  try to go to the remembered x_pos if it exists *and* we are at
         *  the beginning or at the end of the current line
         */
        if (text_tool->x_pos != -1 && (x_pos <= logical.x ||
                                       x_pos >= logical.x + logical.width))
          x_pos = text_tool->x_pos;

        line += count;

        if (line < 0)
          {
            cursor = start;
            break;
          }
        else if (line >= pango_layout_get_line_count (layout))
          {
            cursor = end;
            break;
          }

        layout_iter = pango_layout_get_iter (layout);
        for (i = 0; i < line; i++)
          pango_layout_iter_next_line (layout_iter);

        layout_line = pango_layout_iter_get_line_readonly (layout_iter);
        pango_layout_iter_get_line_extents (layout_iter, NULL, &logical);

        pango_layout_iter_free (layout_iter);

        pango_layout_line_x_to_index (layout_line, x_pos - logical.x,
                                      &cursor_index, &trailing);

        gimp_text_buffer_get_iter_at_index (text_tool->buffer, &cursor,
                                            cursor_index, TRUE);

        while (trailing--)
          gtk_text_iter_forward_char (&cursor);
      }
      break;

    case GTK_MOVEMENT_PAGES: /* well... */
    case GTK_MOVEMENT_BUFFER_ENDS:
      if (count < 0)
        {
          gtk_text_buffer_get_start_iter (buffer, &cursor);
        }
      else if (count > 0)
        {
          gtk_text_buffer_get_end_iter (buffer, &cursor);
        }
      break;

    case GTK_MOVEMENT_PARAGRAPH_ENDS:
      if (count < 0)
        {
          gtk_text_iter_set_line_offset (&cursor, 0);
        }
      else if (count > 0)
        {
          if (! gtk_text_iter_ends_line (&cursor))
            gtk_text_iter_forward_to_line_end (&cursor);
        }
      break;

    case GTK_MOVEMENT_DISPLAY_LINE_ENDS:
      if (count < 0)
        {
          gtk_text_iter_set_line_offset (&cursor, 0);
        }
      else if (count > 0)
        {
          if (! gtk_text_iter_ends_line (&cursor))
            gtk_text_iter_forward_to_line_end (&cursor);
        }
      break;

    default:
      return;
    }

  text_tool->x_pos = x_pos;

  gimp_draw_tool_pause (GIMP_DRAW_TOOL (text_tool));

  gimp_text_tool_reset_im_context (text_tool);

  gtk_text_buffer_select_range (buffer, &cursor, sel_start);
  gimp_text_tool_editor_copy_selection_to_clipboard (text_tool);

  gimp_draw_tool_resume (GIMP_DRAW_TOOL (text_tool));
}

static void
gimp_text_tool_insert_at_cursor (GimpTextTool *text_tool,
                                 const gchar  *str)
{
  gimp_text_buffer_insert (text_tool->buffer, str);
}

static gboolean
is_whitespace (gunichar ch,
               gpointer user_data)
{
  return (ch == ' ' || ch == '\t');
}

static gboolean
is_not_whitespace (gunichar ch,
                   gpointer user_data)
{
  return ! is_whitespace (ch, user_data);
}

static gboolean
find_whitepace_region (const GtkTextIter *center,
                       GtkTextIter       *start,
                       GtkTextIter       *end)
{
  *start = *center;
  *end   = *center;

  if (gtk_text_iter_backward_find_char (start, is_not_whitespace, NULL, NULL))
    gtk_text_iter_forward_char (start); /* we want the first whitespace... */

  if (is_whitespace (gtk_text_iter_get_char (end), NULL))
    gtk_text_iter_forward_find_char (end, is_not_whitespace, NULL, NULL);

  return ! gtk_text_iter_equal (start, end);
}

static void
gimp_text_tool_delete_from_cursor (GimpTextTool  *text_tool,
                                   GtkDeleteType  type,
                                   gint           count)
{
  GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer);
  GtkTextIter    cursor;
  GtkTextIter    end;

  GIMP_LOG (TEXT_EDITING, "%s count = %d",
            g_enum_get_value (g_type_class_ref (GTK_TYPE_DELETE_TYPE),
                              type)->value_name,
            count);

  gimp_text_tool_reset_im_context (text_tool);

  gtk_text_buffer_get_iter_at_mark (buffer, &cursor,
                                    gtk_text_buffer_get_insert (buffer));
  end = cursor;

  switch (type)
    {
    case GTK_DELETE_CHARS:
      if (gtk_text_buffer_get_has_selection (buffer))
        {
          gtk_text_buffer_delete_selection (buffer, TRUE, TRUE);
          return;
        }
      else
        {
          gtk_text_iter_forward_cursor_positions (&end, count);
        }
      break;

    case GTK_DELETE_WORD_ENDS:
      if (count < 0)
        {
          if (! gtk_text_iter_starts_word (&cursor))
            gtk_text_iter_backward_visible_word_starts (&cursor, 1);
        }
      else if (count > 0)
        {
          if (! gtk_text_iter_ends_word (&end) &&
              ! gtk_text_iter_forward_visible_word_ends (&end, 1))
            gtk_text_iter_forward_to_line_end (&end);
        }
      break;

    case GTK_DELETE_WORDS:
      if (! gtk_text_iter_starts_word (&cursor))
        gtk_text_iter_backward_visible_word_starts (&cursor, 1);

      if (! gtk_text_iter_ends_word (&end) &&
          ! gtk_text_iter_forward_visible_word_ends (&end, 1))
        gtk_text_iter_forward_to_line_end (&end);
      break;

    case GTK_DELETE_DISPLAY_LINES:
      break;

    case GTK_DELETE_DISPLAY_LINE_ENDS:
      break;

    case GTK_DELETE_PARAGRAPH_ENDS:
      if (count < 0)
        {
          gtk_text_iter_set_line_offset (&cursor, 0);
        }
      else if (count > 0)
        {
          if (! gtk_text_iter_ends_line (&end))
            gtk_text_iter_forward_to_line_end (&end);
          else
            gtk_text_iter_forward_cursor_positions (&end, 1);
        }
      break;

    case GTK_DELETE_PARAGRAPHS:
      break;

    case GTK_DELETE_WHITESPACE:
      find_whitepace_region (&cursor, &cursor, &end);
      break;
    }

  if (! gtk_text_iter_equal (&cursor, &end))
    {
      gtk_text_buffer_delete_interactive (buffer, &cursor, &end, TRUE);
    }
}

static void
gimp_text_tool_backspace (GimpTextTool *text_tool)
{
  GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer);

  gimp_text_tool_reset_im_context (text_tool);

  if (gtk_text_buffer_get_has_selection (buffer))
    {
      gtk_text_buffer_delete_selection (buffer, TRUE, TRUE);
    }
  else
    {
      GtkTextIter cursor;

      gtk_text_buffer_get_iter_at_mark (buffer, &cursor,
                                        gtk_text_buffer_get_insert (buffer));

      gtk_text_buffer_backspace (buffer, &cursor, TRUE, TRUE);
    }
}

static void
gimp_text_tool_toggle_overwrite (GimpTextTool *text_tool)
{
  gimp_draw_tool_pause (GIMP_DRAW_TOOL (text_tool));

  text_tool->overwrite_mode = ! text_tool->overwrite_mode;

  gimp_draw_tool_resume (GIMP_DRAW_TOOL (text_tool));
}

static void
gimp_text_tool_select_all (GimpTextTool *text_tool,
                           gboolean      select)
{
  GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer);

  gimp_draw_tool_pause (GIMP_DRAW_TOOL (text_tool));

  if (select)
    {
      GtkTextIter start, end;

      gtk_text_buffer_get_bounds (buffer, &start, &end);
      gtk_text_buffer_select_range (buffer, &start, &end);
    }
  else
    {
      GtkTextIter cursor;

      gtk_text_buffer_get_iter_at_mark (buffer, &cursor,
                                        gtk_text_buffer_get_insert (buffer));
      gtk_text_buffer_move_mark_by_name (buffer, "selection_bound", &cursor);
    }

  gimp_draw_tool_resume (GIMP_DRAW_TOOL (text_tool));
}

static void
gimp_text_tool_change_size (GimpTextTool *text_tool,
                            gdouble       amount)
{
  GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer);
  GtkTextIter    start;
  GtkTextIter    end;

  if (! gtk_text_buffer_get_selection_bounds (buffer, &start, &end))
    {
      return;
    }

  gtk_text_iter_order (&start, &end);
  gimp_text_buffer_change_size (text_tool->buffer, &start, &end,
                                amount * PANGO_SCALE);
}

static void
gimp_text_tool_change_baseline (GimpTextTool *text_tool,
                                gdouble       amount)
{
  GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer);
  GtkTextIter    start;
  GtkTextIter    end;

  if (! gtk_text_buffer_get_selection_bounds (buffer, &start, &end))
    {
      gtk_text_buffer_get_iter_at_mark (buffer, &start,
                                        gtk_text_buffer_get_insert (buffer));
      gtk_text_buffer_get_end_iter (buffer, &end);
    }

  gtk_text_iter_order (&start, &end);
  gimp_text_buffer_change_baseline (text_tool->buffer, &start, &end,
                                    amount * PANGO_SCALE);
}

static void
gimp_text_tool_change_kerning (GimpTextTool *text_tool,
                               gdouble       amount)
{
  GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer);
  GtkTextIter    start;
  GtkTextIter    end;

  if (! gtk_text_buffer_get_selection_bounds (buffer, &start, &end))
    {
      gtk_text_buffer_get_iter_at_mark (buffer, &start,
                                        gtk_text_buffer_get_insert (buffer));
      end = start;
      gtk_text_iter_forward_char (&end);
    }

  gtk_text_iter_order (&start, &end);
  gimp_text_buffer_change_kerning (text_tool->buffer, &start, &end,
                                   amount * PANGO_SCALE);
}

static void
gimp_text_tool_options_notify (GimpTextOptions *options,
                               GParamSpec      *pspec,
                               GimpTextTool    *text_tool)
{
  const gchar *param_name = g_param_spec_get_name (pspec);

  if (! strcmp (param_name, "use-editor"))
    {
      if (options->use_editor)
        {
          gimp_text_tool_editor_dialog (text_tool);
        }
      else
        {
          if (text_tool->editor_dialog)
            gtk_widget_destroy (text_tool->editor_dialog);
        }
    }
  else if (! strcmp (param_name, "show-on-canvas"))
    {
      gtk_widget_set_visible (text_tool->style_editor,
                              options->show_on_canvas);
      gtk_widget_set_visible (text_tool->style_overlay,
                              options->show_on_canvas);
    }
}

static void
gimp_text_tool_editor_dialog (GimpTextTool *text_tool)
{
  GimpTool          *tool    = GIMP_TOOL (text_tool);
  GimpTextOptions   *options = GIMP_TEXT_TOOL_GET_OPTIONS (text_tool);
  GimpDisplayShell  *shell   = gimp_display_get_shell (tool->display);
  GimpImageWindow   *image_window;
  GimpDialogFactory *dialog_factory;
  GtkWindow         *parent  = NULL;
  gdouble            xres    = 1.0;
  gdouble            yres    = 1.0;

  if (text_tool->editor_dialog)
    {
      gtk_window_present (GTK_WINDOW (text_tool->editor_dialog));
      return;
    }

  image_window   = gimp_display_shell_get_window (shell);
  dialog_factory = gimp_dock_container_get_dialog_factory (GIMP_DOCK_CONTAINER (image_window));

  if (text_tool->image)
    gimp_image_get_resolution (text_tool->image, &xres, &yres);

  g_set_weak_pointer
    (&text_tool->editor_dialog,
     gimp_text_options_editor_new (parent, tool->tool_info->gimp, options,
                                   menus_get_global_menu_factory (tool->tool_info->gimp),
                                   _("GIMP Text Editor"),
                                   text_tool->proxy, text_tool->buffer,
                                   xres, yres));

  gimp_dialog_factory_add_foreign (dialog_factory,
                                   "gimp-text-tool-dialog",
                                   text_tool->editor_dialog,
                                   gimp_widget_get_monitor (GTK_WIDGET (image_window)));

  g_signal_connect (text_tool->editor_dialog, "destroy",
                    G_CALLBACK (gimp_text_tool_editor_destroy),
                    text_tool);

  gtk_widget_show (text_tool->editor_dialog);
}

static void
gimp_text_tool_editor_destroy (GtkWidget    *dialog,
                               GimpTextTool *text_tool)
{
  GimpTextOptions *options = GIMP_TEXT_TOOL_GET_OPTIONS (text_tool);

  g_object_set (options,
                "use-editor", FALSE,
                NULL);
}

static void
gimp_text_tool_enter_text (GimpTextTool *text_tool,
                           const gchar  *str)
{
  GtkTextBuffer *buffer;
  gboolean       had_selection;

  buffer        = GTK_TEXT_BUFFER (text_tool->buffer);
  had_selection = gtk_text_buffer_get_has_selection (buffer);

  gtk_text_buffer_begin_user_action (buffer);

  if (! had_selection && text_tool->overwrite_mode && strcmp (str, "\n"))
    {
      GtkTextIter cursor;

      gtk_text_buffer_get_iter_at_mark (buffer, &cursor,
                                        gtk_text_buffer_get_insert (buffer));

      if (! gtk_text_iter_ends_line (&cursor))
        gimp_text_tool_delete_from_cursor (text_tool, GTK_DELETE_CHARS, 1);
    }

  if (had_selection)
    {
      GtkTextIter start, end;
      GList      *insert_tags = NULL;
      GList      *remove_tags = NULL;

      gtk_text_buffer_get_selection_bounds (buffer, &start, &end);
      gtk_text_iter_order (&start, &end);

      insert_tags = gimp_text_buffer_get_tags_on_iter (text_tool->buffer, &start);

      gimp_text_tool_delete_selection (text_tool);

      remove_tags = gimp_text_buffer_get_all_tags (text_tool->buffer);
      gimp_text_buffer_set_insert_tags (text_tool->buffer,
                                        insert_tags, remove_tags);
    }

  gimp_text_buffer_insert (text_tool->buffer, str);

  gtk_text_buffer_end_user_action (buffer);
}

static void
gimp_text_tool_xy_to_iter (GimpTextTool *text_tool,
                           gdouble       x,
                           gdouble       y,
                           GtkTextIter  *iter)
{
  PangoLayout *layout;
  gint         offset_x;
  gint         offset_y;
  gint         index;
  gint         trailing;

  gimp_text_tool_ensure_layout (text_tool);

  gimp_text_layout_untransform_point (text_tool->layout, &x, &y);

  gimp_text_layout_get_offsets (text_tool->layout, &offset_x, &offset_y);
  x -= offset_x;
  y -= offset_y;

  layout = gimp_text_layout_get_pango_layout (text_tool->layout);

  gimp_text_tool_fix_position (text_tool, &x, &y);

  pango_layout_xy_to_index (layout,
                            x * PANGO_SCALE,
                            y * PANGO_SCALE,
                            &index, &trailing);

  gimp_text_buffer_get_iter_at_index (text_tool->buffer, iter, index, TRUE);

  if (trailing)
    gtk_text_iter_forward_char (iter);
}

static void
gimp_text_tool_im_preedit_start (GtkIMContext *context,
                                 GimpTextTool *text_tool)
{
  GIMP_LOG (TEXT_EDITING, "preedit start");

  text_tool->preedit_active = TRUE;
}

static void
gimp_text_tool_im_preedit_end (GtkIMContext *context,
                               GimpTextTool *text_tool)
{
  gimp_text_tool_delete_selection (text_tool);

  text_tool->preedit_active = FALSE;

  GIMP_LOG (TEXT_EDITING, "preedit end");
}

static void
gimp_text_tool_im_preedit_changed (GtkIMContext *context,
                                   GimpTextTool *text_tool)
{
  GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer);
  PangoAttrList *attrs;

  GIMP_LOG (TEXT_EDITING, "preedit changed");

  gtk_text_buffer_begin_user_action (buffer);

  gimp_text_tool_im_delete_preedit (text_tool);

  gimp_text_tool_delete_selection (text_tool);

  gtk_im_context_get_preedit_string (context,
                                     &text_tool->preedit_string, &attrs,
                                     &text_tool->preedit_cursor);

  if (text_tool->preedit_string && *text_tool->preedit_string)
    {
      PangoAttrIterator *attr_iter;
      GtkTextIter        iter;
      gint               i;

      /* Save the preedit start position. */
      gtk_text_buffer_get_iter_at_mark (buffer, &iter,
                                        gtk_text_buffer_get_insert (buffer));
      text_tool->preedit_start = gtk_text_buffer_create_mark (buffer,
                                                              "preedit-start",
                                                              &iter, TRUE);

      /* Loop through chunks of preedit text with different attributes. */
      attr_iter = pango_attr_list_get_iterator (attrs);
      do
        {
          const gchar *valid_preedit_end;
          gint         attr_start;
          gint         attr_end;

          pango_attr_iterator_range (attr_iter, &attr_start, &attr_end);
          if (attr_start < strlen (text_tool->preedit_string))
            {
              GSList      *attrs_pos;
              GtkTextMark *start_mark;
              GtkTextIter  start;
              GtkTextIter  end;

              gtk_text_buffer_get_iter_at_mark (buffer, &start,
                                                gtk_text_buffer_get_insert (buffer));
              start_mark = gtk_text_buffer_create_mark (buffer,
                                                        NULL,
                                                        &start, TRUE);

              gtk_text_buffer_begin_user_action (buffer);

              /*
               * Returned attribute end by pango_attr_iterator_range()
               * may be G_MAXINT to mean the string end, though somehow
               * we only encountered this in Wayland. Anyway let's take
               * this possibility into account.
               */
              if (attr_end > strlen (text_tool->preedit_string))
                attr_end = strlen (text_tool->preedit_string);

              /* Double check encoding validity. */
              if (! g_utf8_validate (text_tool->preedit_string + attr_start, attr_end - attr_start, &valid_preedit_end))
                g_warning ("%s: preedit string is not valid UTF-8.", G_STRFUNC);

              /* Insert the preedit chunk at current cursor position. */
              gtk_text_buffer_insert_at_cursor (GTK_TEXT_BUFFER (text_tool->buffer),
                                                text_tool->preedit_string + attr_start,
                                                valid_preedit_end - text_tool->preedit_string - attr_start);
              gtk_text_buffer_get_iter_at_mark (buffer, &start,
                                                start_mark);
              gtk_text_buffer_delete_mark (buffer, start_mark);
              gtk_text_buffer_get_iter_at_mark (buffer, &end,
                                                gtk_text_buffer_get_insert (buffer));

              /* Apply text attributes to preedit text. */
              attrs_pos = pango_attr_iterator_get_attrs (attr_iter);
              while (attrs_pos)
                {
                  PangoAttribute *attr = attrs_pos->data;

                  if (attr)
                    {
                      switch (attr->klass->type)
                        {
                        case PANGO_ATTR_UNDERLINE:
                          gtk_text_buffer_apply_tag (buffer,
                                                     text_tool->buffer->preedit_underline_tag,
                                                     &start, &end);
                          break;
                        case PANGO_ATTR_BACKGROUND:
                        case PANGO_ATTR_FOREGROUND:
                            {
                              PangoAttrColor *color_attr = (PangoAttrColor *) attr;
                              GeglColor      *color;

                              color = gegl_color_new (NULL);
                              gegl_color_set_pixel (color, babl_format ("R'G'B' u16"), &color_attr->color);

                              if (attr->klass->type == PANGO_ATTR_BACKGROUND)
                                gimp_text_buffer_set_preedit_bg_color (text_tool->buffer,
                                                                       &start, &end,
                                                                       color);
                              else
                                gimp_text_buffer_set_preedit_color (text_tool->buffer,
                                                                    &start, &end,
                                                                    color);

                              g_object_unref (color);
                            }
                          break;
                        default:
                          /* Unsupported tags. */
                          break;
                        }
                    }

                  attrs_pos = attrs_pos->next;
                }

              gtk_text_buffer_end_user_action (buffer);
            }
        }
      while (pango_attr_iterator_next (attr_iter));

      /* Save the preedit end position. */
      gtk_text_buffer_get_iter_at_mark (buffer, &iter,
                                        gtk_text_buffer_get_insert (buffer));
      text_tool->preedit_end = gtk_text_buffer_create_mark (buffer,
                                                            "preedit-end",
                                                            &iter, FALSE);

      /* Move the cursor to the expected location. */
      gtk_text_buffer_get_iter_at_mark (buffer, &iter, text_tool->preedit_start);
      for (i = 0; i < text_tool->preedit_cursor; i++)
        gtk_text_iter_forward_char (&iter);
      gtk_text_buffer_place_cursor (buffer, &iter);

      pango_attr_iterator_destroy (attr_iter);
    }

  pango_attr_list_unref (attrs);

  gtk_text_buffer_end_user_action (buffer);
}

static void
gimp_text_tool_im_commit (GtkIMContext *context,
                          const gchar  *str,
                          GimpTextTool *text_tool)
{
  gboolean preedit_active = text_tool->preedit_active;

  gimp_text_tool_im_delete_preedit (text_tool);

  /* Some IMEs would emit a preedit-commit before preedit-end.
   * To keep undo consistency, we fake and end then immediate restart of
   * preediting.
   */
  if (preedit_active)
    gimp_text_tool_im_preedit_end (context, text_tool);

  gimp_text_tool_enter_text (text_tool, str);

  if (preedit_active)
    gimp_text_tool_im_preedit_start (context, text_tool);
}

static gboolean
gimp_text_tool_im_retrieve_surrounding (GtkIMContext *context,
                                        GimpTextTool *text_tool)
{
  GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer);
  GtkTextIter    start;
  GtkTextIter    end;
  gint           pos;
  gchar         *text;

  gtk_text_buffer_get_iter_at_mark (buffer, &start,
                                    gtk_text_buffer_get_insert (buffer));
  end = start;

  pos = gtk_text_iter_get_line_index (&start);
  gtk_text_iter_set_line_offset (&start, 0);
  gtk_text_iter_forward_to_line_end (&end);

  text = gtk_text_iter_get_slice (&start, &end);
  gtk_im_context_set_surrounding (context, text, -1, pos);
  g_free (text);

  return TRUE;
}

static gboolean
gimp_text_tool_im_delete_surrounding (GtkIMContext *context,
                                      gint          offset,
                                      gint          n_chars,
                                      GimpTextTool *text_tool)
{
  GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer);
  GtkTextIter    start;
  GtkTextIter    end;

  gtk_text_buffer_get_iter_at_mark (buffer, &start,
                                    gtk_text_buffer_get_insert (buffer));
  end = start;

  gtk_text_iter_forward_chars (&start, offset);
  gtk_text_iter_forward_chars (&end, offset + n_chars);

  gtk_text_buffer_delete_interactive (buffer, &start, &end, TRUE);

  return TRUE;
}

static void
gimp_text_tool_im_delete_preedit (GimpTextTool *text_tool)
{
  if (text_tool->preedit_string)
    {
      if (*text_tool->preedit_string)
        {
          GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer);
          GtkTextIter    start;
          GtkTextIter    end;

          gtk_text_buffer_get_iter_at_mark (buffer, &start,
                                            text_tool->preedit_start);
          gtk_text_buffer_get_iter_at_mark (buffer, &end,
                                            text_tool->preedit_end);

          gtk_text_buffer_delete_interactive (buffer, &start, &end, TRUE);

          gtk_text_buffer_delete_mark (buffer, text_tool->preedit_start);
          gtk_text_buffer_delete_mark (buffer, text_tool->preedit_end);
          text_tool->preedit_start = NULL;
          text_tool->preedit_end = NULL;
        }

      g_clear_pointer (&text_tool->preedit_string, g_free);
    }
}

static void
gimp_text_tool_editor_copy_selection_to_clipboard (GimpTextTool *text_tool)
{
  GtkTextBuffer *buffer = GTK_TEXT_BUFFER (text_tool->buffer);

  if (! text_tool->editor_dialog &&
      gtk_text_buffer_get_has_selection (buffer))
    {
      GimpTool         *tool  = GIMP_TOOL (text_tool);
      GimpDisplayShell *shell = gimp_display_get_shell (tool->display);
      GtkClipboard     *clipboard;

      clipboard = gtk_widget_get_clipboard (GTK_WIDGET (shell),
                                            GDK_SELECTION_PRIMARY);

      gtk_text_buffer_copy_clipboard (buffer, clipboard);
    }
}

static void
gimp_text_tool_fix_position (GimpTextTool *text_tool,
                             gdouble      *x,
                             gdouble      *y)
{
  gint temp, width, height;

  gimp_text_layout_get_size (text_tool->layout, &width, &height);
  switch (gimp_text_tool_get_direction(text_tool))
    {
      case GIMP_TEXT_DIRECTION_RTL:
      case GIMP_TEXT_DIRECTION_LTR:
      break;
      case GIMP_TEXT_DIRECTION_TTB_RTL:
      case GIMP_TEXT_DIRECTION_TTB_RTL_UPRIGHT:
      temp = width - *x;
      *x = *y;
      *y = temp;
      break;
      case GIMP_TEXT_DIRECTION_TTB_LTR:
      case GIMP_TEXT_DIRECTION_TTB_LTR_UPRIGHT:
      temp = *x;
      *x = height - *y;
      *y = temp;
      break;
    }
}

static void
gimp_text_tool_convert_gdkkeyevent (GimpTextTool *text_tool,
                                    GdkEventKey  *kevent)
{
  switch (gimp_text_tool_get_direction (text_tool))
    {
    case GIMP_TEXT_DIRECTION_LTR:
    case GIMP_TEXT_DIRECTION_RTL:
      break;

    case GIMP_TEXT_DIRECTION_TTB_RTL:
    case GIMP_TEXT_DIRECTION_TTB_RTL_UPRIGHT:
#ifdef _WIN32
      switch (kevent->keyval)
        {
        case GDK_KEY_Up:
          kevent->hardware_keycode = 0x25;/* VK_LEFT */
          kevent->keyval = GDK_KEY_Left;
          break;
        case GDK_KEY_Down:
          kevent->hardware_keycode = 0x27;/* VK_RIGHT */
          kevent->keyval = GDK_KEY_Right;
          break;
        case GDK_KEY_Left:
          kevent->hardware_keycode = 0x28;/* VK_DOWN */
          kevent->keyval = GDK_KEY_Down;
          break;
        case GDK_KEY_Right:
          kevent->hardware_keycode = 0x26;/* VK_UP */
          kevent->keyval = GDK_KEY_Up;
          break;
        }
#else
      switch (kevent->keyval)
        {
        case GDK_KEY_Up:
          kevent->hardware_keycode = 113;
          kevent->keyval = GDK_KEY_Left;
          break;
        case GDK_KEY_Down:
          kevent->hardware_keycode = 114;
          kevent->keyval = GDK_KEY_Right;
          break;
        case GDK_KEY_Left:
          kevent->hardware_keycode = 116;
          kevent->keyval = GDK_KEY_Down;
          break;
        case GDK_KEY_Right:
          kevent->hardware_keycode = 111;
          kevent->keyval = GDK_KEY_Up;
          break;
        }
#endif
      break;
    case GIMP_TEXT_DIRECTION_TTB_LTR:
    case GIMP_TEXT_DIRECTION_TTB_LTR_UPRIGHT:
#ifdef _WIN32
      switch (kevent->keyval)
        {
        case GDK_KEY_Up:
          kevent->hardware_keycode = 0x26;/* VK_UP */
          kevent->keyval = GDK_KEY_Up;
          break;
        case GDK_KEY_Down:
          kevent->hardware_keycode = 0x28;/* VK_DOWN */
          kevent->keyval = GDK_KEY_Down;
          break;
        case GDK_KEY_Left:
          kevent->hardware_keycode = 0x25;/* VK_LEFT */
          kevent->keyval = GDK_KEY_Left;
          break;
        case GDK_KEY_Right:
          kevent->hardware_keycode = 0x27;/* VK_RIGHT */
          kevent->keyval = GDK_KEY_Right;
          break;
        }
#else
      switch (kevent->keyval)
        {
        case GDK_KEY_Up:
          kevent->hardware_keycode = 114;
          kevent->keyval = GDK_KEY_Right;
          break;
        case GDK_KEY_Down:
          kevent->hardware_keycode = 113;
          kevent->keyval = GDK_KEY_Left;
          break;
        case GDK_KEY_Left:
          kevent->hardware_keycode = 111;
          kevent->keyval = GDK_KEY_Up;
          break;
        case GDK_KEY_Right:
          kevent->hardware_keycode = 116;
          kevent->keyval = GDK_KEY_Down;
          break;
        }
#endif
      break;
    }
}
