/* 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 <gegl.h>
#include <gtk/gtk.h>

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

#include "actions-types.h"

#include "config/pikadialogconfig.h"

#include "gegl/pika-babl.h"

#include "core/core-enums.h"
#include "core/pika.h"
#include "core/pikacontext.h"
#include "core/pikaimage.h"
#include "core/pikaimage-color-profile.h"
#include "core/pikaimage-convert-indexed.h"
#include "core/pikaimage-convert-precision.h"
#include "core/pikaimage-convert-type.h"
#include "core/pikaimage-crop.h"
#include "core/pikaimage-duplicate.h"
#include "core/pikaimage-flip.h"
#include "core/pikaimage-merge.h"
#include "core/pikaimage-resize.h"
#include "core/pikaimage-rotate.h"
#include "core/pikaimage-scale.h"
#include "core/pikaimage-undo.h"
#include "core/pikaitem.h"
#include "core/pikapickable.h"
#include "core/pikapickable-auto-shrink.h"
#include "core/pikaprogress.h"

#include "widgets/pikadialogfactory.h"
#include "widgets/pikadock.h"
#include "widgets/pikahelp-ids.h"
#include "widgets/pikawidgets-utils.h"

#include "display/pikadisplay.h"
#include "display/pikadisplayshell.h"

#include "dialogs/dialogs.h"
#include "dialogs/color-profile-dialog.h"
#include "dialogs/convert-indexed-dialog.h"
#include "dialogs/convert-precision-dialog.h"
#include "dialogs/grid-dialog.h"
#include "dialogs/image-merge-layers-dialog.h"
#include "dialogs/image-new-dialog.h"
#include "dialogs/image-properties-dialog.h"
#include "dialogs/image-scale-dialog.h"
#include "dialogs/print-size-dialog.h"
#include "dialogs/resize-dialog.h"

#include "actions.h"
#include "image-commands.h"

#include "pika-intl.h"


/*  local function prototypes  */

static void   image_convert_rgb_callback       (GtkWidget                *dialog,
                                                PikaImage                *image,
                                                PikaColorProfile         *new_profile,
                                                GFile                    *new_file,
                                                PikaColorRenderingIntent  intent,
                                                gboolean                  bpc,
                                                gpointer                  user_data);

static void   image_convert_gray_callback      (GtkWidget                *dialog,
                                                PikaImage                *image,
                                                PikaColorProfile         *new_profile,
                                                GFile                    *new_file,
                                                PikaColorRenderingIntent  intent,
                                                gboolean                  bpc,
                                                gpointer                  user_data);

static void   image_convert_indexed_callback   (GtkWidget              *dialog,
                                                PikaImage              *image,
                                                PikaConvertPaletteType  palette_type,
                                                gint                    max_colors,
                                                gboolean                remove_duplicates,
                                                PikaConvertDitherType   dither_type,
                                                gboolean                dither_alpha,
                                                gboolean                dither_text_layers,
                                                PikaPalette            *custom_palette,
                                                gpointer                user_data);

static void   image_convert_precision_callback (GtkWidget              *dialog,
                                                PikaImage              *image,
                                                PikaPrecision           precision,
                                                GeglDitherMethod        layer_dither_method,
                                                GeglDitherMethod        text_layer_dither_method,
                                                GeglDitherMethod        mask_dither_method,
                                                gpointer                user_data);

static void   image_profile_assign_callback    (GtkWidget                *dialog,
                                                PikaImage                *image,
                                                PikaColorProfile         *new_profile,
                                                GFile                    *new_file,
                                                PikaColorRenderingIntent  intent,
                                                gboolean                  bpc,
                                                gpointer                  user_data);

static void   image_profile_convert_callback   (GtkWidget                *dialog,
                                                PikaImage                *image,
                                                PikaColorProfile         *new_profile,
                                                GFile                    *new_file,
                                                PikaColorRenderingIntent  intent,
                                                gboolean                  bpc,
                                                gpointer                  user_data);

static void   image_resize_callback            (GtkWidget              *dialog,
                                                PikaViewable           *viewable,
                                                PikaContext            *context,
                                                gint                    width,
                                                gint                    height,
                                                PikaUnit                unit,
                                                gint                    offset_x,
                                                gint                    offset_y,
                                                gdouble                 xres,
                                                gdouble                 yres,
                                                PikaUnit                res_unit,
                                                PikaFillType            fill_type,
                                                PikaItemSet             layer_set,
                                                gboolean                resize_text_layers,
                                                gpointer                user_data);

static void   image_print_size_callback        (GtkWidget              *dialog,
                                                PikaImage              *image,
                                                gdouble                 xresolution,
                                                gdouble                 yresolution,
                                                PikaUnit                resolution_unit,
                                                gpointer                user_data);

static void   image_scale_callback             (GtkWidget              *dialog,
                                                PikaViewable           *viewable,
                                                gint                    width,
                                                gint                    height,
                                                PikaUnit                unit,
                                                PikaInterpolationType   interpolation,
                                                gdouble                 xresolution,
                                                gdouble                 yresolution,
                                                PikaUnit                resolution_unit,
                                                gpointer                user_data);

static void   image_merge_layers_callback      (GtkWidget              *dialog,
                                                PikaImage              *image,
                                                PikaContext            *context,
                                                PikaMergeType           merge_type,
                                                gboolean                merge_active_group,
                                                gboolean                discard_invisible,
                                                gpointer                user_data);

static void   image_softproof_profile_callback  (GtkWidget                *dialog,
                                                 PikaImage                *image,
                                                 PikaColorProfile         *new_profile,
                                                 GFile                    *new_file,
                                                 PikaColorRenderingIntent  intent,
                                                 gboolean                  bpc,
                                                 gpointer                  user_data);



/*  private variables  */

static PikaUnit               image_resize_unit  = PIKA_UNIT_PIXEL;
static PikaUnit               image_scale_unit   = PIKA_UNIT_PIXEL;
static PikaInterpolationType  image_scale_interp = -1;
static PikaPalette           *image_convert_indexed_custom_palette = NULL;


/*  public functions  */

