/* PIKA - Photo and Image Kooker Application
 * a rebranding of The GNU Image Manipulation Program (created with heckimp)
 * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
 *
 * Original copyright, applying to most contents (license remains unchanged): 
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * 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 <string.h>
#include <stdlib.h>

#include <gegl.h>
#include <gtk/gtk.h>

#ifdef GDK_WINDOWING_WAYLAND
#include <gdk/gdkwayland.h>
#endif

#include "libpikabase/pikabase.h"
#include "libpikamath/pikamath.h"
#include "libpikacolor/pikacolor.h"
#include "libpikaconfig/pikaconfig.h"
#include "libpikawidgets/pikawidgets.h"

#include "display-types.h"
#include "tools/tools-types.h"

#include "config/pikacoreconfig.h"
#include "config/pikadisplayconfig.h"
#include "config/pikadisplayoptions.h"

#include "gegl/pika-babl.h"

#include "core/pika.h"
#include "core/pika-utils.h"
#include "core/pikachannel.h"
#include "core/pikacontext.h"
#include "core/pikaimage.h"
#include "core/pikaimage-grid.h"
#include "core/pikaimage-guides.h"
#include "core/pikaimage-snap.h"
#include "core/pikapickable.h"
#include "core/pikaprojectable.h"
#include "core/pikaprojection.h"
#include "core/pikatemplate.h"

#include "menus/menus.h"

#include "widgets/pikadevices.h"
#include "widgets/pikahelp-ids.h"
#include "widgets/pikauimanager.h"
#include "widgets/pikawidgets-utils.h"

#include "tools/tool_manager.h"

#include "pikacanvas.h"
#include "pikacanvascanvasboundary.h"
#include "pikacanvaslayerboundary.h"
#include "pikadisplay.h"
#include "pikadisplayshell.h"
#include "pikadisplayshell-appearance.h"
#include "pikadisplayshell-callbacks.h"
#include "pikadisplayshell-cursor.h"
#include "pikadisplayshell-dnd.h"
#include "pikadisplayshell-expose.h"
#include "pikadisplayshell-filter.h"
#include "pikadisplayshell-handlers.h"
#include "pikadisplayshell-items.h"
#include "pikadisplayshell-profile.h"
#include "pikadisplayshell-progress.h"
#include "pikadisplayshell-render.h"
#include "pikadisplayshell-rotate.h"
#include "pikadisplayshell-rulers.h"
#include "pikadisplayshell-scale.h"
#include "pikadisplayshell-scroll.h"
#include "pikadisplayshell-scrollbars.h"
#include "pikadisplayshell-selection.h"
#include "pikadisplayshell-title.h"
#include "pikadisplayshell-tool-events.h"
#include "pikadisplayshell-transform.h"
#include "pikaimagewindow.h"
#include "pikamotionbuffer.h"
#include "pikastatusbar.h"

#include "about.h"
#include "pika-log.h"
#include "pika-priorities.h"

#include "pika-intl.h"


enum
{
  PROP_0,
  PROP_POPUP_MANAGER,
  PROP_INITIAL_MONITOR,
  PROP_DISPLAY,
  PROP_UNIT,
  PROP_TITLE,
  PROP_STATUS,
  PROP_SHOW_ALL,
  PROP_INFINITE_CANVAS
};

enum
{
  SCALED,
  SCROLLED,
  ROTATED,
  RECONNECT,
  LAST_SIGNAL
};


typedef struct _PikaDisplayShellOverlay PikaDisplayShellOverlay;

struct _PikaDisplayShellOverlay
{
  PikaDisplayShell *shell;
  gdouble           image_x;
  gdouble           image_y;
  PikaHandleAnchor  anchor;
  gint              spacing_x;
  gint              spacing_y;
};


/*  local function prototypes  */

static void      pika_color_managed_iface_init     (PikaColorManagedInterface *iface);

static void      pika_display_shell_constructed    (GObject          *object);
static void      pika_display_shell_dispose        (GObject          *object);
static void      pika_display_shell_finalize       (GObject          *object);
static void      pika_display_shell_set_property   (GObject          *object,
                                                    guint             property_id,
                                                    const GValue     *value,
                                                    GParamSpec       *pspec);
static void      pika_display_shell_get_property   (GObject          *object,
                                                    guint             property_id,
                                                    GValue           *value,
                                                    GParamSpec       *pspec);

static void      pika_display_shell_unrealize      (GtkWidget        *widget);
static void      pika_display_shell_unmap          (GtkWidget        *widget);
static void      pika_display_shell_screen_changed (GtkWidget        *widget,
                                                    GdkScreen        *previous);
static gboolean  pika_display_shell_popup_menu     (GtkWidget        *widget);

static void      pika_display_shell_real_scaled    (PikaDisplayShell *shell);
static void      pika_display_shell_real_scrolled  (PikaDisplayShell *shell);
static void      pika_display_shell_real_rotated   (PikaDisplayShell *shell);

static const guint8 *
                 pika_display_shell_get_icc_profile(PikaColorManaged *managed,
                                                    gsize            *len);
static PikaColorProfile *
               pika_display_shell_get_color_profile(PikaColorManaged *managed);
static void      pika_display_shell_profile_changed(PikaColorManaged *managed);
static void    pika_display_shell_simulation_profile_changed
                                                   (PikaColorManaged *managed);
static void    pika_display_shell_simulation_intent_changed
                                                   (PikaColorManaged *managed);
static void    pika_display_shell_simulation_bpc_changed
                                                   (PikaColorManaged *managed);
static void      pika_display_shell_zoom_button_callback
                                                   (PikaDisplayShell *shell,
                                                    GtkWidget        *zoom_button);
static void      pika_display_shell_sync_config    (PikaDisplayShell  *shell,
                                                    PikaDisplayConfig *config);

static void    pika_display_shell_overlay_allocate (GtkWidget        *child,
                                                    GtkAllocation    *allocation,
                                                    PikaDisplayShellOverlay *overlay);
static void      pika_display_shell_remove_overlay (GtkWidget        *canvas,
                                                    GtkWidget        *child,
                                                    PikaDisplayShell *shell);
static void   pika_display_shell_transform_overlay (PikaDisplayShell *shell,
                                                    GtkWidget        *child,
                                                    gdouble          *x,
                                                    gdouble          *y);


G_DEFINE_TYPE_WITH_CODE (PikaDisplayShell, pika_display_shell,
                         GTK_TYPE_EVENT_BOX,
                         G_IMPLEMENT_INTERFACE (PIKA_TYPE_PROGRESS,
                                                pika_display_shell_progress_iface_init)
                         G_IMPLEMENT_INTERFACE (PIKA_TYPE_COLOR_MANAGED,
                                                pika_color_managed_iface_init))


#define parent_class pika_display_shell_parent_class

static guint display_shell_signals[LAST_SIGNAL] = { 0 };