void
image_new_cmd_callback (PikaAction *action,
                        GVariant   *value,
                        gpointer    data)
{
  GtkWidget *widget;
  GtkWidget *dialog;
  return_if_no_widget (widget, data);

  dialog = pika_dialog_factory_dialog_new (pika_dialog_factory_get_singleton (),
                                           pika_widget_get_monitor (widget),
                                           NULL /*ui_manager*/,
                                           widget,
                                           "pika-image-new-dialog", -1, FALSE);

  if (dialog)
    {
      PikaImage *image = action_data_get_image (data);

      image_new_dialog_set (dialog, image, NULL);

      gtk_window_present (GTK_WINDOW (dialog));
    }
}

void
image_duplicate_cmd_callback (PikaAction *action,
                              GVariant   *value,
                              gpointer    data)
{
  PikaDisplay      *display;
  PikaImage        *image;
  PikaDisplayShell *shell;
  PikaImage        *new_image;
  return_if_no_display (display, data);

  image = pika_display_get_image (display);
  shell = pika_display_get_shell (display);

  new_image = pika_image_duplicate (image);

  pika_create_display (new_image->pika, new_image, shell->unit,
                       pika_zoom_model_get_factor (shell->zoom),
                       G_OBJECT (pika_widget_get_monitor (GTK_WIDGET (shell))));

  g_object_unref (new_image);
}

void
image_convert_base_type_cmd_callback (PikaAction *action,
                                      GVariant   *value,
                                      gpointer    data)
{
  PikaImage         *image;
  PikaDisplay       *display;
  GtkWidget         *widget;
  PikaDialogConfig  *config;
  GtkWidget         *dialog;
  PikaImageBaseType  base_type;
  GError            *error = NULL;
  return_if_no_image (image, data);
  return_if_no_display (display, data);
  return_if_no_widget (widget, data);

  base_type = (PikaImageBaseType) g_variant_get_int32 (value);

  if (base_type == pika_image_get_base_type (image))
    return;

#define CONVERT_TYPE_DIALOG_KEY "pika-convert-type-dialog"

  dialog = dialogs_get_dialog (G_OBJECT (image), CONVERT_TYPE_DIALOG_KEY);

  if (dialog)
    {
      gtk_widget_destroy (dialog);
      dialog = NULL;
    }

  config = PIKA_DIALOG_CONFIG (image->pika->config);

  switch (base_type)
    {
    case PIKA_RGB:
    case PIKA_GRAY:
      if (pika_image_get_color_profile (image))
        {
          ColorProfileDialogType    dialog_type;
          PikaColorProfileCallback  callback;
          PikaColorProfile         *current_profile;
          PikaColorProfile         *default_profile;
          PikaTRCType               trc;

          current_profile =
            pika_color_managed_get_color_profile (PIKA_COLOR_MANAGED (image));

          trc = pika_babl_trc (pika_image_get_precision (image));

          if (base_type == PIKA_RGB)
            {
              dialog_type = COLOR_PROFILE_DIALOG_CONVERT_TO_RGB;
              callback    = image_convert_rgb_callback;

              default_profile = pika_babl_get_builtin_color_profile (PIKA_RGB,
                                                                     trc);
            }
          else
            {
              dialog_type = COLOR_PROFILE_DIALOG_CONVERT_TO_GRAY;
              callback    = image_convert_gray_callback;

              default_profile = pika_babl_get_builtin_color_profile (PIKA_GRAY,
                                                                     trc);
            }

          dialog = color_profile_dialog_new (dialog_type,
                                             image,
                                             action_data_get_context (data),
                                             widget,
                                             current_profile,
                                             default_profile,
                                             0, 0,
                                             callback,
                                             display);
        }
      else if (! pika_image_convert_type (image, base_type, NULL, NULL, &error))
        {
          pika_message_literal (image->pika,
                                G_OBJECT (widget), PIKA_MESSAGE_WARNING,
                                error->message);
          g_clear_error (&error);
        }
      break;

    case PIKA_INDEXED:
      dialog = convert_indexed_dialog_new (image,
                                           action_data_get_context (data),
                                           widget,
                                           config->image_convert_indexed_palette_type,
                                           config->image_convert_indexed_max_colors,
                                           config->image_convert_indexed_remove_duplicates,
                                           config->image_convert_indexed_dither_type,
                                           config->image_convert_indexed_dither_alpha,
                                           config->image_convert_indexed_dither_text_layers,
                                           image_convert_indexed_custom_palette,
                                           image_convert_indexed_callback,
                                           display);
      break;
    }

  if (dialog)
    {
      dialogs_attach_dialog (G_OBJECT (image),
                             CONVERT_TYPE_DIALOG_KEY, dialog);
      gtk_window_present (GTK_WINDOW (dialog));
    }

  /*  always flush, also when only the indexed dialog was shown, so
   *  the menu items get updated back to the current image type
   */
  pika_image_flush (image);
}

void
image_convert_precision_cmd_callback (PikaAction *action,
                                      GVariant   *value,
                                      gpointer    data)
{
  PikaImage         *image;
  PikaDisplay       *display;
  GtkWidget         *widget;
  PikaDialogConfig  *config;
  GtkWidget         *dialog;
  PikaComponentType  component_type;
  return_if_no_image (image, data);
  return_if_no_display (display, data);
  return_if_no_widget (widget, data);

  component_type = (PikaComponentType) g_variant_get_int32 (value);

  if (component_type == pika_image_get_component_type (image))
    return;

#define CONVERT_PRECISION_DIALOG_KEY "pika-convert-precision-dialog"

  dialog = dialogs_get_dialog (G_OBJECT (image), CONVERT_PRECISION_DIALOG_KEY);

  if (dialog)
    {
      gtk_widget_destroy (dialog);
      dialog = NULL;
    }

  config = PIKA_DIALOG_CONFIG (image->pika->config);

  dialog = convert_precision_dialog_new (image,
                                         action_data_get_context (data),
                                         widget,
                                         component_type,
                                         config->image_convert_precision_layer_dither_method,
                                         config->image_convert_precision_text_layer_dither_method,
                                         config->image_convert_precision_channel_dither_method,
                                         image_convert_precision_callback,
                                         display);

  dialogs_attach_dialog (G_OBJECT (image),
                         CONVERT_PRECISION_DIALOG_KEY, dialog);

  gtk_window_present (GTK_WINDOW (dialog));

  /*  see comment above  */
  pika_image_flush (image);
}

void
image_convert_trc_cmd_callback (PikaAction *action,
                                GVariant   *value,
                                gpointer    data)
{
  PikaImage     *image;
  PikaDisplay   *display;
  PikaTRCType    trc_type;
  PikaPrecision  precision;
  return_if_no_image (image, data);
  return_if_no_display (display, data);

  trc_type = (PikaTRCType) g_variant_get_int32 (value);

  if (trc_type == pika_babl_format_get_trc (pika_image_get_layer_format (image,
                                                                         FALSE)))
    return;

  precision = pika_babl_precision (pika_image_get_component_type (image),
                                   trc_type);

  pika_image_convert_precision (image, precision,
                                GEGL_DITHER_NONE,
                                GEGL_DITHER_NONE,
                                GEGL_DITHER_NONE,
                                PIKA_PROGRESS (display));
  pika_image_flush (image);
}

void
image_color_profile_use_srgb_cmd_callback (PikaAction *action,
                                           GVariant   *value,
                                           gpointer    data)
{
  PikaImage *image;
  gboolean   use_srgb;
  return_if_no_image (image, data);

  use_srgb = g_variant_get_boolean (value);

  if (use_srgb != pika_image_get_use_srgb_profile (image, NULL))
    {
      pika_image_set_use_srgb_profile (image, use_srgb);
      pika_image_flush (image);
    }
}

void
image_color_profile_assign_cmd_callback (PikaAction *action,
                                         GVariant   *value,
                                         gpointer    data)
{
  PikaImage   *image;
  PikaDisplay *display;
  GtkWidget   *widget;
  GtkWidget   *dialog;
  return_if_no_image (image, data);
  return_if_no_display (display, data);
  return_if_no_widget (widget, data);

#define PROFILE_ASSIGN_DIALOG_KEY "pika-profile-assign-dialog"

  dialog = dialogs_get_dialog (G_OBJECT (image), PROFILE_ASSIGN_DIALOG_KEY);

  if (! dialog)
    {
      PikaColorProfile *current_profile;
      PikaColorProfile *default_profile;

      current_profile = pika_color_managed_get_color_profile (PIKA_COLOR_MANAGED (image));
      default_profile = pika_image_get_builtin_color_profile (image);

      dialog = color_profile_dialog_new (COLOR_PROFILE_DIALOG_ASSIGN_PROFILE,
                                         image,
                                         action_data_get_context (data),
                                         widget,
                                         current_profile,
                                         default_profile,
                                         0, 0,
                                         image_profile_assign_callback,
                                         display);

      dialogs_attach_dialog (G_OBJECT (image),
                             PROFILE_ASSIGN_DIALOG_KEY, dialog);
    }

  gtk_window_present (GTK_WINDOW (dialog));
}

void
image_color_profile_convert_cmd_callback (PikaAction *action,
                                          GVariant   *value,
                                          gpointer    data)
{
  PikaImage   *image;
  PikaDisplay *display;
  GtkWidget   *widget;
  GtkWidget   *dialog;
  return_if_no_image (image, data);
  return_if_no_display (display, data);
  return_if_no_widget (widget, data);

#define PROFILE_CONVERT_DIALOG_KEY "pika-profile-convert-dialog"

  dialog = dialogs_get_dialog (G_OBJECT (image), PROFILE_CONVERT_DIALOG_KEY);

  if (! dialog)
    {
      PikaDialogConfig *config = PIKA_DIALOG_CONFIG (image->pika->config);
      PikaColorProfile *current_profile;
      PikaColorProfile *default_profile;

      current_profile = pika_color_managed_get_color_profile (PIKA_COLOR_MANAGED (image));
      default_profile = pika_image_get_builtin_color_profile (image);

      dialog = color_profile_dialog_new (COLOR_PROFILE_DIALOG_CONVERT_TO_PROFILE,
                                         image,
                                         action_data_get_context (data),
                                         widget,
                                         current_profile,
                                         default_profile,
                                         config->image_convert_profile_intent,
                                         config->image_convert_profile_bpc,
                                         image_profile_convert_callback,
                                         display);

      dialogs_attach_dialog (G_OBJECT (image),
                             PROFILE_CONVERT_DIALOG_KEY, dialog);
    }

  gtk_window_present (GTK_WINDOW (dialog));
}

void
image_color_profile_discard_cmd_callback (PikaAction *action,
                                          GVariant   *value,
                                          gpointer    data)
{
  PikaImage *image;
  return_if_no_image (image, data);

  pika_image_assign_color_profile (image, NULL, NULL, NULL);
  pika_image_flush (image);
}

static void
image_profile_save_dialog_response (GtkWidget *dialog,
                                    gint       response_id,
                                    PikaImage *image)
{
  if (response_id == GTK_RESPONSE_ACCEPT)
    {
      PikaColorProfile *profile;
      GFile            *file;
      GError           *error = NULL;

      profile = pika_color_managed_get_color_profile (PIKA_COLOR_MANAGED (image));
      file    = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));

      if (! file)
        return;

      if (! pika_color_profile_save_to_file (profile, file, &error))
        {
          pika_message (image->pika, NULL,
                        PIKA_MESSAGE_WARNING,
                        _("Saving color profile failed: %s"),
                        error->message);
          g_clear_error (&error);
          g_object_unref (file);
          return;
        }

      g_object_unref (file);
    }

  gtk_widget_destroy (dialog);
}