static void
pika_display_shell_class_init (PikaDisplayShellClass *klass)
{
  GObjectClass   *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  display_shell_signals[SCALED] =
    g_signal_new ("scaled",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (PikaDisplayShellClass, scaled),
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 0);

  display_shell_signals[SCROLLED] =
    g_signal_new ("scrolled",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (PikaDisplayShellClass, scrolled),
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 0);

  display_shell_signals[ROTATED] =
    g_signal_new ("rotated",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (PikaDisplayShellClass, rotated),
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 0);

  display_shell_signals[RECONNECT] =
    g_signal_new ("reconnect",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (PikaDisplayShellClass, reconnect),
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 0);

  object_class->constructed        = pika_display_shell_constructed;
  object_class->dispose            = pika_display_shell_dispose;
  object_class->finalize           = pika_display_shell_finalize;
  object_class->set_property       = pika_display_shell_set_property;
  object_class->get_property       = pika_display_shell_get_property;

  widget_class->unrealize          = pika_display_shell_unrealize;
  widget_class->unmap              = pika_display_shell_unmap;
  widget_class->screen_changed     = pika_display_shell_screen_changed;
  widget_class->popup_menu         = pika_display_shell_popup_menu;

  klass->scaled                    = pika_display_shell_real_scaled;
  klass->scrolled                  = pika_display_shell_real_scrolled;
  klass->rotated                   = pika_display_shell_real_rotated;
  klass->reconnect                 = NULL;

  g_object_class_install_property (object_class, PROP_POPUP_MANAGER,
                                   g_param_spec_object ("popup-manager",
                                                        NULL, NULL,
                                                        PIKA_TYPE_UI_MANAGER,
                                                        PIKA_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_INITIAL_MONITOR,
                                   g_param_spec_object ("initial-monitor",
                                                        NULL, NULL,
                                                        GDK_TYPE_MONITOR,
                                                        PIKA_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_DISPLAY,
                                   g_param_spec_object ("display", NULL, NULL,
                                                        PIKA_TYPE_DISPLAY,
                                                        PIKA_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_UNIT,
                                   pika_param_spec_unit ("unit", NULL, NULL,
                                                         TRUE, FALSE,
                                                         PIKA_UNIT_PIXEL,
                                                         PIKA_PARAM_READWRITE));

  g_object_class_install_property (object_class, PROP_TITLE,
                                   g_param_spec_string ("title", NULL, NULL,
                                                        PIKA_NAME,
                                                        PIKA_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_STATUS,
                                   g_param_spec_string ("status", NULL, NULL,
                                                        NULL,
                                                        PIKA_PARAM_READWRITE));

  g_object_class_install_property (object_class, PROP_SHOW_ALL,
                                   g_param_spec_boolean ("show-all",
                                                         NULL, NULL,
                                                         FALSE,
                                                         PIKA_PARAM_READWRITE));

  g_object_class_install_property (object_class, PROP_INFINITE_CANVAS,
                                   g_param_spec_boolean ("infinite-canvas",
                                                         NULL, NULL,
                                                         FALSE,
                                                         PIKA_PARAM_READABLE));

  gtk_widget_class_set_css_name (widget_class, "PikaDisplayShell");
}

static void
pika_color_managed_iface_init (PikaColorManagedInterface *iface)
{
  iface->get_icc_profile            = pika_display_shell_get_icc_profile;
  iface->get_color_profile          = pika_display_shell_get_color_profile;
  iface->profile_changed            = pika_display_shell_profile_changed;
  iface->simulation_profile_changed = pika_display_shell_simulation_profile_changed;
  iface->simulation_intent_changed  = pika_display_shell_simulation_intent_changed;
  iface->simulation_bpc_changed     = pika_display_shell_simulation_bpc_changed;
}

static void
pika_display_shell_init (PikaDisplayShell *shell)
{
  const gchar *env;

  shell->options            = g_object_new (PIKA_TYPE_DISPLAY_OPTIONS, NULL);
  shell->fullscreen_options = g_object_new (PIKA_TYPE_DISPLAY_OPTIONS_FULLSCREEN, NULL);
  shell->no_image_options   = g_object_new (PIKA_TYPE_DISPLAY_OPTIONS_NO_IMAGE, NULL);

  shell->zoom        = pika_zoom_model_new ();
  shell->dot_for_dot = TRUE;
  shell->scale_x     = 1.0;
  shell->scale_y     = 1.0;

  shell->show_image  = TRUE;

  shell->show_all    = FALSE;

  pika_display_shell_items_init (shell);

  shell->cursor_handedness = PIKA_HANDEDNESS_RIGHT;
  shell->current_cursor    = (PikaCursorType) -1;
  shell->tool_cursor       = PIKA_TOOL_CURSOR_NONE;
  shell->cursor_modifier   = PIKA_CURSOR_MODIFIER_NONE;
  shell->override_cursor   = (PikaCursorType) -1;

  shell->filter_format     = babl_format ("R'G'B'A float");
  shell->filter_profile    = pika_babl_get_builtin_color_profile (PIKA_RGB,
                                                                  PIKA_TRC_NON_LINEAR);

  shell->render_scale      = 1;

  shell->render_buf_width  = 256;
  shell->render_buf_height = 256;

  shell->snapped_side_horizontal      = PIKA_ARRANGE_HFILL;
  shell->snapped_layer_horizontal     = NULL;
  shell->snapped_side_vertical        = PIKA_ARRANGE_HFILL;
  shell->snapped_layer_vertical       = NULL;
  shell->equidistance_side_horizontal = PIKA_ARRANGE_HFILL;
  shell->near_layer_horizontal1       = NULL;
  shell->near_layer_horizontal2       = NULL;
  shell->equidistance_side_vertical   = PIKA_ARRANGE_HFILL;
  shell->near_layer_vertical1         = NULL;
  shell->near_layer_vertical2         = NULL;

  env = g_getenv ("PIKA_DISPLAY_RENDER_BUF_SIZE");

  if (env)
    {
      gint width  = atoi (env);
      gint height = width;

      env = strchr (env, 'x');
      if (env)
        height = atoi (env + 1);

      if (width  > 0 && width  <= 8192 &&
          height > 0 && height <= 8192)
        {
          shell->render_buf_width  = width;
          shell->render_buf_height = height;
        }
    }

  shell->motion_buffer   = pika_motion_buffer_new ();

  g_signal_connect (shell->motion_buffer, "stroke",
                    G_CALLBACK (pika_display_shell_buffer_stroke),
                    shell);
  g_signal_connect (shell->motion_buffer, "hover",
                    G_CALLBACK (pika_display_shell_buffer_hover),
                    shell);

  shell->zoom_focus_point = NULL;

  gtk_widget_set_events (GTK_WIDGET (shell), (GDK_POINTER_MOTION_MASK    |
                                              GDK_BUTTON_PRESS_MASK      |
                                              GDK_KEY_PRESS_MASK         |
                                              GDK_KEY_RELEASE_MASK       |
                                              GDK_FOCUS_CHANGE_MASK      |
                                              GDK_VISIBILITY_NOTIFY_MASK |
                                              GDK_SCROLL_MASK            |
                                              GDK_SMOOTH_SCROLL_MASK));

  /*  zoom model callback  */
  g_signal_connect_swapped (shell->zoom, "zoomed",
                            G_CALLBACK (pika_display_shell_scale_update),
                            shell);

  /*  active display callback  */
  g_signal_connect (shell, "button-press-event",
                    G_CALLBACK (pika_display_shell_events),
                    shell);
  g_signal_connect (shell, "button-release-event",
                    G_CALLBACK (pika_display_shell_events),
                    shell);
  g_signal_connect (shell, "key-press-event",
                    G_CALLBACK (pika_display_shell_events),
                    shell);

  pika_help_connect (GTK_WIDGET (shell), pika_standard_help_func,
                     PIKA_HELP_IMAGE_WINDOW, NULL, NULL);
}

static void
pika_display_shell_constructed (GObject *object)
{
  PikaDisplayShell  *shell = PIKA_DISPLAY_SHELL (object);
  PikaDisplayConfig *config;
  PikaImage         *image;
  GtkWidget         *grid;
  GtkWidget         *gtk_image;
  PikaAction        *action;
  gint               image_width;
  gint               image_height;
  gint               shell_width;
  gint               shell_height;

  G_OBJECT_CLASS (parent_class)->constructed (object);

  pika_assert (PIKA_IS_UI_MANAGER (shell->popup_manager));
  pika_assert (PIKA_IS_DISPLAY (shell->display));

  config = shell->display->config;
  image  = pika_display_get_image (shell->display);

  pika_display_shell_profile_init (shell);

  if (image)
    {
      image_width  = pika_image_get_width  (image);
      image_height = pika_image_get_height (image);
    }
  else
    {
      /* These values are arbitrary. The width is determined by the
       * menubar and the height is chosen to give a window aspect
       * ratio of roughly 3:1 (as requested by the UI team).
       */
      image_width  = PIKA_DEFAULT_IMAGE_WIDTH;
      image_height = PIKA_DEFAULT_IMAGE_HEIGHT / 3;
    }

  shell->dot_for_dot = config->default_dot_for_dot;

  if (config->monitor_res_from_gdk)
    {
      pika_get_monitor_resolution (shell->initial_monitor,
                                   &shell->monitor_xres, &shell->monitor_yres);
    }
  else
    {
      shell->monitor_xres = config->monitor_xres;
      shell->monitor_yres = config->monitor_yres;
    }

  /* adjust the initial scale -- so that window fits on screen. */
  if (image)
    {
      pika_display_shell_set_initial_scale (shell, 1.0, //scale,
                                            &shell_width, &shell_height);
    }
  else
    {
      shell_width  = -1;
      shell_height = image_height;
    }

  pika_display_shell_sync_config (shell, config);

  /*  the grid containing everything  */
  grid = gtk_grid_new ();
  gtk_container_add (GTK_CONTAINER (shell), grid);
  gtk_widget_show (grid);

  /*  the horizontal scrollbar  */
  shell->hsbdata = gtk_adjustment_new (0, 0, image_width,
                                       1, 1, image_width);
  shell->hsb = gtk_scrollbar_new (GTK_ORIENTATION_HORIZONTAL, shell->hsbdata);
  gtk_widget_set_can_focus (shell->hsb, FALSE);

  /*  the vertical scrollbar  */
  shell->vsbdata = gtk_adjustment_new (0, 0, image_height,
                                       1, 1, image_height);
  shell->vsb = gtk_scrollbar_new (GTK_ORIENTATION_VERTICAL, shell->vsbdata);
  gtk_widget_set_can_focus (shell->vsb, FALSE);

  /*  the menu popup button  */
  shell->origin = gtk_event_box_new ();
  gtk_image = gtk_image_new_from_icon_name (PIKA_ICON_MENU_RIGHT,
                                            GTK_ICON_SIZE_MENU);
  gtk_container_add (GTK_CONTAINER (shell->origin), gtk_image);
  gtk_widget_show (gtk_image);

  g_signal_connect (shell->origin, "button-press-event",
                    G_CALLBACK (pika_display_shell_origin_button_press),
                    shell);

  pika_help_set_help_data (shell->origin,
                           _("Access the image menu"),
                           PIKA_HELP_IMAGE_WINDOW_ORIGIN);

  /*  the canvas  */
  shell->canvas = pika_canvas_new (config);
  gtk_widget_set_size_request (shell->canvas, shell_width, shell_height);
  gtk_container_set_border_width (GTK_CONTAINER (shell->canvas), 10);

  g_signal_connect (shell->canvas, "remove",
                    G_CALLBACK (pika_display_shell_remove_overlay),
                    shell);

  pika_display_shell_dnd_init (shell);
  pika_display_shell_selection_init (shell);

  shell->zoom_gesture = gtk_gesture_zoom_new (GTK_WIDGET (shell->canvas));
  gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (shell->zoom_gesture),
                                              GTK_PHASE_CAPTURE);
  shell->zoom_gesture_active = FALSE;

  shell->rotate_gesture = gtk_gesture_rotate_new (GTK_WIDGET (shell->canvas));
  gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (shell->rotate_gesture),
                                              GTK_PHASE_CAPTURE);
  shell->rotate_gesture_active = FALSE;

  /*  the horizontal ruler  */
  shell->hrule = pika_ruler_new (GTK_ORIENTATION_HORIZONTAL);
  gtk_widget_set_events (GTK_WIDGET (shell->hrule),
                         GDK_BUTTON_PRESS_MASK |
                         GDK_BUTTON_RELEASE_MASK);
  pika_ruler_add_track_widget (PIKA_RULER (shell->hrule), shell->canvas);

  g_signal_connect (shell->hrule, "button-press-event",
                    G_CALLBACK (pika_display_shell_hruler_button_press),
                    shell);

  pika_help_set_help_data (shell->hrule, NULL, PIKA_HELP_IMAGE_WINDOW_RULER);

  /*  the vertical ruler  */
  shell->vrule = pika_ruler_new (GTK_ORIENTATION_VERTICAL);
  gtk_widget_set_events (GTK_WIDGET (shell->vrule),
                         GDK_BUTTON_PRESS_MASK |
                         GDK_BUTTON_RELEASE_MASK);
  pika_ruler_add_track_widget (PIKA_RULER (shell->vrule), shell->canvas);

  g_signal_connect (shell->vrule, "button-press-event",
                    G_CALLBACK (pika_display_shell_vruler_button_press),
                    shell);

  pika_help_set_help_data (shell->vrule, NULL, PIKA_HELP_IMAGE_WINDOW_RULER);

  /*  set the rulers as track widgets for each other, so we don't end up
   *  with one ruler wrongly being stuck a few pixels off while we are
   *  hovering the other
   */
  pika_ruler_add_track_widget (PIKA_RULER (shell->hrule), shell->vrule);
  pika_ruler_add_track_widget (PIKA_RULER (shell->vrule), shell->hrule);

  pika_devices_add_widget (shell->display->pika, shell->hrule);
  pika_devices_add_widget (shell->display->pika, shell->vrule);

  g_signal_connect (shell->canvas, "grab-notify",
                    G_CALLBACK (pika_display_shell_canvas_grab_notify),
                    shell);

  pika_widget_set_native_handle (GTK_WIDGET (shell), &shell->window_handle);

  g_signal_connect (shell->canvas, "realize",
                    G_CALLBACK (pika_display_shell_canvas_realize),
                    shell);
  g_signal_connect (shell->canvas, "size-allocate",
                    G_CALLBACK (pika_display_shell_canvas_size_allocate),
                    shell);
  g_signal_connect (shell->canvas, "draw",
                    G_CALLBACK (pika_display_shell_canvas_draw),
                    shell);

  g_signal_connect (shell->canvas, "enter-notify-event",
                    G_CALLBACK (pika_display_shell_canvas_tool_events),
                    shell);
  g_signal_connect (shell->canvas, "leave-notify-event",
                    G_CALLBACK (pika_display_shell_canvas_tool_events),
                    shell);
  g_signal_connect (shell->canvas, "proximity-in-event",
                    G_CALLBACK (pika_display_shell_canvas_tool_events),
                    shell);
  g_signal_connect (shell->canvas, "proximity-out-event",
                    G_CALLBACK (pika_display_shell_canvas_tool_events),
                    shell);
  g_signal_connect (shell->canvas, "focus-in-event",
                    G_CALLBACK (pika_display_shell_canvas_tool_events),
                    shell);
  g_signal_connect (shell->canvas, "focus-out-event",
                    G_CALLBACK (pika_display_shell_canvas_tool_events),
                    shell);
  g_signal_connect (shell->canvas, "button-press-event",
                    G_CALLBACK (pika_display_shell_canvas_tool_events),
                    shell);
  g_signal_connect (shell->canvas, "button-release-event",
                    G_CALLBACK (pika_display_shell_canvas_tool_events),
                    shell);
  g_signal_connect (shell->canvas, "scroll-event",
                    G_CALLBACK (pika_display_shell_canvas_tool_events),
                    shell);
  g_signal_connect (shell->canvas, "motion-notify-event",
                    G_CALLBACK (pika_display_shell_canvas_tool_events),
                    shell);
  g_signal_connect (shell->canvas, "key-press-event",
                    G_CALLBACK (pika_display_shell_canvas_tool_events),
                    shell);
  g_signal_connect (shell->canvas, "key-release-event",
                    G_CALLBACK (pika_display_shell_canvas_tool_events),
                    shell);
  g_signal_connect (shell->zoom_gesture, "begin",
                    G_CALLBACK (pika_display_shell_zoom_gesture_begin),
                    shell);
  g_signal_connect (shell->zoom_gesture, "update",
                    G_CALLBACK (pika_display_shell_zoom_gesture_update),
                    shell);
  g_signal_connect (shell->zoom_gesture, "end",
                    G_CALLBACK (pika_display_shell_zoom_gesture_end),
                    shell);
  g_signal_connect (shell->rotate_gesture, "begin",
                    G_CALLBACK (pika_display_shell_rotate_gesture_begin),
                    shell);
  g_signal_connect (shell->rotate_gesture, "update",
                    G_CALLBACK (pika_display_shell_rotate_gesture_update),
                    shell);
  g_signal_connect (shell->rotate_gesture, "end",
                    G_CALLBACK (pika_display_shell_rotate_gesture_end),
                    shell);

  /*  the zoom button  */
  shell->zoom_button = g_object_new (GTK_TYPE_CHECK_BUTTON,
                                     "draw-indicator", FALSE,
                                     "relief",         GTK_RELIEF_NONE,
                                     "width-request",  18,
                                     "height-request", 18,
                                     NULL);
  gtk_widget_set_can_focus (shell->zoom_button, FALSE);
  gtk_image = gtk_image_new_from_icon_name (PIKA_ICON_ZOOM_FOLLOW_WINDOW,
                                            GTK_ICON_SIZE_MENU);
  gtk_container_add (GTK_CONTAINER (shell->zoom_button), gtk_image);
  gtk_widget_show (gtk_image);

  g_signal_connect_swapped (shell->zoom_button, "toggled",
                            G_CALLBACK (pika_display_shell_zoom_button_callback),
                            shell);

  pika_help_set_help_data (shell->zoom_button,
                           _("Zoom image when window size changes"),
                           PIKA_HELP_IMAGE_WINDOW_ZOOM_FOLLOW_BUTTON);

  /*  the quick mask button  */
  shell->quick_mask_button = g_object_new (GTK_TYPE_CHECK_BUTTON,
                                           "draw-indicator", FALSE,
                                           "relief",         GTK_RELIEF_NONE,
                                           "width-request",  18,
                                           "height-request", 18,
                                           NULL);
  gtk_widget_set_can_focus (shell->quick_mask_button, FALSE);
  gtk_image = gtk_image_new_from_icon_name (PIKA_ICON_QUICK_MASK_OFF,
                                            GTK_ICON_SIZE_MENU);
  gtk_container_add (GTK_CONTAINER (shell->quick_mask_button), gtk_image);
  gtk_widget_show (gtk_image);

  g_signal_connect (shell->quick_mask_button, "toggled",
                    G_CALLBACK (pika_display_shell_quick_mask_toggled),
                    shell);
  g_signal_connect (shell->quick_mask_button, "button-press-event",
                    G_CALLBACK (pika_display_shell_quick_mask_button_press),
                    shell);

  action = pika_ui_manager_find_action (shell->popup_manager,
                                        "quick-mask", "quick-mask-toggle");
  if (action)
    pika_widget_set_accel_help (shell->quick_mask_button, action);
  else
    pika_help_set_help_data (shell->quick_mask_button,
                             _("Toggle Quick Mask"),
                             PIKA_HELP_IMAGE_WINDOW_QUICK_MASK_BUTTON);

  /*  the navigation window button  */
  shell->nav_ebox = gtk_event_box_new ();
  gtk_image = gtk_image_new_from_icon_name (PIKA_ICON_DIALOG_NAVIGATION,
                                            GTK_ICON_SIZE_MENU);
  gtk_container_add (GTK_CONTAINER (shell->nav_ebox), gtk_image);
  gtk_widget_show (gtk_image);

  g_signal_connect (shell->nav_ebox, "button-press-event",
                    G_CALLBACK (pika_display_shell_navigation_button_press),
                    shell);

  pika_help_set_help_data (shell->nav_ebox,
                           _("Navigate the image display"),
                           PIKA_HELP_IMAGE_WINDOW_NAV_BUTTON);

  /*  the statusbar  */
  shell->statusbar = pika_statusbar_new ();
  pika_statusbar_set_shell (PIKA_STATUSBAR (shell->statusbar), shell);
  pika_help_set_help_data (shell->statusbar, NULL,
                           PIKA_HELP_IMAGE_WINDOW_STATUS_BAR);

  /*  pack all the widgets  */
  gtk_grid_attach (GTK_GRID (grid), shell->origin, 0, 0, 1, 1);

  gtk_widget_set_hexpand (shell->hrule, TRUE);
  gtk_grid_attach (GTK_GRID (grid), shell->hrule, 1, 0, 1, 1);

  gtk_widget_set_vexpand (shell->vrule, TRUE);
  gtk_grid_attach (GTK_GRID (grid), shell->vrule, 0, 1, 1, 1);

  gtk_widget_set_hexpand (shell->canvas, TRUE);
  gtk_widget_set_vexpand (shell->canvas, TRUE);
  gtk_grid_attach (GTK_GRID (grid), shell->canvas, 1, 1, 1, 1);

  gtk_grid_attach (GTK_GRID (grid), shell->zoom_button, 2, 0, 1, 1);
  gtk_grid_attach (GTK_GRID (grid), shell->quick_mask_button, 0, 2, 1, 1);

  gtk_grid_attach (GTK_GRID (grid), shell->vsb, 2, 1, 1, 1);
  gtk_grid_attach (GTK_GRID (grid), shell->hsb, 1, 2, 1, 1);

  gtk_grid_attach (GTK_GRID (grid), shell->nav_ebox, 2, 2, 1, 1);

  gtk_widget_set_hexpand (shell->statusbar, TRUE);
  gtk_grid_attach (GTK_GRID (grid), shell->statusbar, 0, 3, 3, 1);

  /*  show everything that is always shown */
  gtk_widget_show (GTK_WIDGET (shell->canvas));

  if (image)
    {
      pika_display_shell_connect (shell);

      /* After connecting to the image we want to center it. Since we
       * not even finished creating the display shell, we can safely
       * assume we will get a size-allocate later.
       */
      shell->size_allocate_center_image = TRUE;
    }
  else
    {
#if 0
      /* Disabled because it sets GDK_POINTER_MOTION_HINT on
       * shell->canvas. For info see Bug 677375
       */
      pika_help_set_help_data (shell->canvas,
                               _("Drop image files here to open them"),
                               NULL);
#endif

      pika_statusbar_empty (PIKA_STATUSBAR (shell->statusbar));
    }

  /* make sure the information is up-to-date */
  pika_display_shell_scale_update (shell);

  pika_display_shell_set_show_all (shell, config->default_show_all);
}

static void
pika_display_shell_dispose (GObject *object)
{
  PikaDisplayShell *shell = PIKA_DISPLAY_SHELL (object);

  if (shell->display && pika_display_get_shell (shell->display))
    pika_display_shell_disconnect (shell);

  shell->popup_manager = NULL;

  if (shell->selection)
    pika_display_shell_selection_free (shell);

  pika_display_shell_filter_set (shell, NULL);

  if (shell->filter_idle_id)
    {
      g_source_remove (shell->filter_idle_id);
      shell->filter_idle_id = 0;
    }

  g_clear_object (&shell->zoom_gesture);
  g_clear_object (&shell->rotate_gesture);

  g_clear_pointer (&shell->render_cache,       cairo_surface_destroy);
  g_clear_pointer (&shell->render_cache_valid, cairo_region_destroy);

  g_clear_pointer (&shell->render_surface, cairo_surface_destroy);
  g_clear_pointer (&shell->mask_surface,   cairo_surface_destroy);
  g_clear_pointer (&shell->checkerboard,   cairo_pattern_destroy);

  pika_display_shell_profile_finalize (shell);

  g_clear_object (&shell->filter_buffer);
  shell->filter_data   = NULL;
  shell->filter_stride = 0;

  g_clear_object (&shell->mask);

  pika_display_shell_items_free (shell);

  g_clear_object (&shell->motion_buffer);

  if (shell->zoom_focus_point)
    {
      g_slice_free (GdkPoint, shell->zoom_focus_point);
      shell->zoom_focus_point = NULL;
    }

  if (shell->title_idle_id)
    {
      g_source_remove (shell->title_idle_id);
      shell->title_idle_id = 0;
    }

  if (shell->fill_idle_id)
    {
      g_source_remove (shell->fill_idle_id);
      shell->fill_idle_id = 0;
    }

  g_clear_pointer (&shell->nav_popup, gtk_widget_destroy);

  if (shell->blink_timeout_id)
    {
      g_source_remove (shell->blink_timeout_id);
      shell->blink_timeout_id = 0;
    }

  shell->display = NULL;

  if (shell->window_handle != NULL)
    {
#ifdef GDK_WINDOWING_WAYLAND
      if (GDK_IS_WAYLAND_DISPLAY (gdk_display_get_default ()) &&
          /* The GdkWindow is likely already destroyed. */
          gtk_widget_get_window (GTK_WIDGET (shell)) != NULL)
        gdk_wayland_window_unexport_handle (gtk_widget_get_window (GTK_WIDGET (shell)));
#endif
      g_clear_pointer (&shell->window_handle, g_bytes_unref);
    }

  G_OBJECT_CLASS (parent_class)->dispose (object);
}

static void
pika_display_shell_finalize (GObject *object)
{
  PikaDisplayShell *shell = PIKA_DISPLAY_SHELL (object);

  g_clear_object (&shell->zoom);
  g_clear_pointer (&shell->rotate_transform,   g_free);
  g_clear_pointer (&shell->rotate_untransform, g_free);
  g_clear_object (&shell->options);
  g_clear_object (&shell->fullscreen_options);
  g_clear_object (&shell->no_image_options);
  g_clear_pointer (&shell->title,  g_free);
  g_clear_pointer (&shell->status, g_free);

  G_OBJECT_CLASS (parent_class)->finalize (object);
}

static void
pika_display_shell_set_property (GObject      *object,
                                 guint         property_id,
                                 const GValue *value,
                                 GParamSpec   *pspec)
{
  PikaDisplayShell *shell = PIKA_DISPLAY_SHELL (object);

  switch (property_id)
    {
    case PROP_POPUP_MANAGER:
      shell->popup_manager = g_value_get_object (value);
      break;
    case PROP_INITIAL_MONITOR:
      shell->initial_monitor = g_value_get_object (value);
      break;
    case PROP_DISPLAY:
      shell->display = g_value_get_object (value);
      break;
    case PROP_UNIT:
      pika_display_shell_set_unit (shell, g_value_get_int (value));
      break;
    case PROP_TITLE:
      g_free (shell->title);
      shell->title = g_value_dup_string (value);
      break;
    case PROP_STATUS:
      g_free (shell->status);
      shell->status = g_value_dup_string (value);
      break;
    case PROP_SHOW_ALL:
      pika_display_shell_set_show_all (shell, g_value_get_boolean (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
pika_display_shell_get_property (GObject    *object,
                                 guint       property_id,
                                 GValue     *value,
                                 GParamSpec *pspec)
{
  PikaDisplayShell *shell = PIKA_DISPLAY_SHELL (object);

  switch (property_id)
    {
    case PROP_POPUP_MANAGER:
      g_value_set_object (value, shell->popup_manager);
      break;
    case PROP_INITIAL_MONITOR:
      g_value_set_object (value, shell->initial_monitor);
      break;
    case PROP_DISPLAY:
      g_value_set_object (value, shell->display);
      break;
    case PROP_UNIT:
      g_value_set_int (value, shell->unit);
      break;
    case PROP_TITLE:
      g_value_set_string (value, shell->title);
      break;
    case PROP_STATUS:
      g_value_set_string (value, shell->status);
      break;
    case PROP_SHOW_ALL:
      g_value_set_boolean (value, shell->show_all);
      break;
    case PROP_INFINITE_CANVAS:
      g_value_set_boolean (value,
                           pika_display_shell_get_infinite_canvas (shell));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
pika_display_shell_unrealize (GtkWidget *widget)
{
  PikaDisplayShell *shell = PIKA_DISPLAY_SHELL (widget);

  if (shell->nav_popup)
    gtk_widget_unrealize (shell->nav_popup);

  GTK_WIDGET_CLASS (parent_class)->unrealize (widget);
}

static void
pika_display_shell_unmap (GtkWidget *widget)
{
  PikaDisplayShell *shell = PIKA_DISPLAY_SHELL (widget);

  pika_display_shell_selection_undraw (shell);

  GTK_WIDGET_CLASS (parent_class)->unmap (widget);
}

static void
pika_display_shell_screen_changed (GtkWidget *widget,
                                   GdkScreen *previous)
{
  PikaDisplayShell *shell = PIKA_DISPLAY_SHELL (widget);

  if (GTK_WIDGET_CLASS (parent_class)->screen_changed)
    GTK_WIDGET_CLASS (parent_class)->screen_changed (widget, previous);

  if (shell->display->config->monitor_res_from_gdk)
    {
      pika_get_monitor_resolution (pika_widget_get_monitor (widget),
                                   &shell->monitor_xres,
                                   &shell->monitor_yres);
    }
  else
    {
      shell->monitor_xres = shell->display->config->monitor_xres;
      shell->monitor_yres = shell->display->config->monitor_yres;
    }
}

static gboolean
pika_display_shell_popup_menu (GtkWidget *widget)
{
  PikaDisplayShell *shell = PIKA_DISPLAY_SHELL (widget);

  pika_context_set_display (pika_get_user_context (shell->display->pika),
                            shell->display);

  pika_ui_manager_ui_popup_at_widget (shell->popup_manager,
                                      "/image-menubar",
                                      NULL, NULL,
                                      shell->origin,
                                      GDK_GRAVITY_EAST,
                                      GDK_GRAVITY_NORTH_WEST,
                                      NULL,
                                      NULL, NULL);

  return TRUE;
}

static void
pika_display_shell_real_scaled (PikaDisplayShell *shell)
{
  PikaContext *user_context;

  if (! shell->display)
    return;

  pika_display_shell_title_update (shell);

  user_context = pika_get_user_context (shell->display->pika);

  if (shell->display == pika_context_get_display (user_context))
    {
      pika_display_shell_update_priority_rect (shell);

      pika_ui_manager_update (shell->popup_manager, shell->display);
    }
}

static void
pika_display_shell_real_scrolled (PikaDisplayShell *shell)
{
  PikaContext *user_context;

  if (! shell->display)
    return;

  pika_display_shell_title_update (shell);

  user_context = pika_get_user_context (shell->display->pika);

  if (shell->display == pika_context_get_display (user_context))
    {
      pika_display_shell_update_priority_rect (shell);

    }
}

static void
pika_display_shell_real_rotated (PikaDisplayShell *shell)
{
  PikaContext *user_context;

  if (! shell->display)
    return;

  pika_display_shell_title_update (shell);

  user_context = pika_get_user_context (shell->display->pika);

  if (shell->display == pika_context_get_display (user_context))
    {
      pika_display_shell_update_priority_rect (shell);

      pika_ui_manager_update (shell->popup_manager, shell->display);
    }
}

static const guint8 *
pika_display_shell_get_icc_profile (PikaColorManaged *managed,
                                    gsize            *len)
{
  PikaDisplayShell *shell = PIKA_DISPLAY_SHELL (managed);
  PikaImage        *image = pika_display_get_image (shell->display);

  if (image)
    return pika_color_managed_get_icc_profile (PIKA_COLOR_MANAGED (image), len);

  return NULL;
}

static PikaColorProfile *
pika_display_shell_get_color_profile (PikaColorManaged *managed)
{
  PikaDisplayShell *shell = PIKA_DISPLAY_SHELL (managed);
  PikaImage        *image = pika_display_get_image (shell->display);

  if (image)
    return pika_color_managed_get_color_profile (PIKA_COLOR_MANAGED (image));

  return NULL;
}

static void
pika_display_shell_profile_changed (PikaColorManaged *managed)
{
  PikaDisplayShell *shell = PIKA_DISPLAY_SHELL (managed);

  pika_display_shell_profile_update (shell);
  pika_display_shell_expose_full (shell);
  pika_display_shell_render_invalidate_full (shell);
}

static void
pika_display_shell_simulation_profile_changed (PikaColorManaged *managed)
{
  PikaDisplayShell *shell = PIKA_DISPLAY_SHELL (managed);

  pika_display_shell_expose_full (shell);
  pika_display_shell_render_invalidate_full (shell);
}

static void
pika_display_shell_simulation_intent_changed (PikaColorManaged *managed)
{
  PikaDisplayShell *shell = PIKA_DISPLAY_SHELL (managed);

  pika_display_shell_expose_full (shell);
  pika_display_shell_render_invalidate_full (shell);
}

static void
pika_display_shell_simulation_bpc_changed (PikaColorManaged *managed)
{
  PikaDisplayShell *shell = PIKA_DISPLAY_SHELL (managed);

  pika_display_shell_expose_full (shell);
  pika_display_shell_render_invalidate_full (shell);
}

static void
pika_display_shell_zoom_button_callback (PikaDisplayShell *shell,
                                         GtkWidget        *zoom_button)
{
  shell->zoom_on_resize =
    gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (zoom_button));

  if (shell->zoom_on_resize &&
      pika_display_shell_scale_image_is_within_viewport (shell, NULL, NULL))
    {
      /* Implicitly make a View -> Fit Image in Window */
      pika_display_shell_scale_fit_in (shell);
    }
}

static void
pika_display_shell_sync_config (PikaDisplayShell  *shell,
                                PikaDisplayConfig *config)
{
  pika_config_sync (G_OBJECT (config->default_view),
                    G_OBJECT (shell->options), 0);
  pika_config_sync (G_OBJECT (config->default_fullscreen_view),
                    G_OBJECT (shell->fullscreen_options), 0);
}

static void
pika_display_shell_overlay_allocate (GtkWidget               *child,
                                     GtkAllocation           *allocation,
                                     PikaDisplayShellOverlay *overlay)
{
  gdouble x, y;

  pika_display_shell_transform_overlay (overlay->shell, child, &x, &y);

  pika_overlay_box_set_child_position (PIKA_OVERLAY_BOX (overlay->shell->canvas),
                                       child, x, y);
}

static void
pika_display_shell_remove_overlay (GtkWidget        *canvas,
                                   GtkWidget        *child,
                                   PikaDisplayShell *shell)
{
  PikaDisplayShellOverlay *overlay;

  overlay = g_object_get_data (G_OBJECT (child), "image-coords-overlay");

  g_signal_handlers_disconnect_by_func (child,
                                        pika_display_shell_overlay_allocate,
                                        overlay);

  shell->children = g_list_remove (shell->children, child);
}

static void
pika_display_shell_transform_overlay (PikaDisplayShell *shell,
                                      GtkWidget        *child,
                                      gdouble          *x,
                                      gdouble          *y)
{
  PikaDisplayShellOverlay *overlay;
  GtkRequisition           requisition;

  overlay = g_object_get_data (G_OBJECT (child), "image-coords-overlay");

  pika_display_shell_transform_xy_f (shell,
                                     overlay->image_x,
                                     overlay->image_y,
                                     x, y);

  gtk_widget_get_preferred_size (child, &requisition, NULL);

  switch (overlay->anchor)
    {
    case PIKA_HANDLE_ANCHOR_CENTER:
      *x -= requisition.width  / 2;
      *y -= requisition.height / 2;
      break;

    case PIKA_HANDLE_ANCHOR_NORTH:
      *x -= requisition.width / 2;
      *y += overlay->spacing_y;
      break;

    case PIKA_HANDLE_ANCHOR_NORTH_WEST:
      *x += overlay->spacing_x;
      *y += overlay->spacing_y;
      break;

    case PIKA_HANDLE_ANCHOR_NORTH_EAST:
      *x -= requisition.width + overlay->spacing_x;
      *y += overlay->spacing_y;
      break;

    case PIKA_HANDLE_ANCHOR_SOUTH:
      *x -= requisition.width / 2;
      *y -= requisition.height + overlay->spacing_y;
      break;

    case PIKA_HANDLE_ANCHOR_SOUTH_WEST:
      *x += overlay->spacing_x;
      *y -= requisition.height + overlay->spacing_y;
      break;

    case PIKA_HANDLE_ANCHOR_SOUTH_EAST:
      *x -= requisition.width + overlay->spacing_x;
      *y -= requisition.height + overlay->spacing_y;
      break;

    case PIKA_HANDLE_ANCHOR_WEST:
      *x += overlay->spacing_x;
      *y -= requisition.height / 2;
      break;

    case PIKA_HANDLE_ANCHOR_EAST:
      *x -= requisition.width + overlay->spacing_x;
      *y -= requisition.height / 2;
      break;
    }
}


/*  public functions  */

GtkWidget *
pika_display_shell_new (PikaDisplay   *display,
                        PikaUnit       unit,
                        gdouble        scale,
                        PikaUIManager *popup_manager,
                        GdkMonitor    *monitor)
{
  g_return_val_if_fail (PIKA_IS_DISPLAY (display), NULL);
  g_return_val_if_fail (PIKA_IS_UI_MANAGER (popup_manager), NULL);
  g_return_val_if_fail (GDK_IS_MONITOR (monitor), NULL);

  return g_object_new (PIKA_TYPE_DISPLAY_SHELL,
                       "popup-manager",   popup_manager,
                       "initial-monitor", monitor,
                       "display",         display,
                       "unit",            unit,
                       NULL);
}

void
pika_display_shell_add_overlay (PikaDisplayShell *shell,
                                GtkWidget        *child,
                                gdouble           image_x,
                                gdouble           image_y,
                                PikaHandleAnchor  anchor,
                                gint              spacing_x,
                                gint              spacing_y)
{
  PikaDisplayShellOverlay *overlay;
  gdouble                  x, y;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
  g_return_if_fail (GTK_IS_WIDGET (shell));

  overlay = g_new0 (PikaDisplayShellOverlay, 1);

  overlay->shell     = shell;
  overlay->image_x   = image_x;
  overlay->image_y   = image_y;
  overlay->anchor    = anchor;
  overlay->spacing_x = spacing_x;
  overlay->spacing_y = spacing_y;

  g_object_set_data_full (G_OBJECT (child), "image-coords-overlay", overlay,
                          (GDestroyNotify) g_free);

  shell->children = g_list_prepend (shell->children, child);

  g_signal_connect (child, "size-allocate",
                    G_CALLBACK (pika_display_shell_overlay_allocate),
                    overlay);

  pika_display_shell_transform_overlay (shell, child, &x, &y);

  pika_overlay_box_add_child (PIKA_OVERLAY_BOX (shell->canvas), child, 0.0, 0.0);
  pika_overlay_box_set_child_position (PIKA_OVERLAY_BOX (shell->canvas),
                                       child, x, y);
}

void
pika_display_shell_move_overlay (PikaDisplayShell *shell,
                                 GtkWidget        *child,
                                 gdouble           image_x,
                                 gdouble           image_y,
                                 PikaHandleAnchor  anchor,
                                 gint              spacing_x,
                                 gint              spacing_y)
{
  PikaDisplayShellOverlay *overlay;
  gdouble                  x, y;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
  g_return_if_fail (GTK_IS_WIDGET (shell));

  overlay = g_object_get_data (G_OBJECT (child), "image-coords-overlay");

  g_return_if_fail (overlay != NULL);

  overlay->image_x   = image_x;
  overlay->image_y   = image_y;
  overlay->anchor    = anchor;
  overlay->spacing_x = spacing_x;
  overlay->spacing_y = spacing_y;

  pika_display_shell_transform_overlay (shell, child, &x, &y);

  pika_overlay_box_set_child_position (PIKA_OVERLAY_BOX (shell->canvas),
                                       child, x, y);
}

PikaImageWindow *
pika_display_shell_get_window (PikaDisplayShell *shell)
{
  g_return_val_if_fail (PIKA_IS_DISPLAY_SHELL (shell), NULL);

  return PIKA_IMAGE_WINDOW (gtk_widget_get_ancestor (GTK_WIDGET (shell),
                                                     PIKA_TYPE_IMAGE_WINDOW));
}

PikaStatusbar *
pika_display_shell_get_statusbar (PikaDisplayShell *shell)
{
  g_return_val_if_fail (PIKA_IS_DISPLAY_SHELL (shell), NULL);

  return PIKA_STATUSBAR (shell->statusbar);
}

PikaColorConfig *
pika_display_shell_get_color_config (PikaDisplayShell *shell)
{
  g_return_val_if_fail (PIKA_IS_DISPLAY_SHELL (shell), NULL);

  return shell->color_config;
}

void
pika_display_shell_present (PikaDisplayShell *shell)
{
  PikaImageWindow *window;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  window = pika_display_shell_get_window (shell);

  if (window)
    {
      pika_image_window_set_active_shell (window, shell);

      gtk_window_present (GTK_WINDOW (window));
    }
}

void
pika_display_shell_reconnect (PikaDisplayShell *shell)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
  g_return_if_fail (PIKA_IS_DISPLAY (shell->display));
  g_return_if_fail (pika_display_get_image (shell->display) != NULL);

  if (shell->fill_idle_id)
    {
      g_source_remove (shell->fill_idle_id);
      shell->fill_idle_id = 0;
    }

  g_signal_emit (shell, display_shell_signals[RECONNECT], 0);

  pika_color_managed_profile_changed (PIKA_COLOR_MANAGED (shell));
  pika_display_shell_simulation_profile_changed (PIKA_COLOR_MANAGED (shell));
  pika_display_shell_simulation_intent_changed (PIKA_COLOR_MANAGED (shell));
  pika_display_shell_simulation_bpc_changed (PIKA_COLOR_MANAGED (shell));

  pika_display_shell_scroll_clamp_and_update (shell);

  pika_display_shell_scaled (shell);

  pika_display_shell_expose_full (shell);
  pika_display_shell_render_invalidate_full (shell);
}

static gboolean
pika_display_shell_blink (PikaDisplayShell *shell)
{
  shell->blink_timeout_id = 0;

  if (shell->blink)
    {
      shell->blink = FALSE;
    }
  else
    {
      shell->blink = TRUE;

      shell->blink_timeout_id =
        g_timeout_add (100, (GSourceFunc) pika_display_shell_blink, shell);
    }

  pika_display_shell_expose_full (shell);

  return FALSE;
}

void
pika_display_shell_empty (PikaDisplayShell *shell)
{
  PikaContext     *user_context;
  PikaImageWindow *window;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
  g_return_if_fail (PIKA_IS_DISPLAY (shell->display));
  g_return_if_fail (pika_display_get_image (shell->display) == NULL);

  window = pika_display_shell_get_window (shell);

  if (shell->fill_idle_id)
    {
      g_source_remove (shell->fill_idle_id);
      shell->fill_idle_id = 0;
    }

  pika_display_shell_selection_undraw (shell);

  pika_display_shell_unset_cursor (shell);

  pika_display_shell_filter_set (shell, NULL);

  pika_display_shell_sync_config (shell, shell->display->config);

  pika_display_shell_appearance_update (shell);
  pika_image_window_update_tabs (window);
#if 0
  pika_help_set_help_data (shell->canvas,
                           _("Drop image files here to open them"), NULL);
#endif

  pika_statusbar_empty (PIKA_STATUSBAR (shell->statusbar));

  shell->flip_horizontally = FALSE;
  shell->flip_vertically   = FALSE;
  shell->rotate_angle      = 0.0;
  pika_display_shell_rotate_update_transform (shell);

  pika_display_shell_expose_full (shell);
  pika_display_shell_render_invalidate_full (shell);

  user_context = pika_get_user_context (shell->display->pika);

  if (shell->display == pika_context_get_display (user_context))
    pika_ui_manager_update (shell->popup_manager, shell->display);

  shell->blink_timeout_id =
    g_timeout_add (1403230, (GSourceFunc) pika_display_shell_blink, shell);
}

static gboolean
pika_display_shell_fill_idle (PikaDisplayShell *shell)
{
  GtkWidget *toplevel = gtk_widget_get_toplevel (GTK_WIDGET (shell));

  shell->fill_idle_id = 0;

  if (GTK_IS_WINDOW (toplevel))
    {
      pika_display_shell_scale_shrink_wrap (shell, TRUE);

      gtk_window_present (GTK_WINDOW (toplevel));
    }

  return FALSE;
}

void
pika_display_shell_fill (PikaDisplayShell *shell,
                         PikaImage        *image,
                         PikaUnit          unit,
                         gdouble           scale)
{
  PikaDisplayConfig *config;
  PikaImageWindow   *window;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
  g_return_if_fail (PIKA_IS_DISPLAY (shell->display));
  g_return_if_fail (PIKA_IS_IMAGE (image));

  config = shell->display->config;
  window = pika_display_shell_get_window (shell);

  shell->show_image  = TRUE;

  shell->dot_for_dot = config->default_dot_for_dot;

  pika_display_shell_set_unit (shell, unit);
  pika_display_shell_set_initial_scale (shell, scale, NULL, NULL);
  pika_display_shell_scale_update (shell);

  pika_display_shell_sync_config (shell, config);

  pika_image_window_suspend_keep_pos (window);
  pika_display_shell_appearance_update (shell);
  pika_image_window_resume_keep_pos (window);

  pika_image_window_update_tabs (window);
#if 0
  pika_help_set_help_data (shell->canvas, NULL, NULL);
#endif

  pika_statusbar_fill (PIKA_STATUSBAR (shell->statusbar));

  /* make sure a size-allocate always occurs, even when the rulers and
   * scrollbars are hidden.  see issue #4968.
   */
  shell->size_allocate_center_image = TRUE;
  gtk_widget_queue_resize (GTK_WIDGET (shell->canvas));

  if (shell->blink_timeout_id)
    {
      g_source_remove (shell->blink_timeout_id);
      shell->blink_timeout_id = 0;
    }

  shell->fill_idle_id =
    g_idle_add_full (PIKA_PRIORITY_DISPLAY_SHELL_FILL_IDLE,
                     (GSourceFunc) pika_display_shell_fill_idle, shell,
                     NULL);

  pika_display_shell_set_show_all (shell, config->default_show_all);
}

void
pika_display_shell_scaled (PikaDisplayShell *shell)
{
  GList *list;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  pika_display_shell_rotate_update_transform (shell);

  for (list = shell->children; list; list = g_list_next (list))
    {
      GtkWidget *child = list->data;
      gdouble    x, y;

      pika_display_shell_transform_overlay (shell, child, &x, &y);

      pika_overlay_box_set_child_position (PIKA_OVERLAY_BOX (shell->canvas),
                                           child, x, y);
    }

  g_signal_emit (shell, display_shell_signals[SCALED], 0);
}

void
pika_display_shell_scrolled (PikaDisplayShell *shell)
{
  GList *list;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  pika_display_shell_rotate_update_transform (shell);

  for (list = shell->children; list; list = g_list_next (list))
    {
      GtkWidget *child = list->data;
      gdouble    x, y;

      pika_display_shell_transform_overlay (shell, child, &x, &y);

      pika_overlay_box_set_child_position (PIKA_OVERLAY_BOX (shell->canvas),
                                           child, x, y);
    }

  g_signal_emit (shell, display_shell_signals[SCROLLED], 0);
}

void
pika_display_shell_rotated (PikaDisplayShell *shell)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  pika_display_shell_rotate_update_transform (shell);

  g_signal_emit (shell, display_shell_signals[ROTATED], 0);
}

void
pika_display_shell_set_unit (PikaDisplayShell *shell,
                             PikaUnit          unit)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  if (shell->unit != unit)
    {
      shell->unit = unit;

      pika_display_shell_rulers_update (shell);

      pika_display_shell_scaled (shell);

      g_object_notify (G_OBJECT (shell), "unit");
    }
}

PikaUnit
pika_display_shell_get_unit (PikaDisplayShell *shell)
{
  g_return_val_if_fail (PIKA_IS_DISPLAY_SHELL (shell), PIKA_UNIT_PIXEL);

  return shell->unit;
}

gboolean
pika_display_shell_snap_coords (PikaDisplayShell *shell,
                                PikaCoords       *coords,
                                gint              snap_offset_x,
                                gint              snap_offset_y,
                                gint              snap_width,
                                gint              snap_height)
{
  PikaImageSnapData snapping_data =
    {
      PIKA_ARRANGE_HFILL,
      NULL,
      PIKA_ARRANGE_HFILL,
      NULL,
      PIKA_ARRANGE_HFILL,
      NULL,
      NULL,
      PIKA_ARRANGE_HFILL,
      NULL,
      NULL
    };
  PikaImage *image;
  gboolean   snap_to_guides       = FALSE;
  gboolean   snap_to_grid         = FALSE;
  gboolean   snap_to_canvas       = FALSE;
  gboolean   snap_to_vectors      = FALSE;
  gboolean   snap_to_bbox         = FALSE;
  gboolean   snap_to_equidistance = FALSE;
  gboolean   snapped              = FALSE;

  g_return_val_if_fail (PIKA_IS_DISPLAY_SHELL (shell), FALSE);
  g_return_val_if_fail (coords != NULL, FALSE);

  image = pika_display_get_image (shell->display);

  if (pika_display_shell_get_snap_to_guides (shell) &&
      pika_image_get_guides (image))
    {
      snap_to_guides = TRUE;
    }

  if (pika_display_shell_get_snap_to_grid (shell) &&
      pika_image_get_grid (image))
    {
      snap_to_grid = TRUE;
    }

  snap_to_canvas = pika_display_shell_get_snap_to_canvas (shell);

  if (pika_display_shell_get_snap_to_vectors (shell) &&
      pika_image_get_selected_vectors (image))
    {
      snap_to_vectors = TRUE;
    }

  if (pika_display_shell_get_snap_to_bbox (shell))
    {
      snap_to_bbox = TRUE;
    }

  shell->snapped_side_horizontal = PIKA_ARRANGE_HFILL;
  shell->snapped_side_vertical = PIKA_ARRANGE_HFILL;

  if (pika_display_shell_get_snap_to_equidistance (shell))
    {
      snap_to_equidistance = TRUE;
    }

  shell->equidistance_side_horizontal = PIKA_ARRANGE_HFILL;
  shell->equidistance_side_vertical = PIKA_ARRANGE_HFILL;

  if (snap_to_guides || snap_to_grid || snap_to_canvas || snap_to_vectors || snap_to_bbox || snap_to_equidistance)
    {
      gint    snap_distance;
      gdouble tx, ty;

      snap_distance = shell->display->config->snap_distance;

      if (snap_width > 0 && snap_height > 0)
        {
          snapped = pika_image_snap_rectangle (image,
                                               &snapping_data,
                                               coords->x + snap_offset_x,
                                               coords->y + snap_offset_y,
                                               coords->x + snap_offset_x +
                                               snap_width,
                                               coords->y + snap_offset_y +
                                               snap_height,
                                               &tx,
                                               &ty,
                                               FUNSCALEX (shell, snap_distance),
                                               FUNSCALEY (shell, snap_distance),
                                               snap_to_guides,
                                               snap_to_grid,
                                               snap_to_canvas,
                                               snap_to_vectors,
                                               snap_to_bbox,
                                               snap_to_equidistance);

          shell->snapped_side_horizontal = snapping_data.snapped_side_horizontal;
          shell->snapped_layer_horizontal = snapping_data.snapped_layer_horizontal;
          shell->snapped_side_vertical = snapping_data.snapped_side_vertical;
          shell->snapped_layer_vertical = snapping_data.snapped_layer_vertical;
          shell->equidistance_side_horizontal = snapping_data.equidistance_side_horizontal;
          shell->equidistance_side_vertical = snapping_data.equidistance_side_vertical;
          shell->near_layer_horizontal1 = snapping_data.near_layer_horizontal1;
          shell->near_layer_horizontal2 = snapping_data.near_layer_horizontal2;
          shell->near_layer_vertical1 = snapping_data.near_layer_vertical1;
          shell->near_layer_vertical2 = snapping_data.near_layer_vertical2;
        }
      else
        {
          snapped = pika_image_snap_point (image,
                                           coords->x + snap_offset_x,
                                           coords->y + snap_offset_y,
                                           &tx,
                                           &ty,
                                           FUNSCALEX (shell, snap_distance),
                                           FUNSCALEY (shell, snap_distance),
                                           snap_to_guides,
                                           snap_to_grid,
                                           snap_to_canvas,
                                           snap_to_vectors,
                                           snap_to_bbox,
                                           shell->show_all);
        }

      if (snapped)
        {
          coords->x = tx - snap_offset_x;
          coords->y = ty - snap_offset_y;
        }
    }

  return snapped;
}

gboolean
pika_display_shell_mask_bounds (PikaDisplayShell *shell,
                                gint             *x,
                                gint             *y,
                                gint             *width,
                                gint             *height)
{
  PikaImage *image;
  PikaLayer *layer;
  gint       x1, y1;
  gint       x2, y2;
  gdouble    x1_f, y1_f;
  gdouble    x2_f, y2_f;

  g_return_val_if_fail (PIKA_IS_DISPLAY_SHELL (shell), FALSE);
  g_return_val_if_fail (x != NULL, FALSE);
  g_return_val_if_fail (y != NULL, FALSE);
  g_return_val_if_fail (width != NULL, FALSE);
  g_return_val_if_fail (height != NULL, FALSE);

  image = pika_display_get_image (shell->display);

  /*  If there is a floating selection, handle things differently  */
  if ((layer = pika_image_get_floating_selection (image)))
    {
      gint fs_x;
      gint fs_y;
      gint fs_width;
      gint fs_height;

      pika_item_get_offset (PIKA_ITEM (layer), &fs_x, &fs_y);
      fs_width  = pika_item_get_width  (PIKA_ITEM (layer));
      fs_height = pika_item_get_height (PIKA_ITEM (layer));

      if (! pika_item_bounds (PIKA_ITEM (pika_image_get_mask (image)),
                              x, y, width, height))
        {
          *x      = fs_x;
          *y      = fs_y;
          *width  = fs_width;
          *height = fs_height;
        }
      else
        {
          pika_rectangle_union (*x, *y, *width, *height,
                                fs_x, fs_y, fs_width, fs_height,
                                x, y, width, height);
        }
    }
  else if (! pika_item_bounds (PIKA_ITEM (pika_image_get_mask (image)),
                               x, y, width, height))
    {
      return FALSE;
    }

  x1 = *x;
  y1 = *y;
  x2 = *x + *width;
  y2 = *y + *height;

  pika_display_shell_transform_bounds (shell,
                                       x1, y1, x2, y2,
                                       &x1_f, &y1_f, &x2_f, &y2_f);

  /*  Make sure the extents are within bounds  */
  x1 = CLAMP (floor (x1_f), 0, shell->disp_width);
  y1 = CLAMP (floor (y1_f), 0, shell->disp_height);
  x2 = CLAMP (ceil (x2_f),  0, shell->disp_width);
  y2 = CLAMP (ceil (y2_f),  0, shell->disp_height);

  *x      = x1;
  *y      = y1;
  *width  = x2 - x1;
  *height = y2 - y1;

  return (*width > 0) && (*height > 0);
}

void
pika_display_shell_set_show_image (PikaDisplayShell *shell,
                                   gboolean          show_image)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  if (show_image != shell->show_image)
    {
      shell->show_image = show_image;

      pika_display_shell_expose_full (shell);
    }
}

void
pika_display_shell_set_show_all (PikaDisplayShell *shell,
                                 gboolean          show_all)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  if (show_all != shell->show_all)
    {
      shell->show_all = show_all;

      if (shell->display && pika_display_get_image (shell->display))
        {
          PikaImage   *image = pika_display_get_image (shell->display);
          PikaContext *user_context;

          if (show_all)
            pika_image_inc_show_all_count (image);
          else
            pika_image_dec_show_all_count (image);

          pika_image_flush (image);

          pika_display_update_bounding_box (shell->display);

          pika_display_shell_update_show_canvas (shell);

          pika_display_shell_scroll_clamp_and_update (shell);
          pika_display_shell_scrollbars_update (shell);

          pika_display_shell_expose_full (shell);

          user_context = pika_get_user_context (shell->display->pika);

          if (shell->display == pika_context_get_display (user_context))
            {
              pika_display_shell_update_priority_rect (shell);

              pika_ui_manager_update (shell->popup_manager, shell->display);
            }
        }

      g_object_notify (G_OBJECT (shell), "show-all");
      g_object_notify (G_OBJECT (shell), "infinite-canvas");
    }
}


PikaPickable *
pika_display_shell_get_pickable (PikaDisplayShell *shell)
{
  PikaImage *image;

  g_return_val_if_fail (PIKA_IS_DISPLAY_SHELL (shell), NULL);

  image = pika_display_get_image (shell->display);

  if (image)
    {
      if (! shell->show_all)
        return PIKA_PICKABLE (image);
      else
        return PIKA_PICKABLE (pika_image_get_projection (image));
    }

  return NULL;
}

PikaPickable *
pika_display_shell_get_canvas_pickable (PikaDisplayShell *shell)
{
  PikaImage *image;

  g_return_val_if_fail (PIKA_IS_DISPLAY_SHELL (shell), NULL);

  image = pika_display_get_image (shell->display);

  if (image)
    {
      if (! pika_display_shell_get_infinite_canvas (shell))
        return PIKA_PICKABLE (image);
      else
        return PIKA_PICKABLE (pika_image_get_projection (image));
    }

  return NULL;
}

GeglRectangle
pika_display_shell_get_bounding_box (PikaDisplayShell *shell)
{
  GeglRectangle  bounding_box = {};
  PikaImage     *image;

  g_return_val_if_fail (PIKA_IS_DISPLAY_SHELL (shell), bounding_box);

  image = pika_display_get_image (shell->display);

  if (image)
    {
      if (! shell->show_all)
        {
          bounding_box.width  = pika_image_get_width  (image);
          bounding_box.height = pika_image_get_height (image);
        }
      else
        {
          bounding_box = pika_projectable_get_bounding_box (
            PIKA_PROJECTABLE (image));
        }
    }

  return bounding_box;
}

gboolean
pika_display_shell_get_infinite_canvas (PikaDisplayShell *shell)
{
  g_return_val_if_fail (PIKA_IS_DISPLAY_SHELL (shell), FALSE);

  return shell->show_all &&
         ! pika_display_shell_get_padding_in_show_all (shell);
}

void
pika_display_shell_update_priority_rect (PikaDisplayShell *shell)
{
  PikaImage *image;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  image = pika_display_get_image (shell->display);

  if (image)
    {
      PikaProjection *projection = pika_image_get_projection (image);
      gint            x, y;
      gint            width, height;

      pika_display_shell_untransform_viewport (shell, ! shell->show_all,
                                               &x, &y, &width, &height);
      pika_projection_set_priority_rect (projection, x, y, width, height);
    }
}

void
pika_display_shell_flush (PikaDisplayShell *shell)
{
  PikaImageWindow *window;
  PikaContext     *context;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  window = pika_display_shell_get_window (shell);

  pika_display_shell_title_update (shell);

  pika_canvas_layer_boundary_set_layers (PIKA_CANVAS_LAYER_BOUNDARY (shell->layer_boundary),
                                         pika_image_get_selected_layers (pika_display_get_image (shell->display)));

  pika_canvas_canvas_boundary_set_image (PIKA_CANVAS_CANVAS_BOUNDARY (shell->canvas_boundary),
                                         pika_display_get_image (shell->display));

  if (window && pika_image_window_get_active_shell (window) == shell)
    {
      PikaUIManager *manager = menus_get_image_manager_singleton (shell->display->pika);

      pika_ui_manager_update (manager, shell->display);
    }

  context = pika_get_user_context (shell->display->pika);

  if (shell->display == pika_context_get_display (context))
    {
      pika_ui_manager_update (shell->popup_manager, shell->display);
    }
}

/**
 * pika_display_shell_pause:
 * @shell: a display shell
 *
 * This function increments the pause count or the display shell.
 * If it was zero coming in, then the function pauses the active tool,
 * so that operations on the display can take place without corrupting
 * anything that the tool has drawn.  It "undraws" the current tool
 * drawing, and must be followed by pika_display_shell_resume() after
 * the operation in question is completed.
 **/
void
pika_display_shell_pause (PikaDisplayShell *shell)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  shell->paused_count++;

  if (shell->paused_count == 1)
    {
      /*  pause the currently active tool  */
      tool_manager_control_active (shell->display->pika,
                                   PIKA_TOOL_ACTION_PAUSE,
                                   shell->display);
    }
}

/**
 * pika_display_shell_resume:
 * @shell: a display shell
 *
 * This function decrements the pause count for the display shell.
 * If this brings it to zero, then the current tool is resumed.
 * It is an error to call this function without having previously
 * called pika_display_shell_pause().
 **/
void
pika_display_shell_resume (PikaDisplayShell *shell)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
  g_return_if_fail (shell->paused_count > 0);

  shell->paused_count--;

  if (shell->paused_count == 0)
    {
      /* start the currently active tool */
      tool_manager_control_active (shell->display->pika,
                                   PIKA_TOOL_ACTION_RESUME,
                                   shell->display);
    }
}