void
image_color_profile_save_cmd_callback (PikaAction *action,
                                       GVariant   *value,
                                       gpointer    data)
{
  PikaImage   *image;
  PikaDisplay *display;
  GtkWidget   *widget;
  GtkWidget   *dialog;
  return_if_no_image (image, data);
  return_if_no_display (display, data);
  return_if_no_widget (widget, data);

#define PROFILE_SAVE_DIALOG_KEY "pika-profile-save-dialog"

  dialog = dialogs_get_dialog (G_OBJECT (image), PROFILE_SAVE_DIALOG_KEY);

  if (! dialog)
    {
      GtkWindow        *toplevel;
      PikaColorProfile *profile;
      gchar            *basename;

      toplevel = GTK_WINDOW (gtk_widget_get_toplevel (widget));
      profile  = pika_color_managed_get_color_profile (PIKA_COLOR_MANAGED (image));

      dialog =
        pika_color_profile_chooser_dialog_new (_("Save Color Profile"),
                                               toplevel,
                                               GTK_FILE_CHOOSER_ACTION_SAVE);

      pika_color_profile_chooser_dialog_connect_path (dialog,
                                                      G_OBJECT (image->pika->config),
                                                      "color-profile-path");

      basename = g_strconcat (pika_color_profile_get_label (profile),
                              ".icc", NULL);
      gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (dialog), basename);
      g_free (basename);

      g_signal_connect (dialog, "response",
                        G_CALLBACK (image_profile_save_dialog_response),
                        image);

      dialogs_attach_dialog (G_OBJECT (image), PROFILE_SAVE_DIALOG_KEY, dialog);
    }

  gtk_window_present (GTK_WINDOW (dialog));
}

void
image_resize_cmd_callback (PikaAction *action,
                           GVariant   *value,
                           gpointer    data)
{
  PikaImage   *image;
  GtkWidget   *widget;
  PikaDisplay *display;
  GtkWidget   *dialog;
  return_if_no_image (image, data);
  return_if_no_widget (widget, data);
  return_if_no_display (display, data);

#define RESIZE_DIALOG_KEY "pika-resize-dialog"

  dialog = dialogs_get_dialog (G_OBJECT (image), RESIZE_DIALOG_KEY);

  if (! dialog)
    {
      PikaDialogConfig *config = PIKA_DIALOG_CONFIG (image->pika->config);

      if (image_resize_unit != PIKA_UNIT_PERCENT)
        image_resize_unit = pika_display_get_shell (display)->unit;

      dialog = resize_dialog_new (PIKA_VIEWABLE (image),
                                  action_data_get_context (data),
                                  _("Set Image Canvas Size"),
                                  "pika-image-resize",
                                  widget,
                                  pika_standard_help_func,
                                  PIKA_HELP_IMAGE_RESIZE,
                                  image_resize_unit,
                                  config->image_resize_fill_type,
                                  config->image_resize_layer_set,
                                  config->image_resize_resize_text_layers,
                                  image_resize_callback,
                                  display);

      dialogs_attach_dialog (G_OBJECT (image), RESIZE_DIALOG_KEY, dialog);
    }

  gtk_window_present (GTK_WINDOW (dialog));
}

void
image_resize_to_layers_cmd_callback (PikaAction *action,
                                     GVariant   *value,
                                     gpointer    data)
{
  PikaDisplay  *display;
  PikaImage    *image;
  PikaProgress *progress;
  return_if_no_display (display, data);

  image = pika_display_get_image (display);

  progress = pika_progress_start (PIKA_PROGRESS (display), FALSE,
                                  _("Resizing"));

  pika_image_resize_to_layers (image,
                               action_data_get_context (data),
                               NULL, NULL, NULL, NULL, progress);

  if (progress)
    pika_progress_end (progress);

  pika_image_flush (image);
}

void
image_resize_to_selection_cmd_callback (PikaAction *action,
                                        GVariant   *value,
                                        gpointer    data)
{
  PikaDisplay  *display;
  PikaImage    *image;
  PikaProgress *progress;
  return_if_no_display (display, data);

  image = pika_display_get_image (display);

  progress = pika_progress_start (PIKA_PROGRESS (display), FALSE,
                                  _("Resizing"));

  pika_image_resize_to_selection (image,
                                  action_data_get_context (data),
                                  progress);

  if (progress)
    pika_progress_end (progress);

  pika_image_flush (image);
}

void
image_print_size_cmd_callback (PikaAction *action,
                               GVariant   *value,
                               gpointer    data)
{
  PikaDisplay *display;
  PikaImage   *image;
  GtkWidget   *widget;
  GtkWidget   *dialog;
  return_if_no_display (display, data);
  return_if_no_widget (widget, data);

  image = pika_display_get_image (display);

#define PRINT_SIZE_DIALOG_KEY "pika-print-size-dialog"

  dialog = dialogs_get_dialog (G_OBJECT (image), PRINT_SIZE_DIALOG_KEY);

  if (! dialog)
    {
      dialog = print_size_dialog_new (image,
                                      action_data_get_context (data),
                                      _("Set Image Print Resolution"),
                                      "pika-image-print-size",
                                      widget,
                                      pika_standard_help_func,
                                      PIKA_HELP_IMAGE_PRINT_SIZE,
                                      image_print_size_callback,
                                      NULL);

      dialogs_attach_dialog (G_OBJECT (image), PRINT_SIZE_DIALOG_KEY, dialog);
    }

  gtk_window_present (GTK_WINDOW (dialog));
}

void
image_scale_cmd_callback (PikaAction *action,
                          GVariant   *value,
                          gpointer    data)
{
  PikaDisplay *display;
  PikaImage   *image;
  GtkWidget   *widget;
  GtkWidget   *dialog;
  return_if_no_display (display, data);
  return_if_no_widget (widget, data);

  image = pika_display_get_image (display);

#define SCALE_DIALOG_KEY "pika-scale-dialog"

  dialog = dialogs_get_dialog (G_OBJECT (image), SCALE_DIALOG_KEY);

  if (! dialog)
    {
      if (image_scale_unit != PIKA_UNIT_PERCENT)
        image_scale_unit = pika_display_get_shell (display)->unit;

      if (image_scale_interp == -1)
        image_scale_interp = display->pika->config->interpolation_type;

      dialog = image_scale_dialog_new (image,
                                       action_data_get_context (data),
                                       widget,
                                       image_scale_unit,
                                       image_scale_interp,
                                       image_scale_callback,
                                       display);

      dialogs_attach_dialog (G_OBJECT (image), SCALE_DIALOG_KEY, dialog);
    }

  gtk_window_present (GTK_WINDOW (dialog));
}

void
image_flip_cmd_callback (PikaAction *action,
                         GVariant   *value,
                         gpointer    data)
{
  PikaDisplay         *display;
  PikaImage           *image;
  PikaProgress        *progress;
  PikaOrientationType  orientation;
  return_if_no_display (display, data);

  orientation = (PikaOrientationType) g_variant_get_int32 (value);

  image = pika_display_get_image (display);

  progress = pika_progress_start (PIKA_PROGRESS (display), FALSE,
                                  _("Flipping"));

  pika_image_flip (image, action_data_get_context (data),
                   orientation, progress);

  if (progress)
    pika_progress_end (progress);

  pika_image_flush (image);
}

void
image_rotate_cmd_callback (PikaAction *action,
                           GVariant   *value,
                           gpointer    data)
{
  PikaDisplay      *display;
  PikaImage        *image;
  PikaProgress     *progress;
  PikaRotationType  rotation;
  return_if_no_display (display, data);

  rotation = (PikaRotationType) g_variant_get_int32 (value);

  image = pika_display_get_image (display);

  progress = pika_progress_start (PIKA_PROGRESS (display), FALSE,
                                  _("Rotating"));

  pika_image_rotate (image, action_data_get_context (data),
                     rotation, progress);

  if (progress)
    pika_progress_end (progress);

  pika_image_flush (image);
}

void
image_crop_to_selection_cmd_callback (PikaAction *action,
                                      GVariant   *value,
                                      gpointer    data)
{
  PikaImage *image;
  GtkWidget *widget;
  gint       x, y;
  gint       width, height;
  return_if_no_image (image, data);
  return_if_no_widget (widget, data);

  if (! pika_item_bounds (PIKA_ITEM (pika_image_get_mask (image)),
                          &x, &y, &width, &height))
    {
      pika_message_literal (image->pika,
                            G_OBJECT (widget), PIKA_MESSAGE_WARNING,
                            _("Cannot crop because the current selection "
                              "is empty."));
      return;
    }

  pika_image_crop (image,
                   action_data_get_context (data), PIKA_FILL_TRANSPARENT,
                   x, y, width, height, TRUE);
  pika_image_flush (image);
}

void
image_crop_to_content_cmd_callback (PikaAction *action,
                                    GVariant   *value,
                                    gpointer    data)
{
  PikaImage *image;
  GtkWidget *widget;
  gint       x, y;
  gint       width, height;
  return_if_no_image (image, data);
  return_if_no_widget (widget, data);

  switch (pika_pickable_auto_shrink (PIKA_PICKABLE (image),
                                     0, 0,
                                     pika_image_get_width  (image),
                                     pika_image_get_height (image),
                                     &x, &y, &width, &height))
    {
    case PIKA_AUTO_SHRINK_SHRINK:
      pika_image_crop (image,
                       action_data_get_context (data), PIKA_FILL_TRANSPARENT,
                       x, y, width, height, TRUE);
      pika_image_flush (image);
      break;

    case PIKA_AUTO_SHRINK_EMPTY:
      pika_message_literal (image->pika,
                            G_OBJECT (widget), PIKA_MESSAGE_INFO,
                            _("Cannot crop because the image has no content."));
      break;

    case PIKA_AUTO_SHRINK_UNSHRINKABLE:
      pika_message_literal (image->pika,
                            G_OBJECT (widget), PIKA_MESSAGE_INFO,
                            _("Cannot crop because the image is already "
                              "cropped to its content."));
      break;
    }
}

void
image_merge_layers_cmd_callback (PikaAction *action,
                                 GVariant   *value,
                                 gpointer    data)
{
  GtkWidget   *dialog;
  PikaImage   *image;
  PikaDisplay *display;
  GtkWidget   *widget;
  return_if_no_image (image, data);
  return_if_no_display (display, data);
  return_if_no_widget (widget, data);

#define MERGE_LAYERS_DIALOG_KEY "pika-merge-layers-dialog"

  dialog = dialogs_get_dialog (G_OBJECT (image), MERGE_LAYERS_DIALOG_KEY);

  if (! dialog)
    {
      PikaDialogConfig *config = PIKA_DIALOG_CONFIG (image->pika->config);

      dialog = image_merge_layers_dialog_new (image,
                                              action_data_get_context (data),
                                              widget,
                                              config->layer_merge_type,
                                              config->layer_merge_active_group_only,
                                              config->layer_merge_discard_invisible,
                                              image_merge_layers_callback,
                                              display);

      dialogs_attach_dialog (G_OBJECT (image), MERGE_LAYERS_DIALOG_KEY, dialog);
    }

  gtk_window_present (GTK_WINDOW (dialog));
}

void
image_merge_layers_last_vals_cmd_callback (PikaAction *action,
                                           GVariant   *value,
                                           gpointer    data)
{
  PikaImage        *image;
  PikaDisplay      *display;
  PikaDialogConfig *config;
  return_if_no_image (image, data);
  return_if_no_display (display, data);

  config = PIKA_DIALOG_CONFIG (image->pika->config);

  image_merge_layers_callback (NULL,
                               image,
                               action_data_get_context (data),
                               config->layer_merge_type,
                               config->layer_merge_active_group_only,
                               config->layer_merge_discard_invisible,
                               display);
}

void
image_flatten_image_cmd_callback (PikaAction *action,
                                  GVariant   *value,
                                  gpointer    data)
{
  PikaImage   *image;
  PikaDisplay *display;
  GtkWidget   *widget;
  GError      *error = NULL;
  return_if_no_image (image, data);
  return_if_no_display (display, data);
  return_if_no_widget (widget, data);

  if (! pika_image_flatten (image, action_data_get_context (data),
                            PIKA_PROGRESS (display), &error))
    {
      pika_message_literal (image->pika,
                            G_OBJECT (widget), PIKA_MESSAGE_WARNING,
                            error->message);
      g_clear_error (&error);
      return;
    }

  pika_image_flush (image);
}

void
image_configure_grid_cmd_callback (PikaAction *action,
                                   GVariant   *value,
                                   gpointer    data)
{
  PikaDisplay *display;
  PikaImage   *image;
  GtkWidget   *dialog;
  return_if_no_display (display, data);

  image = pika_display_get_image (display);

#define GRID_DIALOG_KEY "pika-grid-dialog"

  dialog = dialogs_get_dialog (G_OBJECT (image), GRID_DIALOG_KEY);

  if (! dialog)
    {
      PikaDisplayShell *shell = pika_display_get_shell (display);

      dialog = grid_dialog_new (image,
                                action_data_get_context (data),
                                gtk_widget_get_toplevel (GTK_WIDGET (shell)));

      dialogs_attach_dialog (G_OBJECT (image), GRID_DIALOG_KEY, dialog);
    }

  gtk_window_present (GTK_WINDOW (dialog));
}