/**
 * pika_display_shell_set_highlight:
 * @shell:     a #PikaDisplayShell
 * @highlight: a rectangle in image coordinates that should be brought out
 * @opacity:   how much to hide the unselected area
 *
 * This function sets an area of the image that should be
 * accentuated. The actual implementation is to dim all pixels outside
 * this rectangle. Passing %NULL for @highlight unsets the rectangle.
 **/
void
pika_display_shell_set_highlight (PikaDisplayShell   *shell,
                                  const GdkRectangle *highlight,
                                  gdouble             opacity)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  if (highlight)
    {
      pika_canvas_item_begin_change (shell->passe_partout);

      pika_canvas_rectangle_set (shell->passe_partout,
                                 highlight->x,
                                 highlight->y,
                                 highlight->width,
                                 highlight->height);
      g_object_set (shell->passe_partout, "opacity", opacity, NULL);

      pika_canvas_item_set_visible (shell->passe_partout, TRUE);

      pika_canvas_item_end_change (shell->passe_partout);
    }
  else
    {
      pika_canvas_item_set_visible (shell->passe_partout, FALSE);
    }
}

/**
 * pika_display_shell_set_mask:
 * @shell: a #PikaDisplayShell
 * @mask:  a #PikaDrawable (1 byte per pixel)
 * @color: the color to use for drawing the mask
 * @inverted: %TRUE if the mask should be drawn inverted
 *
 * Previews a mask originating at offset_x, offset_x. Depending on
 * @inverted, pixels that are selected or not selected are tinted with
 * the given color.
 **/
void
pika_display_shell_set_mask (PikaDisplayShell *shell,
                             GeglBuffer       *mask,
                             gint              offset_x,
                             gint              offset_y,
                             const PikaRGB    *color,
                             gboolean          inverted)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
  g_return_if_fail (mask == NULL || GEGL_IS_BUFFER (mask));
  g_return_if_fail (mask == NULL || color != NULL);

  if (mask)
    g_object_ref (mask);

  if (shell->mask)
    g_object_unref (shell->mask);

  shell->mask = mask;

  shell->mask_offset_x = offset_x;
  shell->mask_offset_y = offset_y;

  if (mask)
    shell->mask_color = *color;

  shell->mask_inverted = inverted;

  pika_display_shell_expose_full (shell);
  pika_display_shell_render_invalidate_full (shell);
}