void
image_properties_cmd_callback (PikaAction *action,
                               GVariant   *value,
                               gpointer    data)
{
  PikaDisplay *display;
  PikaImage   *image;
  GtkWidget   *dialog;
  return_if_no_display (display, data);

  image = pika_display_get_image (display);

#define PROPERTIES_DIALOG_KEY "pika-image-properties-dialog"

  dialog = dialogs_get_dialog (G_OBJECT (image), PROPERTIES_DIALOG_KEY);

  if (! dialog)
    {
      PikaDisplayShell *shell = pika_display_get_shell (display);

      dialog = image_properties_dialog_new (image,
                                            action_data_get_context (data),
                                            gtk_widget_get_toplevel (GTK_WIDGET (shell)));

      dialogs_attach_dialog (G_OBJECT (image), PROPERTIES_DIALOG_KEY, dialog);
    }

  gtk_window_present (GTK_WINDOW (dialog));
}


/*  private functions  */

static void
image_convert_rgb_callback (GtkWidget                *dialog,
                            PikaImage                *image,
                            PikaColorProfile         *new_profile,
                            GFile                    *new_file,
                            PikaColorRenderingIntent  intent,
                            gboolean                  bpc,
                            gpointer                  user_data)
{
  PikaProgress *progress = user_data;
  GError       *error    = NULL;

  progress = pika_progress_start (progress, FALSE,
                                  _("Converting to RGB (%s)"),
                                  pika_color_profile_get_label (new_profile));

  if (! pika_image_convert_type (image, PIKA_RGB, new_profile,
                                 progress, &error))
    {
      pika_message (image->pika, G_OBJECT (dialog),
                    PIKA_MESSAGE_ERROR,
                    "%s", error->message);
      g_clear_error (&error);

      if (progress)
        pika_progress_end (progress);

      return;
    }

  if (progress)
    pika_progress_end (progress);

  pika_image_flush (image);

 gtk_widget_destroy (dialog);
}

static void
image_convert_gray_callback (GtkWidget                *dialog,
                             PikaImage                *image,
                             PikaColorProfile         *new_profile,
                             GFile                    *new_file,
                             PikaColorRenderingIntent  intent,
                             gboolean                  bpc,
                             gpointer                  user_data)
{
  PikaProgress *progress = user_data;
  GError       *error    = NULL;

  progress = pika_progress_start (progress, FALSE,
                                  _("Converting to grayscale (%s)"),
                                  pika_color_profile_get_label (new_profile));

  if (! pika_image_convert_type (image, PIKA_GRAY, new_profile,
                                 progress, &error))
    {
      pika_message (image->pika, G_OBJECT (dialog),
                    PIKA_MESSAGE_ERROR,
                    "%s", error->message);
      g_clear_error (&error);

      if (progress)
        pika_progress_end (progress);

      return;
    }

  if (progress)
    pika_progress_end (progress);

  pika_image_flush (image);

  gtk_widget_destroy (dialog);
}

static void
image_convert_indexed_callback (GtkWidget              *dialog,
                                PikaImage              *image,
                                PikaConvertPaletteType  palette_type,
                                gint                    max_colors,
                                gboolean                remove_duplicates,
                                PikaConvertDitherType   dither_type,
                                gboolean                dither_alpha,
                                gboolean                dither_text_layers,
                                PikaPalette            *custom_palette,
                                gpointer                user_data)
{
  PikaDialogConfig *config  = PIKA_DIALOG_CONFIG (image->pika->config);
  PikaDisplay      *display = user_data;
  PikaProgress     *progress;
  GError           *error   = NULL;

  g_object_set (config,
                "image-convert-indexed-palette-type",       palette_type,
                "image-convert-indexed-max-colors",         max_colors,
                "image-convert-indexed-remove-duplicates",  remove_duplicates,
                "image-convert-indexed-dither-type",        dither_type,
                "image-convert-indexed-dither-alpha",       dither_alpha,
                "image-convert-indexed-dither-text-layers", dither_text_layers,
                NULL);

  g_set_weak_pointer (&image_convert_indexed_custom_palette, custom_palette);

  progress = pika_progress_start (PIKA_PROGRESS (display), FALSE,
                                  _("Converting to indexed colors"));

  if (! pika_image_convert_indexed (image,
                                    config->image_convert_indexed_palette_type,
                                    config->image_convert_indexed_max_colors,
                                    config->image_convert_indexed_remove_duplicates,
                                    config->image_convert_indexed_dither_type,
                                    config->image_convert_indexed_dither_alpha,
                                    config->image_convert_indexed_dither_text_layers,
                                    image_convert_indexed_custom_palette,
                                    progress,
                                    &error))
    {
      pika_message_literal (image->pika, G_OBJECT (display),
                            PIKA_MESSAGE_WARNING, error->message);
      g_clear_error (&error);

      if (progress)
        pika_progress_end (progress);

      return;
    }

  if (progress)
    pika_progress_end (progress);

  pika_image_flush (image);

  gtk_widget_destroy (dialog);
}

static void
image_convert_precision_callback (GtkWidget        *dialog,
                                  PikaImage        *image,
                                  PikaPrecision     precision,
                                  GeglDitherMethod  layer_dither_method,
                                  GeglDitherMethod  text_layer_dither_method,
                                  GeglDitherMethod  channel_dither_method,
                                  gpointer          user_data)
{
  PikaDialogConfig *config   = PIKA_DIALOG_CONFIG (image->pika->config);
  PikaProgress     *progress = user_data;
  const gchar      *enum_desc;
  const Babl       *old_format;
  const Babl       *new_format;
  gint              old_bits;
  gint              new_bits;

  g_object_set (config,
                "image-convert-precision-layer-dither-method",
                layer_dither_method,
                "image-convert-precision-text-layer-dither-method",
                text_layer_dither_method,
                "image-convert-precision-channel-dither-method",
                channel_dither_method,
                NULL);

  /*  we do the same dither method checks here *and* in the dialog,
   *  because the dialog leaves the passed dither methods untouched if
   *  dithering is disabled and passes the original values to the
   *  callback, in order not to change the values saved in
   *  PikaDialogConfig.
   */

  /* random formats with the right precision */
  old_format = pika_image_get_layer_format (image, FALSE);
  new_format = pika_babl_format (PIKA_RGB, precision, FALSE, NULL);

  old_bits = (babl_format_get_bytes_per_pixel (old_format) * 8 /
              babl_format_get_n_components (old_format));
  new_bits = (babl_format_get_bytes_per_pixel (new_format) * 8 /
              babl_format_get_n_components (new_format));

  if (new_bits >= old_bits ||
      new_bits >  CONVERT_PRECISION_DIALOG_MAX_DITHER_BITS)
    {
      /*  don't dither if we are converting to a higher bit depth,
       *  or to more than MAX_DITHER_BITS.
       */
      layer_dither_method      = GEGL_DITHER_NONE;
      text_layer_dither_method = GEGL_DITHER_NONE;
      channel_dither_method    = GEGL_DITHER_NONE;
    }

  pika_enum_get_value (PIKA_TYPE_PRECISION, precision,
                       NULL, NULL, &enum_desc, NULL);

  progress = pika_progress_start (progress, FALSE,
                                  _("Converting image to %s"),
                                  enum_desc);

  pika_image_convert_precision (image,
                                precision,
                                layer_dither_method,
                                text_layer_dither_method,
                                channel_dither_method,
                                progress);

  if (progress)
    pika_progress_end (progress);

  pika_image_flush (image);

  gtk_widget_destroy (dialog);
}

static void
image_profile_assign_callback (GtkWidget                *dialog,
                               PikaImage                *image,
                               PikaColorProfile         *new_profile,
                               GFile                    *new_file,
                               PikaColorRenderingIntent  intent,
                               gboolean                  bpc,
                               gpointer                  user_data)
{
  GError *error = NULL;

  if (! pika_image_assign_color_profile (image, new_profile, NULL, &error))
    {
      pika_message (image->pika, G_OBJECT (dialog),
                    PIKA_MESSAGE_ERROR,
                    "%s", error->message);
      g_clear_error (&error);

      return;
    }

  pika_image_flush (image);

  gtk_widget_destroy (dialog);
}

static void
image_profile_convert_callback (GtkWidget                *dialog,
                                PikaImage                *image,
                                PikaColorProfile         *new_profile,
                                GFile                    *new_file,
                                PikaColorRenderingIntent  intent,
                                gboolean                  bpc,
                                gpointer                  user_data)
{
  PikaDialogConfig *config   = PIKA_DIALOG_CONFIG (image->pika->config);
  PikaProgress     *progress = user_data;
  GError           *error    = NULL;

  g_object_set (config,
                "image-convert-profile-intent",                   intent,
                "image-convert-profile-black-point-compensation", bpc,
                NULL);

  progress = pika_progress_start (progress, FALSE,
                                  _("Converting to '%s'"),
                                  pika_color_profile_get_label (new_profile));

  if (! pika_image_convert_color_profile (image, new_profile,
                                          config->image_convert_profile_intent,
                                          config->image_convert_profile_bpc,
                                          progress, &error))
    {
      pika_message (image->pika, G_OBJECT (dialog),
                    PIKA_MESSAGE_ERROR,
                    "%s", error->message);
      g_clear_error (&error);

      if (progress)
        pika_progress_end (progress);

      return;
    }

  if (progress)
    pika_progress_end (progress);

  pika_image_flush (image);

  gtk_widget_destroy (dialog);
}

static void
image_resize_callback (GtkWidget    *dialog,
                       PikaViewable *viewable,
                       PikaContext  *context,
                       gint          width,
                       gint          height,
                       PikaUnit      unit,
                       gint          offset_x,
                       gint          offset_y,
                       gdouble       xres,
                       gdouble       yres,
                       PikaUnit      res_unit,
                       PikaFillType  fill_type,
                       PikaItemSet   layer_set,
                       gboolean      resize_text_layers,
                       gpointer      user_data)
{
  PikaDisplay *display = user_data;

  image_resize_unit = unit;

  if (width > 0 && height > 0)
    {
      PikaImage        *image  = PIKA_IMAGE (viewable);
      PikaDialogConfig *config = PIKA_DIALOG_CONFIG (image->pika->config);
      PikaProgress     *progress;
      gdouble           old_xres;
      gdouble           old_yres;
      PikaUnit          old_res_unit;
      gboolean          update_resolution;

      g_object_set (config,
                    "image-resize-fill-type",          fill_type,
                    "image-resize-layer-set",          layer_set,
                    "image-resize-resize-text-layers", resize_text_layers,
                    NULL);

      gtk_widget_destroy (dialog);

      if (width  == pika_image_get_width  (image) &&
          height == pika_image_get_height (image))
        return;

      progress = pika_progress_start (PIKA_PROGRESS (display), FALSE,
                                      _("Resizing"));

      pika_image_get_resolution (image, &old_xres, &old_yres);
      old_res_unit = pika_image_get_unit (image);

      update_resolution = xres     != old_xres ||
                          yres     != old_yres ||
                          res_unit != old_res_unit;

      if (update_resolution)
        {
          pika_image_undo_group_start (image,
                                       PIKA_UNDO_GROUP_IMAGE_SCALE,
                                       _("Change Canvas Size"));
          pika_image_set_resolution (image, xres, yres);
          pika_image_set_unit (image, res_unit);
        }

      pika_image_resize_with_layers (image,
                                     context, fill_type,
                                     width, height,
                                     offset_x, offset_y,
                                     layer_set,
                                     resize_text_layers,
                                     progress);

      if (progress)
        pika_progress_end (progress);

      if (update_resolution)
        pika_image_undo_group_end (image);

      pika_image_flush (image);
    }
  else
    {
      g_warning ("Resize Error: "
                 "Both width and height must be greater than zero.");
    }
}

static void
image_print_size_callback (GtkWidget *dialog,
                           PikaImage *image,
                           gdouble    xresolution,
                           gdouble    yresolution,
                           PikaUnit   resolution_unit,
                           gpointer   data)
{
  gdouble xres;
  gdouble yres;

  gtk_widget_destroy (dialog);

  pika_image_get_resolution (image, &xres, &yres);

  if (xresolution     == xres &&
      yresolution     == yres &&
      resolution_unit == pika_image_get_unit (image))
    return;

  pika_image_undo_group_start (image, PIKA_UNDO_GROUP_IMAGE_SCALE,
                               _("Change Print Size"));

  pika_image_set_resolution (image, xresolution, yresolution);
  pika_image_set_unit (image, resolution_unit);

  pika_image_undo_group_end (image);

  pika_image_flush (image);
}

static void
image_scale_callback (GtkWidget              *dialog,
                      PikaViewable           *viewable,
                      gint                    width,
                      gint                    height,
                      PikaUnit                unit,
                      PikaInterpolationType   interpolation,
                      gdouble                 xresolution,
                      gdouble                 yresolution,
                      PikaUnit                resolution_unit,
                      gpointer                user_data)
{
  PikaProgress *progress = user_data;
  PikaImage    *image    = PIKA_IMAGE (viewable);
  gdouble       xres;
  gdouble       yres;

  image_scale_unit   = unit;
  image_scale_interp = interpolation;

  pika_image_get_resolution (image, &xres, &yres);

  if (width > 0 && height > 0)
    {
      gtk_widget_destroy (dialog);

      if (width           == pika_image_get_width  (image) &&
          height          == pika_image_get_height (image) &&
          xresolution     == xres                          &&
          yresolution     == yres                          &&
          resolution_unit == pika_image_get_unit (image))
        return;

      pika_image_undo_group_start (image, PIKA_UNDO_GROUP_IMAGE_SCALE,
                                   _("Scale Image"));

      pika_image_set_resolution (image, xresolution, yresolution);
      pika_image_set_unit (image, resolution_unit);

      if (width  != pika_image_get_width  (image) ||
          height != pika_image_get_height (image))
        {
          progress = pika_progress_start (progress, FALSE,
                                          _("Scaling"));

          pika_image_scale (image, width, height, interpolation, progress);

          if (progress)
            pika_progress_end (progress);
        }

      pika_image_undo_group_end (image);

      pika_image_flush (image);
    }
  else
    {
      g_warning ("Scale Error: "
                 "Both width and height must be greater than zero.");
    }
}

static void
image_merge_layers_callback (GtkWidget     *dialog,
                             PikaImage     *image,
                             PikaContext   *context,
                             PikaMergeType  merge_type,
                             gboolean       merge_active_group,
                             gboolean       discard_invisible,
                             gpointer       user_data)
{
  PikaDialogConfig *config  = PIKA_DIALOG_CONFIG (image->pika->config);
  PikaDisplay      *display = user_data;

  g_object_set (config,
                "layer-merge-type",              merge_type,
                "layer-merge-active-group-only", merge_active_group,
                "layer-merge-discard-invisible", discard_invisible,
                NULL);

  pika_image_merge_visible_layers (image,
                                   context,
                                   config->layer_merge_type,
                                   config->layer_merge_active_group_only,
                                   config->layer_merge_discard_invisible,
                                   PIKA_PROGRESS (display));

  pika_image_flush (image);

  g_clear_pointer (&dialog, gtk_widget_destroy);
}

void
image_softproof_profile_cmd_callback (PikaAction *action,
                                      GVariant   *value,
                                      gpointer    data)
{
  PikaImage        *image;
  PikaDisplayShell *shell;
  GtkWidget        *dialog;
  return_if_no_image (image, data);
  return_if_no_shell (shell, data);

#define SOFTPROOF_PROFILE_DIALOG_KEY "pika-softproof-profile-dialog"

  dialog = dialogs_get_dialog (G_OBJECT (shell), SOFTPROOF_PROFILE_DIALOG_KEY);

  if (! dialog)
    {
      PikaColorProfile *current_profile;

      current_profile = pika_image_get_simulation_profile (image);

      dialog = color_profile_dialog_new (COLOR_PROFILE_DIALOG_SELECT_SOFTPROOF_PROFILE,
                                         image,
                                         action_data_get_context (data),
                                         GTK_WIDGET (shell),
                                         current_profile,
                                         NULL,
                                         0, 0,
                                         image_softproof_profile_callback,
                                         shell);

      dialogs_attach_dialog (G_OBJECT (shell),
                             SOFTPROOF_PROFILE_DIALOG_KEY, dialog);
    }

  gtk_window_present (GTK_WINDOW (dialog));
}

static void
image_softproof_profile_callback (GtkWidget                *dialog,
                                  PikaImage                *image,
                                  PikaColorProfile         *new_profile,
                                  GFile                    *new_file,
                                  PikaColorRenderingIntent  intent,
                                  gboolean                  bpc,
                                  gpointer                  user_data)
{
  PikaDisplayShell *shell = user_data;

  /* Update image's simulation profile */
  pika_image_set_simulation_profile (image, new_profile);
  pika_color_managed_simulation_profile_changed (PIKA_COLOR_MANAGED (shell));

  gtk_widget_destroy (dialog);
}

void
image_softproof_intent_cmd_callback (PikaAction *action,
                                     GVariant   *value,
                                     gpointer    data)
{
  PikaImage                 *image;
  PikaDisplayShell          *shell;
  PikaColorRenderingIntent   intent;
  return_if_no_image (image, data);
  return_if_no_shell (shell, data);

  intent = (PikaColorRenderingIntent) g_variant_get_int32 (value);

  if (intent != pika_image_get_simulation_intent (image))
    {
      pika_image_set_simulation_intent (image, intent);
      shell->color_config_set = TRUE;
      pika_color_managed_simulation_intent_changed (PIKA_COLOR_MANAGED (shell));
    }
}

void
image_softproof_bpc_cmd_callback (PikaAction *action,
                                  GVariant   *value,
                                  gpointer    data)
{
  PikaImage                 *image;
  PikaDisplayShell          *shell;
  gboolean                   bpc;
  return_if_no_image (image, data);
  return_if_no_shell (shell, data);

  bpc = g_variant_get_boolean (value);

  if (bpc != pika_image_get_simulation_bpc (image))
    {
      pika_image_set_simulation_bpc (image, bpc);
      shell->color_config_set = TRUE;
      pika_color_managed_simulation_bpc_changed (PIKA_COLOR_MANAGED (shell));
    }
}
