Ctrl K
ComunidadGuías…

src/libs/print_settings.c

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/*
    This file is part of darktable,
    Copyright (C) 2014-2026 darktable developers.

    darktable 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.

    darktable 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 darktable.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "common/gdk_event_utils.h"

#include <glib.h>

#include "bauhaus/bauhaus.h"
#include "common/collection.h"
#include "common/colorspaces.h"
#include "common/cups_print.h"
#include "common/image_cache.h"
#include "common/metadata.h"
#include "common/pdf.h"
#include "common/printprof.h"
#include "common/printing.h"
#include "common/styles.h"
#include "common/tags.h"
#include "common/variables.h"
#include "control/jobs.h"
#include "dtgtk/resetlabel.h"
#include "dtgtk/stylemenu.h"
#include "gui/accelerators.h"
#include "gui/drag_and_drop.h"
#include "gui/gtk.h"
#include "libs/lib.h"
#include "libs/lib_api.h"

#ifdef _WIN32
  #include "win/win32_print.h"
  #include <windows.h>
  #include <prntvpt.h>   // PTOpenProvider, PTConvertDevModeToPrintTicket
  #include <objidl.h>    // IStream
#endif
#ifdef GDK_WINDOWING_WIN32
  #include <gdk/gdkwin32.h>
#endif


DT_MODULE(4)

#define PRINT_CONFIG_PREFIX "plugins/print/print/"

const char *name(dt_lib_module_t *self)
{
  return _("print settings");
}

dt_view_type_flags_t views(dt_lib_module_t *self)
{
  return DT_VIEW_PRINT;
}

uint32_t container(dt_lib_module_t *self)
{
  return DT_UI_CONTAINER_PANEL_RIGHT_CENTER;
}

typedef enum _set_controls
{
  BOX_LEFT   = 1 << 0,
  BOX_RIGHT  = 1 << 1,
  BOX_TOP    = 1 << 2,
  BOX_BOTTOM = 1 << 3,
  BOX_TOP_LEFT     = BOX_LEFT | BOX_TOP,
  BOX_TOP_RIGHT    = BOX_RIGHT | BOX_TOP,
  BOX_BOTTOM_LEFT  = BOX_LEFT | BOX_BOTTOM,
  BOX_BOTTOM_RIGHT = BOX_RIGHT | BOX_BOTTOM,
  BOX_ALL          = BOX_LEFT | BOX_RIGHT | BOX_TOP | BOX_BOTTOM
} dt_box_control_set;

typedef enum _unit_t
{
  UNIT_MM = 0,
  UNIT_CM,
  UNIT_IN,
  UNIT_N // needs to be the last one
} _unit_t;

static const float units[UNIT_N] = { 1.0f, 0.1f, 1.0f/25.4f };
static const gchar *_unit_names[] = { N_("mm"), N_("cm"), N_("inch"), NULL };

typedef struct dt_lib_print_settings_t
{
  GtkWidget *profile, *intent, *style, *style_mode, *papers, *media;
  GtkWidget *printers, *orientation, *pprofile, *pintent;
  GtkWidget *width, *height, *black_point_compensation;
  GtkWidget *info;
  GtkWidget *b_x, *b_y;
  GtkWidget *b_width, *b_height;
  GtkWidget *del;
  GtkWidget *grid, *grid_size, *snap_grid;
  GtkWidget *borderless;
  GList *profiles;
  GtkButton *print_button;
  GtkToggleButton *lock_button;
  GtkWidget *b_top, *b_bottom, *b_left, *b_right;
  GtkDarktableToggleButton *dtba[9];	             // Alignment buttons
  GList *paper_list, *media_list;
  gboolean lock_activated;
  dt_print_info_t prt;
  dt_images_box imgs;
  _unit_t unit;
  int v_intent, v_pintent;
  int v_icctype, v_picctype;
  char *v_iccprofile, *v_piccprofile, *v_style;
  gboolean v_style_append, v_black_point_compensation;
  gboolean busy;

  // for adding new area
  gboolean creation;
  gboolean dragging;
  float x1, y1, x2, y2;
  int selected;                    // selected area in imgs.box
  int last_selected;               // last selected area to edit
  dt_box_control_set sel_controls; // which border/corner is selected
  float click_pos_x, click_pos_y;
  gboolean has_changed;

  GList *printer_list;
  dt_pthread_mutex_t printer_list_mutex;

  // for windows side printing support
#ifdef _WIN32  
  char *waiting_for_printer;
  GtkButton *print_settings_button; // button to open printer settings (windows only)
  struct dt_win32_print_ctx_t *settings_ctx;
#endif
} dt_lib_print_settings_t;

typedef struct dt_lib_print_job_t
{
  gchar *job_title;
  dt_print_info_t prt;
  gchar* style;
  gboolean style_append, black_point_compensation;
  dt_colorspaces_color_profile_type_t buf_icc_type, p_icc_type;
  gchar *buf_icc_profile, *p_icc_profile;
  dt_iop_color_intent_t buf_icc_intent, p_icc_intent;
  dt_images_box imgs;
  uint16_t *buf; // ??? should be removed
  dt_pdf_page_t *pdf_page;
  char pdf_filename[PATH_MAX];
#ifdef _WIN32
  void *print_ticket_data; // Windows only: print ticket data for the job
  size_t print_ticket_size; // Windows only: size of the print ticket data
  gboolean is_color_device; // Windows only: TRUE if the printer is color-capable
#endif
} dt_lib_print_job_t;

typedef struct dt_lib_export_profile_t
{
  dt_colorspaces_color_profile_type_t type; // filename is only used for type DT_COLORSPACE_FILE
  char filename[512];                       // icc file name
  char name[512];                           // product name
  int  pos, ppos;                           // position in combo boxen
} dt_lib_export_profile_t;

typedef struct _dialog_description
{
  const char *name;
} dialog_description_t;

static void _update_slider(dt_lib_print_settings_t *ps);
static void _width_changed(GtkWidget *widget, gpointer user_data);
static void _height_changed(GtkWidget *widget, gpointer user_data);
static void _x_changed(GtkWidget *widget, gpointer user_data);
static void _y_changed(GtkWidget *widget, gpointer user_data);

int position(const dt_lib_module_t *self)
{
  return 990;
}

/* get paper dimension for the orientation (in mm) */
static void _get_page_dimension(dt_print_info_t *prt,
                                float *width,
                                float *height)
{
  if(prt->page.landscape)
  {
    *width = prt->paper.height;
    *height = prt->paper.width;
  }
  else
  {
    *width = prt->paper.width;
    *height = prt->paper.height;
  }
}

static void _precision_by_unit(const _unit_t unit,
                               int *n_digits,
                               float *incr,
                               char **format)
{
  // this gives us these precisions
  //  unit  precision  increment
  //   mm      1          1
  //   cm      0.1        0.1
  //   in      0.01       0.05
  //
  // This allows for >= 1mm precision display regardless of unit, and
  // allows for entering common fractions (e.g. 1/4 as .25) for
  // inches. Increment is kept to 1mm except in the cases of inches,
  // where we round up from 0.03937 (1mm) to 0.05 to keep to a factor
  // of a power of ten.
  *n_digits = ceilf(log10f(1.0f / units[unit]));
  if(incr)
  {
    *incr = roundf(units[unit] * 20.0f) / 20.0f;
  }
  if(format)
  {
    *format = g_strdup_printf("%%.%df", *n_digits);
  }
}

// unit conversion
static float _to_mm(dt_lib_print_settings_t *ps,
                    const double value)
{
  return value / units[ps->unit];
}

// horizontal mm to pixels
static float _mm_to_hscreen(dt_lib_print_settings_t *ps,
                            const float value,
                            const gboolean offset)
{
  float width, height;
  _get_page_dimension(&ps->prt, &width, &height);

  return (offset ? ps->imgs.screen.page.x : 0)
    + (ps->imgs.screen.page.width * value / width);
}

// vertical mm to pixels
static float _mm_to_vscreen(dt_lib_print_settings_t *ps,
                            const float value,
                            const gboolean offset)
{
  float width, height;
  _get_page_dimension(&ps->prt, &width, &height);

  return (offset ? ps->imgs.screen.page.y : 0)
    + (ps->imgs.screen.page.height * value / height);
}

static float _hscreen_to_mm(dt_lib_print_settings_t *ps,
                            const float value,
                            const gboolean offset)
{
  float width, height;
  _get_page_dimension(&ps->prt, &width, &height);

  return width * (value - (offset ? ps->imgs.screen.page.x : 0.0f))
    / ps->imgs.screen.page.width;
}

static float _vscreen_to_mm(dt_lib_print_settings_t *ps,
                            const float value,
                            const gboolean offset)
{
  float width, height;
  _get_page_dimension(&ps->prt, &width, &height);

  return height * (value - (offset ? ps->imgs.screen.page.y : 0.0f))
    / ps->imgs.screen.page.height;
}


static inline float _percent_unit_of(dt_lib_print_settings_t *ps,
                                     const float ref,
                                     const float value)
{
  return value * ref * units[ps->unit];
}

// callbacks for in-memory export
typedef struct dt_print_format_t
{
  dt_imageio_module_data_t head;
  int bpp;
  dt_lib_print_job_t *params;
} dt_print_format_t;

static int bpp(dt_imageio_module_data_t *data)
{
  const dt_print_format_t *d = (dt_print_format_t *)data;
  return d->bpp;
}

static int levels(dt_imageio_module_data_t *data)
{
  const dt_print_format_t *d = (dt_print_format_t *)data;
  return IMAGEIO_RGB | (d->bpp == 8 ? IMAGEIO_INT8 : IMAGEIO_INT16);
}

static const char *mime(dt_imageio_module_data_t *data)
{
  return "memory";
}

static int write_image(dt_imageio_module_data_t *data,
                       const char *filename,
                       const void *in,
                       dt_colorspaces_color_profile_type_t over_type,
                       const char *over_filename,
                       void *exif, int exif_len,
                       const dt_imgid_t imgid,
                       const int num,
                       const int total,
                       dt_dev_pixelpipe_t *pipe,
                       const gboolean export_masks)
{
  dt_print_format_t *d = (dt_print_format_t *)data;

  d->params->buf =
    (uint16_t *)g_try_malloc((size_t)3 * (d->bpp == 8?1:2) * d->head.width * d->head.height);
  if(!d->params->buf)
  {
    dt_print(DT_DEBUG_ALWAYS, "[print] unable to allocate memory for image %s", filename);
    return 1;
  }

  if(d->bpp == 8)
  {
    const uint8_t *in_ptr = (const uint8_t *)in;
    uint8_t *out_ptr = (uint8_t *)d->params->buf;
    for(int y = 0; y < d->head.height; y++)
    {
      for(int x = 0; x < d->head.width; x++, in_ptr += 4, out_ptr += 3)
        memcpy(out_ptr, in_ptr, 3);
    }
  }
  else
  {
    const uint16_t *in_ptr = (const uint16_t *)in;
    uint16_t *out_ptr = (uint16_t *)d->params->buf;
    for(int y = 0; y < d->head.height; y++)
    {
      for(int x = 0; x < d->head.width; x++, in_ptr += 4, out_ptr += 3)
        memcpy(out_ptr, in_ptr, 6);
    }
  }
  return 0;
}

// export image imgid with given max_width & max_height, set iwidth &
// iheight with the final image size as exported.
static int _export_image(dt_job_t *job, dt_image_box *img)
{
  dt_lib_print_job_t *params = dt_control_job_get_params(job);

  dt_imageio_module_format_t buf;
  buf.mime = mime;
  buf.levels = levels;
  buf.bpp = bpp;
  buf.write_image = write_image;

  dt_print_format_t dat;
  dat.head.max_width = img->max_width;
  dat.head.max_height = img->max_height;
  dat.head.style[0] = '\0';
  dat.head.style_append = params->style_append;
  dat.bpp = *params->p_icc_profile ? 16 : 8; // set to 16bit when a profile is to be applied
  dat.params = params;

  if(params->style) g_strlcpy(dat.head.style, params->style, sizeof(dat.head.style));

  // let the user know something is happening
  dt_control_job_set_progress(job, 0.05);
  dt_control_log(_("processing `%s' for `%s'"),
                 params->job_title, params->prt.printer.name);

  const gboolean high_quality = TRUE;
  const gboolean upscale = TRUE;
  const gboolean export_masks = FALSE;
  const gboolean is_scaling = FALSE;
  const double scale_factor = 1.0;

  dt_imageio_export_with_flags
    (img->imgid, "unused", &buf, (dt_imageio_module_data_t *)&dat, TRUE, FALSE,
     high_quality, upscale, is_scaling, scale_factor, FALSE, NULL,
     FALSE, export_masks, params->buf_icc_type,
     params->buf_icc_profile, params->buf_icc_intent,  NULL, NULL, 1, 1, NULL, -1);

  img->exp_width = dat.head.width;
  img->exp_height = dat.head.height;

  // we have the exported buffer, let's apply the printer profile

  const dt_colorspaces_color_profile_t *buf_profile =
    dt_colorspaces_get_output_profile(img->imgid,
                                      params->buf_icc_type,
                                      params->buf_icc_profile);
  if(*params->p_icc_profile)
  {
    const dt_colorspaces_color_profile_t *pprof =
      dt_colorspaces_get_profile(params->p_icc_type, params->p_icc_profile,
                                 DT_PROFILE_DIRECTION_OUT);
    if(!pprof)
    {
      dt_control_log(_("cannot open printer profile `%s'"), params->p_icc_profile);
      dt_print(DT_DEBUG_ALWAYS,
               "cannot open printer profile `%s'",
               params->p_icc_profile);
      dt_control_queue_redraw();
      return 1;    
    }
    else
    {
      if(!buf_profile || !buf_profile->profile)
      {
        dt_control_log(_("error getting output profile for image %d"), img->imgid);
        dt_print(DT_DEBUG_ALWAYS,
                 "error getting output profile for image %d",
                 img->imgid);
        dt_control_queue_redraw();
        return 1;
      }
      if(dt_apply_printer_profile
         ((void **)&(params->buf), dat.head.width, dat.head.height,
          dat.bpp, buf_profile->profile,
          pprof->profile, params->p_icc_intent, params->black_point_compensation))
      {
        dt_control_log(_("cannot apply printer profile `%s'"),
                       params->p_icc_profile);
        dt_print(DT_DEBUG_ALWAYS,
                 "cannot apply printer profile `%s'",
                 params->p_icc_profile);
        dt_control_queue_redraw();
        return 1;
      }
    }
  }
  // In _export_image(): Print actual buffer address and size right before assigning to img->buf
  img->buf = params->buf;
  img->img_bpp = dat.bpp; // Store the image bitdepth with the image being sent to print job
  params->buf = NULL;

  return 0;
}

#ifndef _WIN32
static void _create_pdf(dt_job_t *job,
                        dt_images_box imgs,
                        const float width,
                        const float height)
{
  dt_lib_print_job_t *params = dt_control_job_get_params(job);

  const float page_width  = dt_pdf_mm_to_point(width);
  const float page_height = dt_pdf_mm_to_point(height);
  int icc_id = 0;

  dt_pdf_image_t *pdf_image[MAX_IMAGE_PER_PAGE];

  // create the PDF page
  dt_pdf_t *pdf = dt_pdf_start(params->pdf_filename, page_width, page_height,
                               params->prt.printer.resolution,
                               DT_PDF_STREAM_ENCODER_FLATE);

#ifdef __APPLE__
  // On macOS, embed the printer ICC profile in the PDF so the
  // already-converted pixel data is correctly tagged with its actual
  // color space. Without this, cgpdftoraster assumes DeviceRGB = sRGB
  // and misinterprets the data.
  //
  // On Linux this must NOT be done — Poppler (inside pdftoraster) would
  // see the ICCBased color space and convert the data back to sRGB,
  // undoing the LittleCMS conversion. Linux uses cm-calibration instead.
  if(params->p_icc_profile && *params->p_icc_profile)
    icc_id = dt_pdf_add_icc(pdf, params->p_icc_profile);
#endif

  int32_t count = 0;

  for(int k=0; k<imgs.count; k++)
  {
    const int resolution = params->prt.printer.resolution;
    dt_image_box *box = &imgs.box[k];

    //If image is 16bit, it needs to be converted to big endian before sending for PDF creation

    if(box->img_bpp == 16)
    {
      const size_t total_samples = (size_t)3 * box->exp_width * box->exp_height;
      uint16_t *tmp = g_malloc(total_samples * sizeof(uint16_t));
      const uint16_t *src = box->buf;
      for(size_t i = 0; i < total_samples; i++)
      {
        tmp[i] = GUINT16_TO_BE(src[i]);
      }
      
      g_free(box->buf);
      box->buf = tmp;
    }

    if(dt_is_valid_imgid(box->imgid))
    {
      pdf_image[count] =
        dt_pdf_add_image(pdf, 
                         box->buf,
                         box->exp_width, box->exp_height,
                         box->img_bpp, icc_id, 0.0);

      //  PDF bounding-box has origin on bottom-left
      pdf_image[count]->bb_x      = dt_pdf_pixel_to_point(box->print.x, resolution);
      pdf_image[count]->bb_y      = dt_pdf_pixel_to_point(box->print.y, resolution);
      pdf_image[count]->bb_width  = dt_pdf_pixel_to_point(box->print.width, resolution);
      pdf_image[count]->bb_height = dt_pdf_pixel_to_point(box->print.height, resolution);
      count++;
    }
  }

  params->pdf_page = dt_pdf_add_page(pdf, pdf_image, count);
  dt_pdf_finish(pdf, &params->pdf_page, 1);

  // now releases all the buf
  for(int k=0; k<imgs.count; k++)
  {
    dt_image_box *box = &imgs.box[k];
    g_free(box->buf);
    box->buf = NULL;
  }
}
#endif

static void _fill_box_values(dt_lib_print_settings_t *ps)
{
  float x = 0.0f, y = 0.0f, swidth = 0.0f, sheight = 0.0f;

  if(ps->last_selected != -1)
  {
    dt_image_box *box = &ps->imgs.box[ps->last_selected];

    float width, height;
    _get_page_dimension(&ps->prt, &width, &height);

    x       = _percent_unit_of(ps, width, box->pos.x);
    y       = _percent_unit_of(ps, height, box->pos.y);
    swidth  = _percent_unit_of(ps, width, box->pos.width);
    sheight = _percent_unit_of(ps, height, box->pos.height);

    for(int i=0; i<9; i++)
    {
      DT_ENTER_GUI_UPDATE();
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ps->dtba[i]), (i == box->alignment));
      DT_LEAVE_GUI_UPDATE();
    }
  }

  DT_ENTER_GUI_UPDATE();

  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_x), x);

  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_y), y);

  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_width), swidth);

  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_height), sheight);

  DT_LEAVE_GUI_UPDATE();
}

static int _export_and_setup_pos(dt_job_t *job,
                                 dt_image_box *img,
                                 const int32_t idx)
{
  dt_lib_print_job_t *params = dt_control_job_get_params(job);

  float width, height;
  _get_page_dimension(&params->prt, &width, &height);

  dt_printing_setup_page(&params->imgs, width, height, params->prt.printer.resolution);

  dt_print(DT_DEBUG_PRINT, "[print] max image size %d x %d (at resolution %d)",
           img->max_width, img->max_height, params->prt.printer.resolution);

  if(_export_image(job, img))
    return 1;

  dt_printing_setup_image(&params->imgs, idx,
                          img->imgid, img->exp_width, img->exp_height, img->alignment);

  return 0;
}

static int _print_job_run(dt_job_t *job)
{
  dt_lib_print_job_t *params = dt_control_job_get_params(job);

  // get first image on a box, needed as print leader

  dt_imgid_t imgid = NO_IMGID;

  // compute the needed size for picture for the given printer resolution

  for(int k=0; k<params->imgs.count; k++)
  {
    if(dt_is_valid_imgid(params->imgs.box[k].imgid))
    {
      if(!dt_is_valid_imgid(imgid)) imgid = params->imgs.box[k].imgid;
      if(_export_and_setup_pos(job, &params->imgs.box[k], k))
        return 1;
    }
  }

  if(dt_control_job_get_state(job) == DT_JOB_STATE_CANCELLED)
    return 0;
  dt_control_job_set_progress(job, 0.9);

#ifdef _WIN32
  // on Windows, leverage the XPS print api

  float width, height;
  _get_page_dimension(&params->prt, &width, &height);

  // pull icc profile info to pass to win32_print.c to include in the job sent to the printer
  void *icc_data = NULL;
  size_t icc_size = 0;
  {
    dt_colorspaces_color_profile_type_t out_type =
      *params->prt.printer.profile ? params->p_icc_type : params->buf_icc_type;
    const char *out_filename =
      *params->prt.printer.profile ? params->p_icc_profile : params->buf_icc_profile;

    const dt_colorspaces_color_profile_t *cprof =
      dt_colorspaces_get_output_profile(imgid, out_type, out_filename);
    if(cprof && cprof->profile)
    {
      cmsUInt32Number len = 0;
      cmsSaveProfileToMem(cprof->profile, NULL, &len);
      if(len > 0)
      {
        icc_data = malloc(len);
        if(icc_data) cmsSaveProfileToMem(cprof->profile, icc_data, &len);
        icc_size = (size_t)len;
      }
    }
  }

  // send the print job to win32_print.c
  if(!dt_win_print_file(&params->imgs, params->job_title, &params->prt,
                        params->print_ticket_data, params->print_ticket_size,
                        icc_data, icc_size, params->is_color_device, width, height))
  {
    dt_control_log(_("Windows printing failed"));
  }

  free(icc_data);

#else
  // on Linux/OSX, print directly as a PDF

  dt_loc_get_tmp_dir(params->pdf_filename, sizeof(params->pdf_filename));
  g_strlcat(params->pdf_filename, "/pf.XXXXXX.pdf", sizeof(params->pdf_filename));

  const gint fd = g_mkstemp(params->pdf_filename);
  if(fd == -1)
  {
    dt_control_log(_("failed to create temporary PDF for printing"));
    dt_print(DT_DEBUG_ALWAYS, "failed to create temporary PDF for printing");
    return 1;
  }
  close(fd);

  float width, height;
  _get_page_dimension(&params->prt, &width, &height);

  _create_pdf(job, params->imgs, width, height);

  if(dt_control_job_get_state(job) == DT_JOB_STATE_CANCELLED) return 0;
  dt_control_job_set_progress(job, 0.95);

  // send to CUPS

  dt_print_file(imgid, params->pdf_filename, params->job_title, &params->prt);

#endif

  dt_control_job_set_progress(job, 1.0);

  // add tag for this image

  char tag[256] = { 0 };
  guint tagid = 0;
  snprintf (tag, sizeof(tag), "darktable|printed|%s", params->prt.printer.name);
  dt_tag_new(tag, &tagid);

  for(int k=0; k < params->imgs.count; k++)
  {
    if(dt_is_valid_imgid(params->imgs.box[k].imgid))
    {
      if(dt_tag_attach(tagid, params->imgs.box[k].imgid, FALSE, FALSE))
        DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_TAG_CHANGED);
      /* register print timestamp in cache */
      dt_image_cache_set_print_timestamp(params->imgs.box[k].imgid);
    }
  }

  return 0;
}

static void _page_new_area_clicked(GtkWidget *widget, dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;

  if(ps->imgs.count == MAX_IMAGE_PER_PAGE)
  {
    dt_control_log(_("maximum image per page reached"));
    return;
  }

  dt_control_change_cursor("cell");
  ps->creation = TRUE;
  ps->has_changed = TRUE;
}

static void _page_clear_area_clicked(GtkWidget *widget, dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;

  ps->has_changed = TRUE;
  dt_printing_clear_boxes(&ps->imgs);
  gtk_widget_set_sensitive(ps->del, FALSE);
  dt_control_queue_redraw_center();
}

static void _page_delete_area(const dt_lib_module_t *self,
                              const int box_index)
{
  dt_lib_print_settings_t *ps = self->data;

  if(box_index == -1) return;

  for(int k=box_index; k<MAX_IMAGE_PER_PAGE-1; k++)
  {
    memcpy(&ps->imgs.box[k], &ps->imgs.box[k+1], sizeof(dt_image_box));
  }
  ps->last_selected = -1;
  ps->selected = -1;
  dt_printing_clear_box(&ps->imgs.box[MAX_IMAGE_PER_PAGE-1]);
  ps->imgs.count--;

  if(ps->imgs.count > 0)
    ps->selected = 0;
  else
    gtk_widget_set_sensitive(ps->del, FALSE);

  _fill_box_values(ps);

  ps->has_changed = TRUE;
  dt_control_queue_redraw_center();
}

static void _page_delete_area_clicked(GtkWidget *widget, dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;

  _page_delete_area(self, ps->last_selected);
}

static void _print_job_cleanup(void *p)
{
  dt_lib_print_job_t *params = p;
  if(params->pdf_filename[0]) g_unlink(params->pdf_filename);
  free(params->pdf_page);
  free(params->buf);
  g_free(params->style);
  g_free(params->buf_icc_profile);
  g_free(params->p_icc_profile);
  g_free(params->job_title);
#ifdef _WIN32
  free(params->print_ticket_data);
#endif
  free(params);
}

#ifdef _WIN32
// idle callback to force a redraw once GTK is back in control
static gboolean _redraw_later(gpointer user_data)
{
  GtkWidget *widget = GTK_WIDGET(user_data);
  if(GTK_IS_WIDGET(widget))
    gtk_widget_queue_draw(widget);
  return G_SOURCE_REMOVE; // run once, then remove
}

static const dt_paper_info_t *_find_paper_by_dm(GList *paper_list, const dt_paper_info_t *from_dm)
{
  // prefer matching by driver ID — unambiguous, resolves same-dimension
  // variants (e.g. bordered/borderless) correctly
  if(from_dm->dm_paper_id != 0)
  {
    for(GList *l = paper_list; l; l = g_list_next(l))
    {
      const dt_paper_info_t *p = l->data;
      if(p->dm_paper_id == from_dm->dm_paper_id) return p;
    }
  }
  // fall back to dimension matching if no ID, or ID didn't match anything
  for(GList *l = paper_list; l; l = g_list_next(l))
  {
    const dt_paper_info_t *p = l->data;
    if(fabs(p->width - from_dm->width) < 0.5 && fabs(p->height - from_dm->height) < 0.5)
      return p;
  }
  return NULL;
}

static void _sync_print_widgets_from_pinfo(dt_lib_print_settings_t *ps)
{
  dt_bauhaus_combobox_set(ps->orientation, ps->prt.page.landscape ? 1 : 0);

  const dt_paper_info_t *matched = _find_paper_by_dm(ps->paper_list, &ps->prt.paper);
  if(matched)
  {
    memcpy(&ps->prt.paper, matched, sizeof(dt_paper_info_t));   // pulls in the correct name/common_name/id
    dt_bauhaus_combobox_set_from_text(ps->papers, matched->common_name);
  }
}

static void
_print_settings_button_clicked(GtkWidget *widget, dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = (dt_lib_print_settings_t *)self->data;
  if(!ps) return;

  // safer: use the toplevel window as owner
  GtkWidget *toplevel = gtk_widget_get_toplevel(self->widget);
  HWND hwnd_owner = NULL;
  if(GTK_IS_WINDOW(toplevel)) 
  {
    GdkWindow *gdk_win = gtk_widget_get_window(toplevel);
    if(gdk_win)
      hwnd_owner = gdk_win32_window_get_handle(gdk_win);
  }

  if(ps->settings_ctx)
  {
    dt_win_open_printer_settings(ps->settings_ctx, hwnd_owner);
    dt_win_sync_cached_dm_to_pinfo(ps->settings_ctx);
    _sync_print_widgets_from_pinfo(ps);
  }
  else 
  {
    dt_control_log(_("no settings context"));
  }

  // schedule redraw on idle so it runs after dialog closes
  g_idle_add(_redraw_later, self->widget);
}

// Convert the cached DEVMODE in ctx into a PrintTicket buffer the
// background job can carry across without touching any live COM/GDI
// handles itself. Returns TRUE and sets *out_data/*out_size on success;
// on failure, leaves *out_data NULL and logs — caller decides whether
// that's fatal or just "print with driver defaults".
static gboolean _win_build_print_ticket(dt_win32_print_ctx_t *ctx,
                                        void **out_data,
                                        size_t *out_size)
{
  *out_data = NULL;
  *out_size = 0;

  if(!ctx || !ctx->cached_dm || !ctx->base
     || !*ctx->base->printer.name)
    return FALSE;

  gunichar2 *wprinter = g_utf8_to_utf16(ctx->base->printer.name,
                                        -1, NULL, NULL, NULL);
  if(!wprinter) return FALSE;

  HPTPROVIDER hProvider = NULL;
  HRESULT hr = PTOpenProvider((PCWSTR)wprinter, 1, &hProvider);
  g_free(wprinter);
  if(FAILED(hr) || !hProvider)
  {
    dt_control_log(_("could not open print ticket provider"));
    return FALSE;
  }

  gboolean ok = FALSE;
  IStream *pStream = NULL;
  hr = CreateStreamOnHGlobal(NULL, TRUE, &pStream);
  if(SUCCEEDED(hr) && pStream)
  {
    hr = PTConvertDevModeToPrintTicket(hProvider,
                                       ctx->cached_dm->dmSize
                                         + ctx->cached_dm->dmDriverExtra,
                                       (PDEVMODE)ctx->cached_dm,
                                       kPTJobScope,
                                       pStream);
    if(SUCCEEDED(hr))
    {
      // pull the raw bytes out of the stream into a plain buffer —
      // deliberately not handing the job an IStream/COM object, since
      // the job runs on a different thread and shouldn't inherit any
      // COM apartment or lifetime concerns from this one.
      STATSTG statstg = { 0 };
      hr = pStream->lpVtbl->Stat(pStream, &statstg, STATFLAG_NONAME);

      if(SUCCEEDED(hr))
      {
        const size_t actual_size = (size_t)statstg.cbSize.QuadPart;
        HGLOBAL hGlobal = NULL;

        if(actual_size >0 && SUCCEEDED(GetHGlobalFromStream(pStream, &hGlobal)) && hGlobal)
        {
          void *src = GlobalLock(hGlobal);
          if(src)
          {
            void *buf = malloc(actual_size);
            if(buf)
            {
              memcpy(buf, src, actual_size);
              *out_data = buf;
              *out_size = actual_size;
              ok = TRUE;
            }
            GlobalUnlock(hGlobal);
          }
        }
      }
    }
    else
    {
      dt_control_log(_("could not convert printer settings to print ticket"));
    }
    pStream->lpVtbl->Release(pStream);
  }

  PTCloseProvider(hProvider);
  return ok;
}
#endif

static void _print_button_clicked(GtkWidget *widget, dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;

  dt_imgid_t imgid = NO_IMGID;

  // at least one image on a box

  for(int k=0; k<ps->imgs.count; k++)
  {
    if(dt_is_valid_imgid(ps->imgs.box[k].imgid))
    {
      imgid = ps->imgs.box[k].imgid;
      break;
    }
  }

  if(!dt_is_valid_imgid(imgid))
  {
    dt_control_log(_("cannot print until a picture is selected"));
    return;
  }
  if(strlen(ps->prt.printer.name) == 0 || ps->prt.printer.resolution == 0)
  {
    dt_control_log(_("cannot print until a printer is selected"));
    return;
  }
  if(ps->prt.paper.width == 0 || ps->prt.paper.height == 0)
  {
    dt_control_log(_("cannot print until a paper is selected"));
    return;
  }
#ifdef _WIN32
  if(ps->settings_ctx && !ps->settings_ctx->settings_opened)
  {
    GtkWidget *toplevel = gtk_widget_get_toplevel(self->widget);
    HWND hwnd_owner = NULL;
    if(GTK_IS_WINDOW(toplevel)) 
    {
      GdkWindow *gdk_win = gtk_widget_get_window(toplevel);
      if(gdk_win)
        hwnd_owner = gdk_win32_window_get_handle(gdk_win);
    }

    gboolean updated = dt_win_open_printer_settings(ps->settings_ctx, hwnd_owner);

    if(updated)
    {
      dt_win_sync_cached_dm_to_pinfo(ps->settings_ctx); 
      _sync_print_widgets_from_pinfo(ps);
    }
    ps->settings_ctx->settings_opened = TRUE;

    // schedule redraw on idle so it runs after dialog closes
    g_idle_add(_redraw_later, self->widget);
  }
#endif

  dt_job_t *job = dt_control_job_create(&_print_job_run, "print image %d", imgid);
  if(!job) return;

  dt_lib_print_job_t *params = calloc(1, sizeof(dt_lib_print_job_t));
  dt_control_job_set_params(job, params, _print_job_cleanup);

  memcpy(&params->prt,  &ps->prt, sizeof(dt_print_info_t));
  memcpy(&params->imgs, &ps->imgs, sizeof(ps->imgs));

  #ifdef _WIN32
  if(ps->settings_ctx)
  {
    if(!_win_build_print_ticket(ps->settings_ctx,
                                &params->print_ticket_data,
                                &params->print_ticket_size))
    {
      // not fatal — dt_win_print_file can fall back to driver
      // defaults if print_ticket_data is NULL
      dt_control_log(_("printing with default printer settings"));
    }
  }
#endif

  // what to call the image?
  GList *res = dt_metadata_get_lock(imgid, "Xmp.dc.title", NULL);
  if(res != NULL)
  {
    // FIXME: in metadata_view.c, non-printables are filtered, should
    // we do this here?
    params->job_title = g_strdup((gchar *)res->data);
    g_list_free_full(res, &g_free);
  }
  else
  {
    const dt_image_t *img = dt_image_cache_get(imgid, 'r');
    if(!img)
    {
      // in this case no need to release from cache what we couldn't get
      dt_control_log(_("cannot get image %d for printing"), imgid);
      dt_control_job_dispose(job);
      return;
    }
    params->job_title = g_strdup(img->filename);
    dt_image_cache_read_release(img);
  }
  // FIXME: ellipsize title/printer as the export completed message is ellipsized
  gchar *message = g_strdup_printf(_("processing `%s' for `%s'"),
                                   params->job_title, params->prt.printer.name);
  dt_control_job_add_progress(job, message, TRUE);
  g_free(message);

  // FIXME: getting this from conf as w/prior code, but switch to getting from ps
  params->style = dt_conf_get_string(PRINT_CONFIG_PREFIX "style");
  params->style_append = ps->v_style_append;

  // FIXME: getting these from conf as w/prior code, but switch to getting them from ps
  params->buf_icc_type = dt_conf_get_int(PRINT_CONFIG_PREFIX "icctype");
  params->buf_icc_profile = dt_conf_get_string(PRINT_CONFIG_PREFIX "iccprofile");
  params->buf_icc_intent = dt_conf_get_int(PRINT_CONFIG_PREFIX "iccintent");

  params->p_icc_type = ps->v_picctype;
  params->p_icc_profile = g_strdup(ps->v_piccprofile);
  params->p_icc_intent = ps->v_pintent;
  params->black_point_compensation = ps->v_black_point_compensation;

#ifdef _WIN32
  params->is_color_device = ps->settings_ctx ? ps->settings_ctx->is_color_device : TRUE;
#endif

  // Propagate the printer profile selection so dt_print_file() can
  // tell CUPS to skip color management when dt already did the
  // ICC conversion. See cups_print.c for platform-specific details.
  if(ps->v_piccprofile && *ps->v_piccprofile)
    g_strlcpy(params->prt.printer.profile, ps->v_piccprofile,
              sizeof(params->prt.printer.profile));

  dt_control_add_job(DT_JOB_QUEUE_USER_EXPORT, job);
}

static void _set_printer(const dt_lib_module_t *self,
                         const char *printer_name)
{
  dt_lib_print_settings_t *ps = self->data;

#ifdef _WIN32
  const gboolean orient_state = ps->prt.page.landscape;
#endif

  dt_get_printer_info(printer_name, &ps->prt.printer);

  // if this is a turboprint printer, disable the printer profile

  if(ps->prt.printer.is_turboprint)
    dt_bauhaus_combobox_set(ps->pprofile, 0);

  dt_conf_set_string(PRINT_CONFIG_PREFIX "printer", printer_name);

  // add papers for the given printer
  dt_bauhaus_combobox_clear(ps->papers);
  if(ps->paper_list) g_list_free_full(ps->paper_list, free);
  ps->paper_list = dt_get_papers (&ps->prt.printer);
  for(const GList *papers = ps->paper_list; papers; papers = g_list_next (papers))
  {
    const dt_paper_info_t *p = papers->data;
    dt_bauhaus_combobox_add(ps->papers, p->common_name);
  }
  const char *default_paper = dt_conf_get_string_const(PRINT_CONFIG_PREFIX "paper");
  //Paper reset to first entry if the default paper is not available for the selected printer and write to ps not relying on value-changed
  const dt_paper_info_t *chosen_paper = dt_get_paper(ps->paper_list, default_paper);
  if(!chosen_paper && ps->paper_list)
    chosen_paper = ps->paper_list->data;   // fall back to the first real entry

  if(chosen_paper)
  {
    //make sure the paper change is actually reflected in the printer info, not just the combobox
    memcpy(&ps->prt.paper, chosen_paper, sizeof(dt_paper_info_t));
    dt_bauhaus_combobox_set_from_text(ps->papers, chosen_paper->common_name);
  }

  // add corresponding supported media
  dt_bauhaus_combobox_clear(ps->media);
  if(ps->media_list) g_list_free_full(ps->media_list, free);
  ps->media_list = dt_get_media_type(&ps->prt.printer);
  for(const GList *media = ps->media_list; media; media = g_list_next (media))
  {
    const dt_medium_info_t *m = media->data;
    dt_bauhaus_combobox_add(ps->media, m->common_name);
  }
  const char *default_medium = dt_conf_get_string_const(PRINT_CONFIG_PREFIX "medium");
  //Medium reset to first entry if the default medium is not available for the selected printer and write to ps not relying on value-changed
  const dt_medium_info_t *chosen_medium = dt_get_medium(ps->media_list, default_medium);
  if(!chosen_medium && ps->media_list)
    chosen_medium = ps->media_list->data;   // fall back to the first real entry
  if(chosen_medium)
  {
    //make sure the medium change is actually reflected in the printer info, not just the combobox
    memcpy(&ps->prt.medium, chosen_medium, sizeof(dt_medium_info_t));
    dt_bauhaus_combobox_set_from_text(ps->media, chosen_medium->common_name);
  }

  dt_view_print_settings(darktable.view_manager, &ps->prt, &ps->imgs);

#ifdef _WIN32
  // free old context
  if(ps->settings_ctx)
    dt_win32_print_ctx_free(ps->settings_ctx);
  // populate for this printer to settings_ctx for windows print dialog caching
  ps->settings_ctx = dt_win32_print_ctx_new(&ps->prt);
  if(ps->settings_ctx)
  {
    // sync the cached DEVMODE to the printer info to align state
    dt_win_sync_cached_dm_to_pinfo(ps->settings_ctx);
    ps->prt.page.landscape = orient_state; // restore orientation state after printer change
    if(ps->settings_ctx->cached_dm) dt_sync_print_settings_to_dm(ps->settings_ctx->cached_dm, ps->settings_ctx->base);
    _sync_print_widgets_from_pinfo(ps);
  }
#endif
}
//Callback when printer details are ready (Windows only)
#ifdef _WIN32
static void _printer_ready_cb(dt_printer_info_t *pinfo, void *user_data)
{
  dt_lib_module_t *self = (dt_lib_module_t *)user_data;
  dt_lib_print_settings_t *ps = self->data;

  // What’s currently selected in the combo box?
  const gchar *current = dt_bauhaus_combobox_get_text(self->widget);

  if((ps->waiting_for_printer &&
      g_strcmp0(ps->waiting_for_printer, pinfo->name) == 0) ||
     (current && g_strcmp0(current, pinfo->name) == 0))
  {
    _set_printer(self, pinfo->name);
    dt_conf_set_string("plugins/lighttable/print/printer", pinfo->name);

    g_clear_pointer(&ps->waiting_for_printer, g_free);

    // Re‑enable the combo box now that details are ready
    gtk_widget_set_sensitive(ps->printers, TRUE);
    gtk_widget_set_sensitive(ps->papers, TRUE);

    // Now force a GUI refresh of the module
    if(self->gui_update)
    {
      self->gui_update(self);
    }
  }
}
#endif

static void _printer_changed(GtkWidget *combo, const dt_lib_module_t *self)
{
  const gchar *printer_name = dt_bauhaus_combobox_get_text(combo);
  if(!printer_name) return;

#ifdef _WIN32
  dt_lib_print_settings_t *ps = self->data;
  // Ask backend if details are ready
  if(!dt_printer_details_valid(printer_name))
  {
    g_free(ps->waiting_for_printer);
    ps->waiting_for_printer = g_strdup(printer_name);

    // Disable combo box to prevent multiple overlapping jobs
    gtk_widget_set_sensitive(ps->printers, FALSE);
    gtk_widget_set_sensitive(ps->papers, FALSE);
        // gtk_widget_set_sensitive(ps->papers, FALSE);
    dt_control_log(_("loading printer details for %s…"), printer_name);

    return;
  }
#endif

  // Default path (Linux, or Windows once details are valid)
  _set_printer(self, printer_name);
  dt_conf_set_string("plugins/lighttable/print/printer", printer_name);
}


static void
_paper_changed(GtkWidget *combo, const dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;

  const gchar *paper_name = dt_bauhaus_combobox_get_text(combo);

  if(!paper_name) return;

  const dt_paper_info_t *paper = dt_get_paper(ps->paper_list, paper_name);

  if(paper)
    memcpy(&ps->prt.paper, paper, sizeof(dt_paper_info_t));

  float width, height;
  _get_page_dimension(&ps->prt, &width, &height);

  dt_printing_setup_page(&ps->imgs, width, height, ps->prt.printer.resolution);

  dt_conf_set_string(PRINT_CONFIG_PREFIX "paper", paper_name);
  dt_view_print_settings(darktable.view_manager, &ps->prt, &ps->imgs);

#ifdef _WIN32
//  Change in paper size should get pushed to the cached DEVMODE so that the print dialog reflects the new size. 
//  This is important for Windows printing, as the DEVMODE is used to communicate settings to the printer driver.
  if(ps->settings_ctx && ps->settings_ctx->cached_dm)
    dt_sync_print_settings_to_dm(ps->settings_ctx->cached_dm, ps->settings_ctx->base);
#endif

  _update_slider(ps);
}

static void
_media_changed(GtkWidget *combo, const dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;

  const gchar *medium_name = dt_bauhaus_combobox_get_text(combo);

  if(!medium_name) return;

  const dt_medium_info_t *medium = dt_get_medium(ps->media_list, medium_name);

  if(medium)
  {
    memcpy(&ps->prt.medium, medium, sizeof(dt_medium_info_t));

  dt_conf_set_string(PRINT_CONFIG_PREFIX "medium", medium_name);
  dt_view_print_settings(darktable.view_manager, &ps->prt, &ps->imgs);

  //  Print media selection is currently hidden on Windows, but if it were to be exposed, we would want to sync the DEVMODE with the new media selection.
#ifdef _WIN32
  if(ps->settings_ctx && ps->settings_ctx->cached_dm)
    dt_sync_print_settings_to_dm(ps->settings_ctx->cached_dm, ps->settings_ctx->base);
#endif

    _update_slider(ps);
  }
  else
  {
    // No matching medium found: clear or fall back gracefully for NULL state
    memset(&ps->prt.medium, 0, sizeof(dt_medium_info_t));
    dt_bauhaus_combobox_set(ps->media, 0); // fallback to first entry if any
  }
}



static void
_update_slider(dt_lib_print_settings_t *ps)
{
  dt_view_print_settings(darktable.view_manager, &ps->prt, &ps->imgs);

  // if widget are created, let's display the current image size

  // Use the last selected image box if none is currently selected to allow for updating the size/DPI/scale information
  int sel = (ps->selected != -1) ? ps->selected : ps->last_selected;
  if(sel != -1
     && dt_is_valid_imgid(ps->imgs.box[sel].imgid)
     && ps->width && ps->height
     && ps->info)
  {
    const dt_image_box *box = &ps->imgs.box[sel];

    dt_image_pos box_size_mm, box_size;
    dt_printing_get_image_pos_mm(&ps->imgs, box, &box_size_mm);
    dt_printing_get_image_pos(&ps->imgs, box, &box_size);

    const double w = box_size_mm.width * units[ps->unit];
    const double h = box_size_mm.height * units[ps->unit];
    char *value, *precision;
    int n_digits;
    _precision_by_unit(ps->unit, &n_digits, NULL, &precision);

    value = g_strdup_printf(precision, w);
    gtk_label_set_text(GTK_LABEL(ps->width), value);
    g_free(value);

    value = g_strdup_printf(precision, h);
    gtk_label_set_text(GTK_LABEL(ps->height), value);
    g_free(value);
    g_free(precision);

    // compute the image down/up scale and report information

    const float iwidth  = box->img_width;
    const float iheight = box->img_height;
    const float awidth  = box_size.width;
    const float aheight = box_size.height;

    const double scale = (iwidth <= awidth)
      ? awidth / iwidth
      : aheight/ iheight;

    value = g_strdup_printf(_("%3.2f (dpi:%d)"), scale,
                            scale <= 1.0
                            ? (int)ps->prt.printer.resolution
                            : (int)(ps->prt.printer.resolution / scale));
    gtk_label_set_text(GTK_LABEL(ps->info), value);
    g_free(value);
  }
}

static void
_top_border_callback(GtkWidget *spin, dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;
  const double value = gtk_spin_button_get_value(GTK_SPIN_BUTTON(spin));

  dt_conf_set_float(PRINT_CONFIG_PREFIX "top_margin", value);

  ps->prt.page.margin_top = _to_mm(ps, value);

  if(ps->lock_activated)
  {
    ps->prt.page.margin_bottom = _to_mm(ps, value);
    ps->prt.page.margin_left = _to_mm(ps, value);
    ps->prt.page.margin_right = _to_mm(ps, value);

    gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_bottom), value);
    gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_left), value);
    gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_right), value);

    dt_conf_set_float(PRINT_CONFIG_PREFIX "bottom_margin", value);
    dt_conf_set_float(PRINT_CONFIG_PREFIX "left_margin", value);
    dt_conf_set_float(PRINT_CONFIG_PREFIX "right_margin", value);
  }
  _update_slider(ps);
}

static void
_bottom_border_callback(GtkWidget *spin, dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;
  const double value = gtk_spin_button_get_value(GTK_SPIN_BUTTON(spin));

  dt_conf_set_float(PRINT_CONFIG_PREFIX "bottom_margin", value);

  ps->prt.page.margin_bottom = _to_mm(ps, value);
  _update_slider(ps);
}

static void
_left_border_callback(GtkWidget *spin, dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;
  const double value = gtk_spin_button_get_value(GTK_SPIN_BUTTON(spin));

  dt_conf_set_float(PRINT_CONFIG_PREFIX "left_margin", value);

  ps->prt.page.margin_left = _to_mm(ps, value);
  _update_slider(ps);
}

static void
_right_border_callback(GtkWidget *spin, dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;
  const double value = gtk_spin_button_get_value(GTK_SPIN_BUTTON(spin));

  dt_conf_set_float(PRINT_CONFIG_PREFIX "right_margin", value);

  ps->prt.page.margin_right = _to_mm(ps, value);
  _update_slider(ps);
}

static void
_lock_callback(GtkWidget *button, dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;

  ps->lock_activated = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));

  dt_conf_set_bool(PRINT_CONFIG_PREFIX "lock_borders", ps->lock_activated);

  gtk_widget_set_sensitive(GTK_WIDGET(ps->b_bottom), !ps->lock_activated);
  gtk_widget_set_sensitive(GTK_WIDGET(ps->b_left), !ps->lock_activated);
  gtk_widget_set_sensitive(GTK_WIDGET(ps->b_right), !ps->lock_activated);

  //  get value of top and set it to all other borders

  const double value = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ps->b_top));
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_bottom), value);
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_left), value);
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_right), value);

  _update_slider (ps);
}

static void
_alignment_callback(GtkWidget *tb, dt_lib_module_t *self)
{
  DT_GUARD_GUI_UPDATE();

  int index=-1;
  dt_lib_print_settings_t *ps = self->data;

  for(int i=0; i<9; i++)
  {
    /* block signal handler */
    g_signal_handlers_block_by_func(ps->dtba[i],_alignment_callback, self);

    if(GTK_WIDGET(ps->dtba[i]) == tb)
    {
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ps->dtba[i]), TRUE);
      index=i;
    }
    else gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ps->dtba[i]), FALSE);

    /* unblock signal handler */
    g_signal_handlers_unblock_by_func(ps->dtba[i], _alignment_callback, self);
  }

  if(ps->last_selected != -1)
  {
    dt_printing_setup_image(&ps->imgs, ps->last_selected,
                            ps->imgs.box[ps->last_selected].imgid, 100, 100, index);
  }
  _update_slider(ps);
}

static void
_orientation_changed(GtkWidget *combo, dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;

  ps->prt.page.landscape = dt_bauhaus_combobox_get(combo);

#ifdef _WIN32
  if(ps->settings_ctx && ps->settings_ctx->cached_dm)
    dt_sync_print_settings_to_dm(ps->settings_ctx->cached_dm, ps->settings_ctx->base);
#endif

  _update_slider(ps);
}

static void
_snap_grid_callback(GtkWidget *widget, dt_lib_module_t *self)
{
  dt_control_queue_redraw_center();
}

static void
_grid_callback(GtkWidget *widget, dt_lib_module_t *self)
{
  dt_control_queue_redraw_center();
}

static void _grid_size_changed(GtkWidget *widget, dt_lib_module_t *self)
{
  DT_GUARD_GUI_UPDATE();

  dt_lib_print_settings_t *ps = self->data;
  const float value = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ps->grid_size));
  dt_conf_set_float(PRINT_CONFIG_PREFIX "grid_size", _to_mm(ps, value));

  dt_control_queue_redraw_center();
}

static void
_unit_changed(GtkWidget *combo, dt_lib_module_t *self)
{
  DT_TRY_GUI_UPDATE();

  dt_lib_print_settings_t *ps = self->data;

  const float grid_size = dt_conf_get_float(PRINT_CONFIG_PREFIX "grid_size");

  const int unit = dt_bauhaus_combobox_get(combo);
  if(unit < 0) return; // shouldn't happen, but it could be -1 if nothing is selected
  ps->unit = unit;
  dt_conf_set_string(PRINT_CONFIG_PREFIX "unit", _unit_names[unit]);

  const double margin_top = ps->prt.page.margin_top;
  const double margin_left = ps->prt.page.margin_left;
  const double margin_right = ps->prt.page.margin_right;
  const double margin_bottom = ps->prt.page.margin_bottom;

  int n_digits;
  float incr;
  _precision_by_unit(ps->unit, &n_digits, &incr, NULL);

  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(ps->b_top), n_digits);
  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(ps->b_bottom), n_digits);
  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(ps->b_left), n_digits);
  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(ps->b_right), n_digits);
  gtk_spin_button_set_increments(GTK_SPIN_BUTTON(ps->b_top), incr, 10.f*incr);
  gtk_spin_button_set_increments(GTK_SPIN_BUTTON(ps->b_bottom), incr, 10.f*incr);
  gtk_spin_button_set_increments(GTK_SPIN_BUTTON(ps->b_left), incr, 10.f*incr);
  gtk_spin_button_set_increments(GTK_SPIN_BUTTON(ps->b_right), incr, 10.f*incr);

  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(ps->b_x), n_digits);
  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(ps->b_y), n_digits);
  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(ps->b_width), n_digits);
  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(ps->b_height), n_digits);
  gtk_spin_button_set_increments(GTK_SPIN_BUTTON(ps->b_x), incr, 10.f*incr);
  gtk_spin_button_set_increments(GTK_SPIN_BUTTON(ps->b_y), incr, 10.f*incr);
  gtk_spin_button_set_increments(GTK_SPIN_BUTTON(ps->b_width), incr, 10.f*incr);
  gtk_spin_button_set_increments(GTK_SPIN_BUTTON(ps->b_height), incr, 10.f*incr);

  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(ps->grid_size), n_digits);
  gtk_spin_button_set_increments(GTK_SPIN_BUTTON(ps->grid_size), incr, 10.f*incr);

  _update_slider(ps);

  // convert margins to new unit
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_top),
                            margin_top * units[ps->unit]);
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_bottom),
                            margin_bottom * units[ps->unit]);
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_left),
                            margin_left * units[ps->unit]);
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_right),
                            margin_right * units[ps->unit]);

  // grid size
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->grid_size),
                            grid_size * units[ps->unit]);

  DT_LEAVE_GUI_UPDATE();

  _fill_box_values(ps);
}

static inline gboolean _is_style_set(const char *name)
{
  return name && name[0] && strcmp(name, _("none")) && dt_styles_exists(name);
}

static void _update_style_label(dt_lib_print_settings_t *ps, const char *name)
{
  if(!ps)
    return;

  const gboolean is_style_set = _is_style_set(name);

  if(ps->style_mode)
  {
    gtk_widget_set_visible(GTK_WIDGET(ps->style_mode), is_style_set);
  }

  // We use the string "none" to indicate that we don't apply any style to the export
  char *localized_style = is_style_set
    ? dt_util_localize_segmented_name(name, TRUE)
    : g_strdup(_("none"));

  // Use only the leaf part of the segmented style name in the tooltip
  char *leaf = strrchr(localized_style, '|');
  leaf = leaf ? leaf+1 : localized_style;
  gtk_label_set_text(GTK_LABEL(ps->style), leaf);
  char *tooltip = g_strdup_printf(_("style to be applied on print:\n<b>%s</b>"),
                                  localized_style);
  g_free(localized_style);
  gtk_widget_set_tooltip_markup(ps->style, tooltip);
  g_free(tooltip);
  g_free(ps->v_style);
  ps->v_style = g_strdup(is_style_set ? name : "");
  dt_conf_set_string(PRINT_CONFIG_PREFIX "style", is_style_set ? name : "");
}

static void _update_style(const dt_stylemenu_data_t *menu_data)
{
  _update_style_label(menu_data->user_data, menu_data->name);
}

static void _apply_style_activate_callback(GSimpleAction *action,
                                           GVariant *parameter,
                                           gpointer user_data)
{
  const dt_stylemenu_data_t *menu_data = (dt_stylemenu_data_t *)g_variant_get_uint64(parameter);

  _update_style(menu_data);
  dtgtk_stylemenu_free_menu_data();
}

static void _style_popupmenu_callback(GtkWidget *w, gpointer user_data)
{
  GActionGroup *action_group = gtk_widget_get_action_group(w, "styles");
  if(action_group == NULL)
  {
    GActionEntry action_entries[] =
    {
      { "activate", _apply_style_activate_callback, "t", NULL },
    };

    action_group = G_ACTION_GROUP(g_simple_action_group_new());
    g_action_map_add_action_entries(G_ACTION_MAP(action_group),
                                    action_entries,
                                    G_N_ELEMENTS(action_entries),
                                    NULL);
    gtk_widget_insert_action_group(w, 
                                   "styles",
                                   G_ACTION_GROUP(action_group));
    g_object_unref(action_group);
  }

  /* if we got any styles, lets popup menu for selection */
  GtkWidget *popover_menu = dtgtk_build_style_popover_menu(w, TRUE, user_data);
  if(popover_menu)
  {
    gtk_popover_popup(GTK_POPOVER(popover_menu));
  }
  else
    dt_control_log(_("no styles have been created yet"));
}

static void
_style_mode_changed(GtkWidget *widget, dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;

  if(dt_bauhaus_combobox_get(ps->style_mode) == 0)
    ps->v_style_append = FALSE;
  else
    ps->v_style_append = TRUE;

  dt_conf_set_bool(PRINT_CONFIG_PREFIX "style_append", ps->v_style_append);
}

static void
_profile_changed(GtkWidget *widget, dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;
  const int pos = dt_bauhaus_combobox_get(widget);
  for(const GList *prof = ps->profiles; prof; prof = g_list_next(prof))
  {
    dt_lib_export_profile_t *pp = prof->data;
    if(pp->pos == pos)
    {
      dt_conf_set_int(PRINT_CONFIG_PREFIX "icctype", pp->type);
      dt_conf_set_string(PRINT_CONFIG_PREFIX "iccprofile", pp->filename);
      g_free(ps->v_iccprofile);
      ps->v_icctype = pp->type;
      ps->v_iccprofile = g_strdup(pp->filename);
      return;
    }
  }
  dt_conf_set_int(PRINT_CONFIG_PREFIX "icctype", DT_COLORSPACE_NONE);
  dt_conf_set_string(PRINT_CONFIG_PREFIX "iccprofile", "");
  g_free(ps->v_iccprofile);
  ps->v_icctype = DT_COLORSPACE_NONE;
  ps->v_iccprofile = g_strdup("");
}

#ifdef _WIN32
/* Helper to rebuild the image/export profile combo on windows depending on driver
or printer color management to prevent misinterpretation of color data within
the printer driver which likely expects sRGB or AdobeRGB only. Still allow
anything wider (ProPhoto, Rec2020, "image settings") when darktable is managing
color through internal color management controls. */
static void _rebuild_image_profile_combo(dt_lib_print_settings_t *ps,
                                         const gboolean driver_managed)
{
  dt_bauhaus_combobox_clear(ps->profile);
  int n = 0;
  int srgb_pos = -1, adobergb_pos = -1;

  if(!driver_managed) 
  {
    dt_bauhaus_combobox_add(ps->profile, _("image settings"));
    n = 1;
  }
  // pos 0 reserved for "image settings" in unrestricted mode;
  // in driver-managed mode there is no pos 0 placeholder — first
  // real entry starts the list.

  for(GList *l = ps->profiles; l; l = g_list_next(l))
  {
    dt_lib_export_profile_t *prof = l->data;

    if(driver_managed && prof->type != DT_COLORSPACE_SRGB
       && prof->type != DT_COLORSPACE_ADOBERGB)
      continue; // restricted list: skip everything else

    dt_bauhaus_combobox_add(ps->profile, prof->name);
    prof->pos = n++;
    if(driver_managed)
    {
      if(prof->type == DT_COLORSPACE_SRGB)     srgb_pos = prof->pos;
      if(prof->type == DT_COLORSPACE_ADOBERGB) adobergb_pos = prof->pos;
    }
  }

  int target = 0; // "image settings" fallback for the !driver_managed case

  if(driver_managed)
  {
    // preserve AdobeRGB if that's already the selection; otherwise sRGB
    target = (ps->v_icctype == DT_COLORSPACE_ADOBERGB && adobergb_pos >= 0)
                ? adobergb_pos : srgb_pos;

    // set state first, matching gui_init's own convention — never rely
    // on dt_bauhaus_combobox_set to trigger _profile_changed as a side
    // effect; it does not reliably do so
    dt_lib_export_profile_t *chosen = NULL;
    for(GList *l = ps->profiles; l; l = g_list_next(l))
    {
      dt_lib_export_profile_t *prof = l->data;
      if(prof->pos == target) { chosen = prof; break; }
    }
    if(chosen)
    {
      dt_conf_set_int(PRINT_CONFIG_PREFIX "icctype", chosen->type);
      dt_conf_set_string(PRINT_CONFIG_PREFIX "iccprofile", chosen->filename);
      g_free(ps->v_iccprofile);
      ps->v_icctype = chosen->type;
      ps->v_iccprofile = g_strdup(chosen->filename);
    }
  }
  else
  {
    // returning to the full list: ps->v_icctype/v_iccprofile are already
    // correct (unchanged by this direction) — just find where that same
    // selection landed in the newly-expanded list
    gboolean found = FALSE;
    for(GList *l = ps->profiles; l; l = g_list_next(l))
    {
      dt_lib_export_profile_t *prof = l->data;
      if(prof->type == ps->v_icctype
         && (prof->type != DT_COLORSPACE_FILE
             || g_strcmp0(prof->filename, ps->v_iccprofile) == 0))
      {
        target = prof->pos;
        found = TRUE;
        break;
      }
    }
    if(!found)
    {
      // safety net only — sRGB/AdobeRGB should always be present, so
      // this shouldn't normally trigger. If it does, mirror gui_init's
      // own combo_idx==-1 fallback and reset state to match pos 0.
      dt_conf_set_int(PRINT_CONFIG_PREFIX "icctype", DT_COLORSPACE_NONE);
      dt_conf_set_string(PRINT_CONFIG_PREFIX "iccprofile", "");
      g_free(ps->v_iccprofile);
      ps->v_icctype = DT_COLORSPACE_NONE;
      ps->v_iccprofile = g_strdup("");
    }
  }

  dt_bauhaus_combobox_set(ps->profile, target);
}
#endif

static void
_printer_profile_changed(GtkWidget *widget, dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;
  const int pos = dt_bauhaus_combobox_get(widget);

#ifdef _WIN32
  _rebuild_image_profile_combo(ps, pos == 0); // triggers a limitation in image profiles to Windows printer driver typical supported profiles (sRGB, AdobeRGB)
#endif

  for(const GList *prof = ps->profiles; prof; prof = g_list_next(prof))
  {
    dt_lib_export_profile_t *pp = prof->data;
    if(pp->ppos == pos)
    {
      dt_conf_set_int("plugins/print/printer/icctype", pp->type);
      dt_conf_set_string("plugins/print/printer/iccprofile", pp->filename);
      g_free(ps->v_piccprofile);
      ps->v_picctype = pp->type;
      ps->v_piccprofile = g_strdup(pp->filename);

      // activate the black compensation and printer intent
      gtk_widget_set_sensitive(GTK_WIDGET(ps->black_point_compensation), TRUE);
      return;
    }
  }
  dt_conf_set_int("plugins/print/printer/icctype", DT_COLORSPACE_NONE);
  dt_conf_set_string("plugins/print/printer/iccprofile", "");
  g_free(ps->v_piccprofile);
  ps->v_picctype = DT_COLORSPACE_NONE;
  ps->v_piccprofile = g_strdup("");
  gtk_widget_set_sensitive(GTK_WIDGET(ps->black_point_compensation), FALSE);
}

static void
_printer_intent_callback (GtkWidget *widget, dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;
  const int pos = dt_bauhaus_combobox_get(widget);
  dt_conf_set_int("plugins/print/printer/iccintent", pos);
  ps->v_pintent = pos;
  ps->prt.printer.intent = pos;
}

static void
_printer_bpc_callback(GtkWidget *widget, dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;
  ps->v_black_point_compensation =
    gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(ps->black_point_compensation));
  dt_conf_set_bool(PRINT_CONFIG_PREFIX "black_point_compensation",
                   ps->v_black_point_compensation);
}

static void
_intent_callback(GtkWidget *widget, dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;
  const int pos = dt_bauhaus_combobox_get(widget);
  // record the intent that will override the out rendering module on export
  dt_conf_set_int(PRINT_CONFIG_PREFIX "iccintent", pos - 1);
  ps->v_intent = pos - 1;
}

static void _set_orientation(dt_lib_print_settings_t *ps, dt_imgid_t imgid)
{
  dt_mipmap_buffer_t buf;
  dt_mipmap_cache_get(&buf, imgid, DT_MIPMAP_0, DT_MIPMAP_BEST_EFFORT, 'r');

  // If there's a mipmap available, figure out orientation based upon
  // its dimensions. Otherwise, don't touch orientation until the
  // mipmap arrives.
  if(buf.size != DT_MIPMAP_NONE)
  {
    ps->prt.page.landscape = (buf.width > buf.height);
    dt_view_print_settings(darktable.view_manager, &ps->prt, &ps->imgs);
    dt_bauhaus_combobox_set(ps->orientation, ps->prt.page.landscape ? 1 : 0);

    // Make sure the Windows DEVMODE is updated to match the new orientation
  #ifdef _WIN32
    if(ps->settings_ctx && ps->settings_ctx->cached_dm)
      dt_sync_print_settings_to_dm(ps->settings_ctx->cached_dm, ps->settings_ctx->base);
  #endif
  }

  dt_mipmap_cache_release(&buf);
  dt_control_queue_redraw_center();
}

static void _load_image_full_page(dt_lib_print_settings_t *ps,
                                  const dt_imgid_t imgid)
{
  _set_orientation(ps, imgid);

  dt_printing_setup_box(&ps->imgs, 0,
                        ps->imgs.screen.page.x, ps->imgs.screen.page.y,
                        ps->imgs.screen.page.width, ps->imgs.screen.page.height);
  float width, height;
  _get_page_dimension(&ps->prt, &width, &height);

  dt_printing_setup_page(&ps->imgs, width, height, ps->prt.printer.resolution);

  dt_printing_setup_image(&ps->imgs, 0, imgid, 100, 100, ALIGNMENT_CENTER);

  dt_control_queue_redraw_center();
}

static void _print_settings_update_callback(gpointer instance,
                                            const dt_imgid_t imgid,
                                            dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;

  // if a mipmap has arrived for an image just activated in fullpage
  // mode, reorient the page (landscape or portrait) based on the
  // mipmap's orientation
  if(ps->imgs.count == 1
     && ps->imgs.box[0].imgid == imgid
     && !ps->has_changed)
  {
    dt_printing_clear_box(&ps->imgs.box[0]);
    _load_image_full_page(ps, imgid);
  }
}

static void _print_settings_activate_callback(gpointer instance,
                                              const dt_imgid_t imgid,
                                              dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;

  // load an image with a simple click on the filmstrip only if a
  // single image is present
  if(ps->imgs.count == 1)
  {
    if(ps->has_changed)
    {
      dt_printing_setup_image(&ps->imgs, 0, imgid, 100, 100, ps->imgs.box[0].alignment);
      dt_control_queue_redraw_center();
    }
    else
    {
      dt_printing_clear_box(&ps->imgs.box[0]);
      _load_image_full_page(ps, imgid);
    }
  }
}

static GList* _get_profiles()
{
  //  Create list of profiles
  GList *list = NULL;

  dt_lib_export_profile_t *prof = g_malloc0(sizeof(dt_lib_export_profile_t));
  prof->type = DT_COLORSPACE_SRGB;
  dt_utf8_strlcpy(prof->name, _("sRGB"), sizeof(prof->name));
  prof->pos = -2;
  prof->ppos = -2;
  list = g_list_prepend(list, prof);

  prof = g_malloc0(sizeof(dt_lib_export_profile_t));
  prof->type = DT_COLORSPACE_ADOBERGB;
  dt_utf8_strlcpy(prof->name, _("Adobe RGB (compatible)"), sizeof(prof->name));
  prof->pos = -2;
  prof->ppos = -2;
  list = g_list_prepend(list, prof);

  // add the profiles from datadir/color/out/*.icc
  for(GList *iter = darktable.color_profiles->profiles; iter; iter = g_list_next(iter))
  {
    dt_colorspaces_color_profile_t *p = iter->data;
    if(p->type == DT_COLORSPACE_FILE)
    {
      prof = g_malloc0(sizeof(dt_lib_export_profile_t));
      g_strlcpy(prof->name, p->name, sizeof(prof->name));
      g_strlcpy(prof->filename, p->filename, sizeof(prof->filename));
      prof->type = DT_COLORSPACE_FILE;
      prof->pos = -2;
      prof->ppos = -2;
      list = g_list_prepend(list, prof);
    }
  }

  return g_list_reverse(list);  // list was built in reverse order, so un-reverse it
}

static void _new_printer_callback(dt_printer_info_t *printer,
                                  void *user_data)
{
  const dt_lib_module_t *self = (dt_lib_module_t *)user_data;
  dt_lib_print_settings_t *d = (dt_lib_print_settings_t*)self->data;

  dt_pthread_mutex_lock(&d->printer_list_mutex);
  d->printer_list = g_list_append(d->printer_list, g_strdup(printer->name));
  dt_pthread_mutex_unlock(&d->printer_list_mutex);

  d->prt.num_printers++;
}

void view_enter(struct dt_lib_module_t *self,
                struct dt_view_t *old_view,
                struct dt_view_t *new_view)
{
  dt_lib_print_settings_t *d = (dt_lib_print_settings_t*)self->data;

  dt_pthread_mutex_lock(&d->printer_list_mutex);
  if(d->printer_list != NULL)
  {
    // The printer list was filled by dt_printers_discovery() in a background job.
    // Now we fill the printer combo with the found printers.
    char *default_printer = dt_conf_get_string(PRINT_CONFIG_PREFIX "printer");

    for(const GList *iter = d->printer_list; iter; iter = g_list_next(iter))
    {
      const gchar *printer_name = iter->data;
      dt_bauhaus_combobox_add(d->printers, printer_name);
    }

    if(default_printer[0] == '\0'
       || !dt_bauhaus_combobox_set_from_text(d->printers, default_printer))
    {
      // no default printer or default printer not found
      dt_bauhaus_combobox_set(d->printers, 0);
      // no default printer or default printer not found — fall back to
      // the first currently-installed printer, and explicitly run setup
      // rather than relying on the combobox's "value-changed" signal to
      // fire on a programmatic set
      if(d->printer_list)
        _set_printer(self, (const char *)d->printer_list->data);
    }

    g_free(default_printer);
    g_list_free_full(d->printer_list, g_free);
    d->printer_list = NULL;
  }
  dt_pthread_mutex_unlock(&d->printer_list_mutex);

  // user activated a new image via the filmstrip or user entered view
  // mode which activates an image: get image_id and orientation
  DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_VIEWMANAGER_THUMBTABLE_ACTIVATE, _print_settings_activate_callback);

  // when an updated mipmap, we may have new orientation information
  // about the current image.
  DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_MIPMAP_UPDATED, _print_settings_update_callback);

  // NOTE: it would be proper to set image_id here to -1, but this
  // seems to make no difference
}

void view_leave(struct dt_lib_module_t *self,
                struct dt_view_t *old_view,
                struct dt_view_t *new_view)
{
  DT_CONTROL_SIGNAL_DISCONNECT(_print_settings_activate_callback, self);
  DT_CONTROL_SIGNAL_DISCONNECT(_print_settings_update_callback, self);
}

static gboolean _expose_again(gpointer user_data)
{
  dt_lib_print_settings_t *ps = (dt_lib_print_settings_t *)user_data;

  if(dt_is_valid_imgid(ps->imgs.imgid_to_load))
  {
    _load_image_full_page(ps, ps->imgs.imgid_to_load);
    ps->imgs.imgid_to_load = NO_IMGID;
  }

  dt_control_queue_redraw_center();
  return FALSE;
}

static void _get_control(dt_lib_print_settings_t *ps,
                         const float x,
                         const float y)
{
  const float dist = 20.0;

  const dt_image_box *b = &ps->imgs.box[ps->selected];

  ps->sel_controls = 0;

  if(fabsf(b->screen.x - x) < dist)
    ps->sel_controls |= BOX_LEFT;

  if(fabsf(b->screen.y - y) < dist)
    ps->sel_controls |= BOX_TOP;

  if(fabsf((b->screen.x + b->screen.width) - x) < dist)
    ps->sel_controls |= BOX_RIGHT;

  if(fabsf((b->screen.y + b->screen.height) - y) < dist)
    ps->sel_controls |= BOX_BOTTOM;

  if(ps->sel_controls == 0) ps->sel_controls = BOX_ALL;
}

int mouse_leave(struct dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;

  if(ps->last_selected != -1)
  {
    dt_control_set_mouse_over_id(ps->imgs.box[ps->last_selected].imgid);
  }

  return 0;
}

static void _snap_to_grid(dt_lib_print_settings_t *ps,
                          float *x,
                          float *y)
{
  if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(ps->snap_grid)))
  {
    // V lines
    const float step =
      gtk_spin_button_get_value(GTK_SPIN_BUTTON(ps->grid_size)) / units[ps->unit];

    // only display the grid if a step of 5 pixels
    const float diff = DT_PIXEL_APPLY_DPI(5);

    float grid_pos = (float)ps->imgs.screen.page.x;

    const float h_step = _mm_to_hscreen(ps, step, FALSE);

    while(grid_pos < ps->imgs.screen.page.x + ps->imgs.screen.page.width)
    {
      if(fabsf(*x - grid_pos) < diff) *x = grid_pos;
      grid_pos += h_step;
    }

    // H lines
    grid_pos = (float)ps->imgs.screen.page.y;

    const float v_step = _mm_to_vscreen(ps, step, FALSE);

    while(grid_pos < ps->imgs.screen.page.y + ps->imgs.screen.page.height)
    {
      if(fabsf(*y - grid_pos) < diff) *y = grid_pos;
      grid_pos += v_step;
    }
  }
  // FIXME: should clamp values to page size here?
}

int mouse_moved(struct dt_lib_module_t *self,
                const double x,
                const double y,
                const double pressure,
                const int which)
{
  dt_lib_print_settings_t *ps = self->data;

  gboolean expose = FALSE;

  if(ps->creation)
    dt_control_change_cursor("cell");

  if(ps->creation && ps->dragging)
  {
    ps->x2 = x;
    ps->y2 = y;
    _snap_to_grid(ps, &ps->x2, &ps->y2);
    expose = TRUE;
  }
  else if(ps->dragging)
  {
    dt_image_box *b = &ps->imgs.box[ps->selected];
    const float dx = x - ps->click_pos_x;
    const float dy = y - ps->click_pos_y;
    const float coef = dx / b->screen.width;

    switch(ps->sel_controls)
    {
       case BOX_ALL:
         ps->x1 = b->screen.x + dx;
         ps->y1 = b->screen.y + dy;
         ps->x2 = b->screen.x + b->screen.width + dx;
         ps->y2 = b->screen.y + b->screen.height + dy;
         break;
       case BOX_LEFT:
         ps->x1 = b->screen.x + dx;
         break;
       case BOX_TOP:
         ps->y1 = b->screen.y + dy;
         break;
       case BOX_RIGHT:
         ps->x2 = b->screen.x + b->screen.width + dx;
         break;
       case BOX_BOTTOM:
         ps->y2 = b->screen.y + b->screen.height + dy;
         break;
       case BOX_TOP_LEFT:
         ps->x1 = b->screen.x + dx;
         ps->y1 = b->screen.y + (coef * b->screen.height);
         break;
       case BOX_TOP_RIGHT:
         ps->x2 = b->screen.x + b->screen.width + dx;
         ps->y1 = b->screen.y - (coef * b->screen.height);
         break;
       case BOX_BOTTOM_LEFT:
         ps->x1 = b->screen.x + dx;
         ps->y2 = b->screen.y + b->screen.height - (coef * b->screen.height);
         break;
       case BOX_BOTTOM_RIGHT:
         ps->x2 = b->screen.x + b->screen.width + dx;
         ps->y2 = b->screen.y + b->screen.height + (coef * b->screen.height);
         break;
       default:
         break;
    }
    expose = TRUE;

    _snap_to_grid(ps, &ps->x1, &ps->y1);
    _snap_to_grid(ps, &ps->x2, &ps->y2);
  }
  else if(!ps->creation)
  {
    const int bidx = dt_printing_get_image_box(&ps->imgs, x, y);
    ps->sel_controls = 0;

    if(bidx == -1)
    {
      if(ps->selected != -1) expose = TRUE;
      ps->selected = -1;
    }
    else
    {
      expose = TRUE;
      ps->selected = bidx;
      _fill_box_values(ps);
      _get_control(ps, x, y);
    }
  }

  if(expose) dt_control_queue_redraw_center();

  return 0;
}

static void _swap(float *a, float *b)
{
  const float tmp = *a;
  *a = *b;
  *b = tmp;
}

int button_released(struct dt_lib_module_t *self,
                    const double x,
                    const double y,
                    const int which,
                    const uint32_t state)
{
  dt_lib_print_settings_t *ps = self->data;

  if(ps->dragging)
  {
    int idx = -1;

    gtk_widget_set_sensitive(ps->del, TRUE);

    // handle new area
    if(ps->creation)
    {
      idx = ps->imgs.count++;
    }
    else if(ps->selected != -1)
    {
      idx = ps->selected;
    }

    if(idx != -1)
    {
      // make sure the area is in the the printable area taking into account the margins

      // don't allow a too small area
      if(ps->x2 < ps->x1) _swap(&ps->x1, &ps->x2);
      if(ps->y2 < ps->y1) _swap(&ps->y1, &ps->y2);

      const float dx = ps->x2 - ps->x1;
      const float dy = ps->y2 - ps->y1;

      dt_printing_setup_box(&ps->imgs, idx, ps->x1, ps->y1, dx, dy);
      // make the new created box the last edited one
      ps->last_selected = idx;
      _fill_box_values(ps);
    }
  }

  _update_slider(ps);

  ps->creation = FALSE;
  ps->dragging = FALSE;

  dt_control_change_cursor("default");

  return 0;
}

int button_pressed(struct dt_lib_module_t *self,
                   const double x,
                   const double y,
                   const double pressure,
                   const int which,
                   const int type,
                   const uint32_t state)
{
  dt_lib_print_settings_t *ps = self->data;

  ps->click_pos_x = x;
  ps->click_pos_y = y;
  ps->last_selected = -1;

  if(ps->creation)
  {
    ps->dragging = TRUE;
    ps->selected = -1;
    ps->x1 = ps->x2 = x;
    ps->y1 = ps->y2 = y;

    _snap_to_grid(ps, &ps->x1, &ps->y1);
  }
  else if(ps->selected > 0
          && (which == GDK_BUTTON_MIDDLE || (which == GDK_BUTTON_PRIMARY && dt_modifier_is(state, GDK_CONTROL_MASK))))
  {
    // middle click (or ctrl-click), move selected image down
    dt_image_box b;
    memcpy(&b, &ps->imgs.box[ps->selected], sizeof(dt_image_box));
    memcpy(&ps->imgs.box[ps->selected], &ps->imgs.box[ps->selected-1],
           sizeof(dt_image_box));
    memcpy(&ps->imgs.box[ps->selected-1], &b, sizeof(dt_image_box));
  }
  else if(ps->selected != -1 && which == GDK_BUTTON_PRIMARY)
  {
    dt_image_box *b = &ps->imgs.box[ps->selected];

    ps->dragging = TRUE;
    ps->x1 = b->screen.x;
    ps->y1 = b->screen.y;
    ps->x2 = b->screen.x + b->screen.width;
    ps->y2 = b->screen.y + b->screen.height;

    ps->last_selected = ps->selected;
    ps->has_changed = TRUE;

    _get_control(ps, x, y);

    dt_control_change_cursor("pointer");
  }
  else if(ps->selected != -1 && which == GDK_BUTTON_SECONDARY)
  {
    dt_image_box *b = &ps->imgs.box[ps->selected];

    // if image present remove it, otherwise remove the box
    if(dt_is_valid_imgid(b->imgid))
      b->imgid = NO_IMGID;
    else
      _page_delete_area(self, ps->selected);

    ps->last_selected = ps->selected;
    ps->has_changed = TRUE;
  }

  return 0;
}

static void _cairo_rectangle(cairo_t *cr,
                             const int sel_controls,
                             const int x1,
                             const int y1,
                             const int x2,
                             const int y2)
{
  const float sel_width = DT_PIXEL_APPLY_DPI(3.0);
  const float std_width = DT_PIXEL_APPLY_DPI(1.0);

  const gboolean all = sel_controls == BOX_ALL;

  cairo_move_to(cr, x1, y1);
  cairo_set_line_width(cr, (all || sel_controls == BOX_LEFT) ? sel_width : std_width);
  cairo_line_to(cr, x1, y2);
  cairo_stroke(cr);

  cairo_move_to(cr, x1, y2);
  cairo_set_line_width(cr, (all || sel_controls == BOX_BOTTOM) ? sel_width : std_width);
  cairo_line_to(cr, x2, y2);
  cairo_stroke(cr);

  cairo_move_to(cr, x2, y2);
  cairo_set_line_width(cr, (all || sel_controls == BOX_RIGHT) ? sel_width : std_width);
  cairo_line_to(cr, x2, y1);
  cairo_stroke(cr);

  cairo_move_to(cr, x2, y1);
  cairo_set_line_width(cr, (all || sel_controls == BOX_TOP) ? sel_width : std_width);
  cairo_line_to(cr, x1, y1);
  cairo_stroke(cr);

  if(sel_controls == 0)
  {
    const double dash[] = { DT_PIXEL_APPLY_DPI(3.0), DT_PIXEL_APPLY_DPI(3.0) };
    cairo_set_dash(cr, dash, 2, 0);

    // no image inside
    cairo_move_to(cr, x1, y1);
    cairo_line_to(cr, x2, y2);

    cairo_move_to(cr, x1, y2);
    cairo_line_to(cr, x2, y1);
    cairo_stroke(cr);
  }

  cairo_set_dash(cr, NULL, 0, 0);
  cairo_set_line_width(cr, sel_width);

  if(sel_controls == BOX_TOP_LEFT)
  {
    cairo_rectangle (cr, x1, y1, DT_PIXEL_APPLY_DPI(15), DT_PIXEL_APPLY_DPI(15));
    cairo_stroke(cr);
  }

  if(sel_controls == BOX_TOP_RIGHT)
  {
    cairo_rectangle (cr, x2 - DT_PIXEL_APPLY_DPI(15), y1,
                     DT_PIXEL_APPLY_DPI(15), DT_PIXEL_APPLY_DPI(15));
    cairo_stroke(cr);
  }

  if(sel_controls == BOX_BOTTOM_LEFT)
  {
    cairo_rectangle (cr, x1, y2 - DT_PIXEL_APPLY_DPI(15),
                     DT_PIXEL_APPLY_DPI(15), DT_PIXEL_APPLY_DPI(15));
    cairo_stroke(cr);
  }

  if(sel_controls == BOX_BOTTOM_RIGHT)
  {
    cairo_rectangle (cr, x2 - DT_PIXEL_APPLY_DPI(15), y2 - DT_PIXEL_APPLY_DPI(15),
                     DT_PIXEL_APPLY_DPI(15), DT_PIXEL_APPLY_DPI(15));
    cairo_stroke(cr);
  }
}

void gui_post_expose(struct dt_lib_module_t *self,
                     cairo_t *cr,
                     const int32_t width,
                     const int32_t height,
                     const int32_t pointerx,
                     const int32_t pointery)
{
  dt_lib_print_settings_t *ps = self->data;

  if(dt_is_valid_imgid(ps->imgs.imgid_to_load))
  {
    // we set orientation and delay the reload to ensure the
    // page is properly set before trying to display the image.
    _set_orientation(ps, ps->imgs.imgid_to_load);
    g_timeout_add(250, _expose_again, ps);
  }

  // display grid

  // 1mm

  const float step =
    gtk_spin_button_get_value(GTK_SPIN_BUTTON(ps->grid_size)) / units[ps->unit];

  // only display grid if spacing more than 5 pixels
  if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(ps->grid))
     && (int)_mm_to_hscreen(ps, step, FALSE) > DT_PIXEL_APPLY_DPI(5))
  {
    const double dash[] = { DT_PIXEL_APPLY_DPI(5.0), DT_PIXEL_APPLY_DPI(5.0) };
    cairo_set_source_rgba(cr, 1, .2, .2, 0.6);

    // V lines
    float grid_pos = (float)ps->imgs.screen.page.x;

    const float h_step = _mm_to_hscreen(ps, step, FALSE);
    int n = 0;

    while(grid_pos < ps->imgs.screen.page.x + ps->imgs.screen.page.width)
    {
      cairo_set_dash(cr,
                     dash, ((n % 5) == 0)
                     ? 0
                     : 2, DT_PIXEL_APPLY_DPI(5));
      cairo_set_line_width(cr,
                           ((n % 5) == 0)
                           ? DT_PIXEL_APPLY_DPI(1.0)
                           : DT_PIXEL_APPLY_DPI(0.5));
      cairo_move_to(cr, grid_pos, ps->imgs.screen.page.y);
      cairo_line_to(cr, grid_pos, ps->imgs.screen.page.y + ps->imgs.screen.page.height);
      cairo_stroke(cr);
      grid_pos += h_step;
      n++;
    }

    // H lines
    grid_pos = (float)ps->imgs.screen.page.y;

    const float v_step = _mm_to_vscreen(ps, step, FALSE);
    n = 0;

    while(grid_pos < ps->imgs.screen.page.y + ps->imgs.screen.page.height)
    {
      cairo_set_dash(cr, dash,
                     ((n % 5) == 0)
                     ? 0
                     : 2, DT_PIXEL_APPLY_DPI(5));
      cairo_set_line_width(cr,
                           ((n % 5) == 0)
                           ? DT_PIXEL_APPLY_DPI(1.0)
                           : DT_PIXEL_APPLY_DPI(0.5));

      cairo_move_to(cr, ps->imgs.screen.page.x, grid_pos);
      cairo_line_to(cr, ps->imgs.screen.page.x + ps->imgs.screen.page.width, grid_pos);
      cairo_stroke(cr);
      grid_pos += v_step;
      n++;
    }
  }

  // disable dash
  cairo_set_source_rgba(cr, 1, .2, .2, 0.6);
  cairo_set_dash(cr, NULL, 0, 0);

  const float scaler = 1.0f / darktable.gui->ppd_thb;

  for(int k=0; k<ps->imgs.count; k++)
  {
    dt_image_box *img = &ps->imgs.box[k];

    if(dt_is_valid_imgid(img->imgid))
    {
      cairo_surface_t *surf = NULL;

      // screen coordinate default to current box if there is no image
      dt_image_pos screen;

      dt_printing_setup_image(&ps->imgs, k, img->imgid, 100, 100, img->alignment);

      dt_printing_get_screen_pos(&ps->imgs, img, &screen);

      const dt_view_surface_value_t res =
        dt_view_image_get_surface(img->imgid, screen.width, screen.height, &surf, TRUE);

      if(res != DT_VIEW_SURFACE_OK)
      {
        // if the image is missing, we reload it again
        g_timeout_add(250, _expose_again, ps);
        if(!ps->busy) dt_control_busy_enter();
        ps->busy = TRUE;
      }
      else
      {
        cairo_save(cr);
        cairo_translate(cr, screen.x, screen.y);
        cairo_scale(cr, scaler, scaler);
        cairo_set_source_surface(cr, surf, 0, 0);
        const double alpha =
          (ps->dragging
           || (ps->selected != -1 && ps->selected != k))
          ? 0.25
          : 1.0;

        cairo_paint_with_alpha(cr, alpha);
        cairo_surface_destroy(surf);
        cairo_restore(cr);
        if(ps->busy) dt_control_busy_leave();
        ps->busy = FALSE;
      }
    }

    if(k == ps->selected || !dt_is_valid_imgid(img->imgid))
    {
      cairo_set_source_rgba(cr, .4, .4, .4, 1.0);
      _cairo_rectangle(cr, (k == ps->selected) ? ps->sel_controls : 0,
                       img->screen.x, img->screen.y,
                       img->screen.x + img->screen.width,
                       img->screen.y + img->screen.height);
      cairo_stroke(cr);
    }
    else if(k == ps->last_selected)
    {
      const double dash[] = { DT_PIXEL_APPLY_DPI(5.0), DT_PIXEL_APPLY_DPI(5.0) };
      cairo_set_line_width(cr, 1);
      cairo_set_dash(cr, dash, 1, DT_PIXEL_APPLY_DPI(5));
      cairo_set_source_rgba(cr, .6, .6, .6, 1.0);
      cairo_rectangle(cr,
                      img->screen.x,     img->screen.y,
                      img->screen.width, img->screen.height);
      cairo_stroke(cr);
    }

    if(k == ps->imgs.motion_over)
    {
      cairo_set_source_rgba(cr, .2, .2, .2, 1.0);
      cairo_rectangle(cr, img->screen.x, img->screen.y,
                      img->screen.width, img->screen.height);
      cairo_fill(cr);
    }
  }

  // now display new area if any
  if(ps->dragging || ps->selected != -1)
  {
    float dx1, dy1, dx2, dy2, dwidth, dheight; // displayed values
    float x1, y1, x2, y2;                      // box screen coordinates

    float pwidth, pheight;
    _get_page_dimension(&ps->prt, &pwidth, &pheight);

    if(ps->dragging)
    {
      x1      = ps->x1;
      y1      = ps->y1;
      x2      = ps->x2;
      y2      = ps->y2;

      dx1     = _hscreen_to_mm(ps, ps->x1, TRUE) * units[ps->unit];
      dy1     = _vscreen_to_mm(ps, ps->y1, TRUE) * units[ps->unit];
      dx2     = _hscreen_to_mm(ps, ps->x2, TRUE) * units[ps->unit];
      dy2     = _vscreen_to_mm(ps, ps->y2, TRUE) * units[ps->unit];
      dwidth  = fabsf(dx2 - dx1);
      dheight = fabsf(dy2 - dy1);
    }
    else
    {
      const dt_image_box *box = &ps->imgs.box[ps->selected];

      // we could use a simpler solution but we want to use the same formulae used
      // to fill the editable box values to avoid discrepancies between values due
      // to rounding errors.

      dx1     = _percent_unit_of(ps, pwidth, box->pos.x);
      dy1     = _percent_unit_of(ps, pheight, box->pos.y);
      dwidth  = _percent_unit_of(ps, pwidth, box->pos.width);
      dheight = _percent_unit_of(ps, pheight, box->pos.height);
      dx2     = dx1 + dwidth;
      dy2     = dy1 + dheight;

      x1 = box->screen.x;
      y1 = box->screen.y;
      x2 = box->screen.x + box->screen.width;
      y2 = box->screen.y + box->screen.height;
    }

    cairo_set_source_rgba(cr, .4, .4, .4, 1.0);
    _cairo_rectangle(cr, ps->sel_controls, x1, y1, x2, y2);

    // display corner coordinates
    // FIXME: here and elsewhere eliminate hardcoded RGB values -- use CSS
    char dimensions[16];
    PangoLayout *layout;
    PangoRectangle ext;
    PangoFontDescription *desc = dt_gui_get_font();
    pango_font_description_set_weight(desc, PANGO_WEIGHT_BOLD);
    pango_font_description_set_absolute_size(desc, DT_PIXEL_APPLY_DPI(16) * PANGO_SCALE);
    layout = pango_cairo_create_layout(cr);
    pango_layout_set_font_description(layout, desc);
    const double text_h = DT_PIXEL_APPLY_DPI(16+2);
    const double margin = DT_PIXEL_APPLY_DPI(6);
    const double dash = DT_PIXEL_APPLY_DPI(4.0);
    int n_digits;
    char *precision;
    _precision_by_unit(ps->unit, &n_digits, NULL, &precision);
    double xp, yp;

    yp = y1 + (y2 - y1 - text_h) * 0.5;

    if(x1 >= ps->imgs.screen.page.x
       && x1 <= (ps->imgs.screen.page.x + ps->imgs.screen.page.width))
    {
      snprintf(dimensions, sizeof(dimensions), precision, dx1);
      pango_layout_set_text(layout, dimensions, -1);
      pango_layout_get_pixel_extents(layout, NULL, &ext);
      xp = ps->imgs.screen.page.x
        + (x1 - text_h - ps->imgs.screen.page.x - ext.width) * 0.5;
      if(xp < ps->imgs.screen.page.x + 3 * margin)
      {
        xp = x1 + 2 * margin;
        // somewhat hacky, assumes that all numeric labels are about
        // the same width
        yp = MIN(y2 - text_h, yp + ext.width + 0.5 * text_h + margin * 3);
      }
      cairo_set_source_rgba(cr, .7, .7, .7, .9);
      cairo_move_to(cr, ps->imgs.screen.page.x, yp + text_h * 0.5);
      cairo_line_to(cr, x1, yp + text_h * 0.5);
      cairo_stroke_preserve(cr);
      cairo_set_source_rgba(cr, .5, .5, .5, .9);
      cairo_set_dash(cr, &dash, 1, dash);
      cairo_stroke(cr);
      cairo_set_dash(cr, NULL, 0, 0);
      dt_gui_draw_rounded_rectangle(cr,
                                    ext.width + 2 * margin,
                                    text_h + 2 * margin,
                                    xp - margin,
                                    yp - margin);
      cairo_set_source_rgb(cr, .8, .8, .8);
      cairo_move_to(cr, xp, yp);
      pango_cairo_show_layout(cr, layout);
    }

    if(x2 >= ps->imgs.screen.page.x
       && x2 <= (ps->imgs.screen.page.x + ps->imgs.screen.page.width))
    {
      snprintf(dimensions, sizeof(dimensions), precision, pwidth * units[ps->unit] - dx2);
      pango_layout_set_text(layout, dimensions, -1);
      pango_layout_get_pixel_extents(layout, NULL, &ext);
      xp = x2 + (ps->imgs.screen.page.x
                 + ps->imgs.screen.page.width - x2 - ext.width) * 0.5;
      if(xp + ext.width + margin > ps->imgs.screen.page.x + ps->imgs.screen.page.width)
        xp = x2 - ext.width - 2 * margin;
      cairo_set_source_rgba(cr, .7, .7, .7, .9);
      cairo_move_to(cr, x2, yp + text_h * 0.5);
      cairo_line_to(cr,
                    ps->imgs.screen.page.x + ps->imgs.screen.page.width,
                    yp + text_h * 0.5);
      cairo_stroke_preserve(cr);
      cairo_set_source_rgba(cr, .5, .5, .5, .9);
      cairo_set_dash(cr, &dash, 1, dash);
      cairo_stroke(cr);
      cairo_set_dash(cr, NULL, 0, 0);
      dt_gui_draw_rounded_rectangle(cr, ext.width + 2 * margin, text_h + 2 * margin,
                                    xp - margin, yp - margin);
      cairo_set_source_rgb(cr, .8, .8, .8);
      cairo_move_to(cr, xp, yp);
      pango_cairo_show_layout(cr, layout);
    }

    xp = x1 + (x2 - x1 - text_h) * 0.5;

    if(y1 >= ps->imgs.screen.page.y
       && y1 <= (ps->imgs.screen.page.y + ps->imgs.screen.page.height))
    {
      snprintf(dimensions, sizeof(dimensions), precision, dy1);
      pango_layout_set_text(layout, dimensions, -1);
      pango_layout_get_pixel_extents(layout, NULL, &ext);
      yp = ps->imgs.screen.page.y
        + (y1 - text_h - ps->imgs.screen.page.y - ext.width) * 0.5;
      if(yp < ps->imgs.screen.page.y + 3 * margin)
      {
        xp = MIN(x2 - text_h, xp + ext.width + 0.5 * text_h + margin * 3);
        yp = y1 + 2 * margin;
      }
      cairo_set_source_rgba(cr, .7, .7, .7, .9);
      cairo_move_to(cr, xp + text_h * 0.5, ps->imgs.screen.page.y);
      cairo_line_to(cr, xp + text_h * 0.5, y1);
      cairo_stroke_preserve(cr);
      cairo_set_source_rgba(cr, .5, .5, .5, .9);
      cairo_set_dash(cr, &dash, 1, dash);
      cairo_stroke(cr);
      cairo_set_dash(cr, NULL, 0, 0);
      dt_gui_draw_rounded_rectangle(cr, text_h + 2 * margin, ext.width + 2 * margin,
                                    xp - margin, yp - margin);
      cairo_set_source_rgb(cr, .8, .8, .8);
      cairo_move_to(cr, xp + text_h * 0.5, yp + ext.width * 0.5);
      cairo_save(cr);
      cairo_rotate(cr, -M_PI_2);
      cairo_rel_move_to(cr, -0.5 * ext.width, -0.5 * text_h);
      pango_cairo_update_layout(cr, layout);
      pango_cairo_show_layout(cr, layout);
      cairo_restore(cr);
    }

    if(y2 >= ps->imgs.screen.page.y
       && y2 <= (ps->imgs.screen.page.y + ps->imgs.screen.page.height))
    {
      snprintf(dimensions, sizeof(dimensions), precision, pheight * units[ps->unit] - dy2);
      pango_layout_set_text(layout, dimensions, -1);
      pango_layout_get_pixel_extents(layout, NULL, &ext);
      yp = y2 + (ps->imgs.screen.page.y
                 + ps->imgs.screen.page.height - y2 - ext.width) * 0.5;
      if(yp + ext.width + margin > ps->imgs.screen.page.y + ps->imgs.screen.page.height)
        yp = y2 - ext.width - 2 * margin;
      cairo_set_source_rgba(cr, .7, .7, .7, .9);
      cairo_move_to(cr, xp + text_h * 0.5, y2);
      cairo_line_to(cr,
                    xp + text_h * 0.5,
                    ps->imgs.screen.page.y + ps->imgs.screen.page.height);
      cairo_stroke_preserve(cr);
      cairo_set_source_rgba(cr, .5, .5, .5, .9);
      cairo_set_dash(cr, &dash, 1, dash);
      cairo_stroke(cr);
      cairo_set_dash(cr, NULL, 0, 0);
      dt_gui_draw_rounded_rectangle(cr, text_h + 2 * margin, ext.width + 2 * margin,
                                    xp - margin, yp - margin);
      cairo_set_source_rgb(cr, .8, .8, .8);
      cairo_move_to(cr, xp + text_h * 0.5, yp + ext.width * 0.5);
      cairo_save(cr);
      cairo_rotate(cr, -M_PI_2);
      cairo_rel_move_to(cr, -0.5 * ext.width, -0.5 * text_h);
      pango_cairo_update_layout(cr, layout);
      pango_cairo_show_layout(cr, layout);
      cairo_restore(cr);
    }

    // display width and height
    snprintf(dimensions, sizeof(dimensions), precision, dwidth);
    pango_layout_set_text(layout, dimensions, -1);
    pango_layout_get_pixel_extents(layout, NULL, &ext);
    xp = (x1 + x2 - ext.width) * .5;
    if(y1 > text_h * 0.5 + margin)
      yp = y1 - text_h * 0.5;
    else
      yp = y1 + text_h - 2 * margin;
    cairo_set_source_rgba(cr, .5, .5, .5, .9);
    dt_gui_draw_rounded_rectangle(cr, ext.width + 2 * margin, text_h + 2 * margin,
                                  xp - margin, yp - margin);
    cairo_set_source_rgb(cr, .8, .8, .8);
    cairo_move_to(cr, xp, yp);
    pango_cairo_show_layout(cr, layout);

    snprintf(dimensions, sizeof(dimensions), precision, dheight);
    pango_layout_set_text(layout, dimensions, -1);
    pango_layout_get_pixel_extents(layout, NULL, &ext);
    if(x1 > text_h * 0.5 + margin)
      xp = x1 - text_h * 0.5;
    else
      xp = x1 + text_h - 2 * margin;
    yp = (y1 + y2) * .5;
    cairo_set_source_rgba(cr, .5, .5, .5, .9);
    dt_gui_draw_rounded_rectangle(cr, text_h + 2 * margin, ext.width + 2 * margin,
                                  xp - margin, yp - margin - 0.5 * ext.width);
    cairo_set_source_rgb(cr, .8, .8, .8);
    cairo_move_to(cr, xp + text_h * 0.5, yp);
    cairo_save(cr);
    cairo_rotate(cr, -M_PI_2);
    cairo_rel_move_to(cr, -0.5 * ext.width, -0.5 * text_h);
    pango_cairo_update_layout(cr, layout);
    pango_cairo_show_layout(cr, layout);
    cairo_restore(cr);

    pango_font_description_free(desc);
    g_object_unref(layout);
    g_free(precision);
  }

  if(ps->imgs.screen.borderless)
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ps->borderless), TRUE);
  else
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ps->borderless), FALSE);
}

static void _width_changed(GtkWidget *widget, gpointer user_data)
{
  DT_GUARD_GUI_UPDATE();

  dt_lib_print_settings_t *ps = (dt_lib_print_settings_t *)user_data;

  const float nv = gtk_spin_button_get_value(GTK_SPIN_BUTTON(widget));
  const float nv_mm = nv / units[ps->unit];

  dt_image_box *box = &ps->imgs.box[ps->last_selected];

  dt_printing_setup_box(&ps->imgs, ps->last_selected,
                        box->screen.x, box->screen.y,
                        _mm_to_hscreen(ps, nv_mm, FALSE), box->screen.height);

  ps->has_changed = TRUE;
  dt_control_queue_redraw_center();
}

static void _height_changed(GtkWidget *widget,
                            gpointer user_data)
{
  DT_GUARD_GUI_UPDATE();

  dt_lib_print_settings_t *ps = (dt_lib_print_settings_t *)user_data;

  const float nv = gtk_spin_button_get_value(GTK_SPIN_BUTTON(widget));
  const float nv_mm = nv / units[ps->unit];

  dt_image_box *box = &ps->imgs.box[ps->last_selected];

  dt_printing_setup_box(&ps->imgs, ps->last_selected,
                        box->screen.x, box->screen.y,
                        box->screen.width, _mm_to_vscreen(ps, nv_mm, FALSE));

  ps->has_changed = TRUE;
  dt_control_queue_redraw_center();
}

static void _x_changed(GtkWidget *widget,
                       gpointer user_data)
{
  DT_GUARD_GUI_UPDATE();

  dt_lib_print_settings_t *ps = (dt_lib_print_settings_t *)user_data;

  const float nv = gtk_spin_button_get_value(GTK_SPIN_BUTTON(widget));
  const float nv_mm = nv / units[ps->unit];

  dt_image_box *box = &ps->imgs.box[ps->last_selected];

  dt_printing_setup_box(&ps->imgs, ps->last_selected,
                        _mm_to_hscreen(ps, nv_mm, TRUE), box->screen.y,
                        box->screen.width, box->screen.height);

  ps->has_changed = TRUE;
  dt_control_queue_redraw_center();
}

static void _y_changed(GtkWidget *widget,
                       gpointer user_data)
{
  DT_GUARD_GUI_UPDATE();

  dt_lib_print_settings_t *ps = (dt_lib_print_settings_t *)user_data;

  const float nv = gtk_spin_button_get_value(GTK_SPIN_BUTTON(widget));
  const float nv_mm = nv / units[ps->unit];

  dt_image_box *box = &ps->imgs.box[ps->last_selected];

  dt_printing_setup_box(&ps->imgs, ps->last_selected,
                        box->screen.x, _mm_to_vscreen(ps, nv_mm, TRUE),
                        box->screen.width, box->screen.height);

  ps->has_changed = TRUE;
  dt_control_queue_redraw_center();
}

void gui_init(dt_lib_module_t *self)
{
  dt_lib_print_settings_t *d = malloc(sizeof(dt_lib_print_settings_t));
  self->data = d;
  self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
  dt_gui_add_help_link(self->widget, "print_overview");

  GtkWidget *label;

  d->paper_list = NULL;
  d->media_list = NULL;
#ifdef _WIN32
  d->settings_ctx = NULL;
#endif
  d->unit = 0;
  d->width = d->height = NULL;
  d->v_piccprofile = NULL;
  d->v_iccprofile = NULL;
  d->v_style = NULL;
  d->creation = d->dragging = FALSE;
  d->selected = -1;
  d->last_selected = -1;
  d->has_changed = FALSE;

  d->printer_list = NULL;
  dt_pthread_mutex_init(&d->printer_list_mutex);

  dt_init_print_info(&d->prt);
  dt_view_print_settings(darktable.view_manager, &d->prt, &d->imgs);

  d->profiles = _get_profiles();

  d->imgs.motion_over = -1;

  // Initialize the new async tracking field (Windows only)
#ifdef _WIN32
  d->waiting_for_printer = NULL;
#endif

  const char *str = dt_conf_get_string_const(PRINT_CONFIG_PREFIX "unit");
  const char **names = _unit_names;
  for(_unit_t i=0; *names; names++, i++)
    if(g_strcmp0(str, *names) == 0)
      d->unit = i;

  dt_printing_clear_boxes(&d->imgs);

  // set all margins + unit from settings

  const float top_b = dt_conf_get_float(PRINT_CONFIG_PREFIX "top_margin");
  const float bottom_b = dt_conf_get_float(PRINT_CONFIG_PREFIX "bottom_margin");
  const float left_b = dt_conf_get_float(PRINT_CONFIG_PREFIX "left_margin");
  const float right_b = dt_conf_get_float(PRINT_CONFIG_PREFIX "right_margin");

  d->prt.page.margin_top = _to_mm(d, top_b);
  d->prt.page.margin_bottom = _to_mm(d, bottom_b);
  d->prt.page.margin_left = _to_mm(d, left_b);
  d->prt.page.margin_right = _to_mm(d, right_b);

  //  create the spin-button now as values could be set when the
  //  printer has no hardware margin

  // FIXME: set digits/increments on all of these by calling _unit_changed() later?
  int n_digits;
  float incr;
  _precision_by_unit(d->unit, &n_digits, &incr, NULL);

  d->b_top    = gtk_spin_button_new_with_range(0, 1000, incr);
  d->b_left   = gtk_spin_button_new_with_range(0, 1000, incr);
  d->b_right  = gtk_spin_button_new_with_range(0, 1000, incr);
  d->b_bottom = gtk_spin_button_new_with_range(0, 1000, incr);
  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(d->b_top),    n_digits);
  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(d->b_bottom), n_digits);
  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(d->b_left),   n_digits);
  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(d->b_right),  n_digits);

  d->b_x      = gtk_spin_button_new_with_range(0, 1000, incr);
  d->b_y      = gtk_spin_button_new_with_range(0, 1000, incr);
  d->b_width  = gtk_spin_button_new_with_range(0, 1000, incr);
  d->b_height = gtk_spin_button_new_with_range(0, 1000, incr);
  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(d->b_x), n_digits);
  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(d->b_y), n_digits);
  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(d->b_width), n_digits);
  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(d->b_height), n_digits);

  d->grid_size = gtk_spin_button_new_with_range(0, 100, incr);
  gtk_spin_button_set_digits(GTK_SPIN_BUTTON(d->grid_size),  n_digits);

  gtk_entry_set_alignment(GTK_ENTRY(d->b_top), 1);
  gtk_entry_set_alignment(GTK_ENTRY(d->b_left), 1);
  gtk_entry_set_alignment(GTK_ENTRY(d->b_right), 1);
  gtk_entry_set_alignment(GTK_ENTRY(d->b_bottom), 1);

  gtk_entry_set_alignment(GTK_ENTRY(d->b_x), 1);
  gtk_entry_set_alignment(GTK_ENTRY(d->b_y), 1);
  gtk_entry_set_alignment(GTK_ENTRY(d->b_width), 1);
  gtk_entry_set_alignment(GTK_ENTRY(d->b_height), 1);

  gtk_entry_set_alignment(GTK_ENTRY(d->grid_size), 1);


  ////////////////////////// PRINTER SETTINGS

  // create papers combo as filled when adding printers
  d->papers = dt_bauhaus_combobox_new_action(DT_ACTION(self));

  label = dt_ui_section_label_new(C_("section", "printer"));
  gtk_box_pack_start(GTK_BOX(self->widget), label, TRUE, TRUE, 0);
  dt_gui_add_help_link(self->widget, "print_settings_printer");
  d->printers = dt_bauhaus_combobox_new_action(DT_ACTION(self));

  gtk_box_pack_start(GTK_BOX(self->widget), d->printers, TRUE, TRUE, 0);
  g_signal_connect(G_OBJECT(d->printers), "value-changed",
                   G_CALLBACK(_printer_changed), self);

  //// media

  d->media = dt_bauhaus_combobox_new_action(DT_ACTION(self));

  dt_bauhaus_widget_set_label(d->media, N_("printer"), N_("media"));

  g_signal_connect(G_OBJECT(d->media), "value-changed",
                   G_CALLBACK(_media_changed), self);
#ifdef _WIN32  
  //TODO:  Wire up media selection for windows, for now hide and let media be set in printer settings dialog
#else
  gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(d->media), TRUE, TRUE, 0);
#endif


  //  Add printer profile combo

  d->pprofile = dt_bauhaus_combobox_new_action(DT_ACTION(self));
  dt_bauhaus_widget_set_label(d->pprofile, N_("printer"), N_("profile"));

  int combo_idx, n;

  gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(d->pprofile), TRUE, TRUE, 0);
  int printer_profile_type =
    dt_conf_get_int("plugins/print/printer/icctype");
  const char *printer_profile =
    dt_conf_get_string_const("plugins/print/printer/iccprofile");
  combo_idx = -1;
  n = 0;

  dt_bauhaus_combobox_add(d->pprofile, _("color management in printer driver"));
  for(const GList *l = d->profiles; l; l = g_list_next(l))
  {
    dt_lib_export_profile_t *prof = l->data;
    // do not add built-in profiles, these are in no way for printing
    if(prof->type == DT_COLORSPACE_FILE)
    {
      dt_bauhaus_combobox_add(d->pprofile, prof->name);
      prof->ppos = ++n;
      if(prof->type == printer_profile_type
         && (prof->type != DT_COLORSPACE_FILE
             || !g_strcmp0(prof->filename, printer_profile)))
      {
        g_free(d->v_piccprofile);
        d->v_picctype = printer_profile_type;
        d->v_piccprofile = g_strdup(printer_profile);
        combo_idx = n;
      }
    }
  }

  // profile not found, maybe a profile has been removed? revert to none
  if(combo_idx == -1)
  {
    dt_conf_set_int("plugins/print/printer/icctype", DT_COLORSPACE_NONE);
    dt_conf_set_string("plugins/print/printer/iccprofile", "");
    g_free(d->v_piccprofile);
    d->v_picctype = DT_COLORSPACE_NONE;
    d->v_piccprofile = g_strdup("");
    combo_idx = 0;
  }
  dt_bauhaus_combobox_set(d->pprofile, combo_idx);

  char *tooltip = dt_ioppr_get_location_tooltip("out", _("printer ICC profiles"));
  gtk_widget_set_tooltip_markup(d->pprofile, tooltip);
  g_free(tooltip);

  g_signal_connect(G_OBJECT(d->pprofile), "value-changed",
                   G_CALLBACK(_printer_profile_changed), (gpointer)self);

  //  Add printer intent combo

  d->v_pintent = dt_conf_get_int("plugins/print/printer/iccintent");
  DT_BAUHAUS_COMBOBOX_NEW_FULL(d->pintent, self, N_("printer"), N_("intent"), NULL,
                               d->v_pintent, _printer_intent_callback, self,
                               N_("perceptual"),
                               N_("relative colorimetric"),
                               NC_("rendering intent", "saturation"),
                               N_("absolute colorimetric"));
  gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(d->pintent), TRUE, TRUE, 0);

  d->prt.printer.intent = d->v_pintent;

  d->black_point_compensation =
    gtk_check_button_new_with_label(_("black point compensation"));
  gtk_box_pack_start(GTK_BOX(self->widget),
                     GTK_WIDGET(d->black_point_compensation), TRUE, FALSE, 0);
  g_signal_connect(d->black_point_compensation, "toggled",
                   G_CALLBACK(_printer_bpc_callback), (gpointer)self);

  d->v_black_point_compensation =
    dt_conf_get_bool(PRINT_CONFIG_PREFIX "black_point_compensation");
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->black_point_compensation),
                               d->v_black_point_compensation);

  gtk_widget_set_tooltip_text
    (d->black_point_compensation,
     _("activate black point compensation when applying the printer profile"));

  gtk_widget_set_sensitive(GTK_WIDGET(d->black_point_compensation),
                           combo_idx == 0 ? FALSE : TRUE);

  ////////////////////////// PAGE SETTINGS

  label = dt_ui_section_label_new(C_("section", "page"));
  gtk_box_pack_start(GTK_BOX(self->widget), label, TRUE, TRUE, 0);
  dt_gui_add_help_link(self->widget, "print_settings_page");

  //// papers

  dt_bauhaus_widget_set_label(d->papers, NULL, N_("paper size"));

  g_signal_connect(G_OBJECT(d->papers), "value-changed", G_CALLBACK(_paper_changed), self);
  gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(d->papers), TRUE, TRUE, 0);

  //// portrait / landscape

  DT_BAUHAUS_COMBOBOX_NEW_FULL(d->orientation, self, NULL, N_("orientation"), NULL,
                               d->prt.page.landscape?1:0, _orientation_changed, self,
                               N_("portrait"), N_("landscape"));
  gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(d->orientation), TRUE, TRUE, 0);

  // NOTE: units has no label, which makes for cleaner UI but means
  // that no action can be assigned
  GtkWidget *ucomb =
    dt_bauhaus_combobox_new_full(DT_ACTION(self), NULL, NULL,
                                 _("measurement units"),
                                 d->unit, (GtkCallback)_unit_changed, self,
                                 _unit_names);
  gtk_box_pack_start(GTK_BOX(self->widget), ucomb, TRUE, TRUE, 0);

  //// image dimensions, create them now as we need them

  GtkWidget *hboxdim = gtk_box_new(GTK_ORIENTATION_HORIZONTAL,0);
  label = gtk_label_new(_("image width/height"));
  gtk_box_pack_start(GTK_BOX(hboxdim), GTK_WIDGET(label), TRUE, TRUE,
                     DT_PIXEL_APPLY_DPI(3));
  d->width = gtk_label_new(_("width"));
  gtk_box_pack_start(GTK_BOX(hboxdim), GTK_WIDGET(d->width), TRUE, TRUE, 0);
  label = gtk_label_new(_(" x "));
  gtk_box_pack_start(GTK_BOX(hboxdim), GTK_WIDGET(label), TRUE, TRUE, 0);
  d->height = gtk_label_new(_("height"));
  gtk_box_pack_start(GTK_BOX(hboxdim), GTK_WIDGET(d->height), TRUE, TRUE, 0);

  //// image information (downscale/upscale)

  GtkWidget *hboxinfo = gtk_box_new(GTK_ORIENTATION_HORIZONTAL,0);
  label = gtk_label_new(_("scale factor"));
  gtk_box_pack_start(GTK_BOX(hboxinfo), GTK_WIDGET(label), TRUE, TRUE,
                     DT_PIXEL_APPLY_DPI(3));
  d->info = gtk_label_new("1.0");
  gtk_box_pack_start(GTK_BOX(hboxinfo), GTK_WIDGET(d->info), TRUE, TRUE, 0);
  gtk_widget_set_tooltip_text(hboxinfo,
               _("image scale factor from native printer DPI:\n"
                 " < 1 means that it is downscaled (best quality)\n"
                 " > 1 means that the image is upscaled\n"
                 " a too large value may result in poor print quality"));

  //// borders

  GtkGrid *bds = GTK_GRID(gtk_grid_new());
  gtk_grid_set_row_spacing(bds, DT_PIXEL_APPLY_DPI(3));
  gtk_grid_set_column_spacing(bds, DT_PIXEL_APPLY_DPI(3));

  d->lock_activated = FALSE;

  //d->b_top  = gtk_spin_button_new_with_range(0, 10000, 1);
  gtk_widget_set_tooltip_text(GTK_WIDGET(d->b_top), _("top margin"));
  gtk_grid_attach(bds, GTK_WIDGET(d->b_top), 1, 0, 1, 1);

  //d->b_left  = gtk_spin_button_new_with_range(0, 10000, 1);
  gtk_widget_set_tooltip_text(GTK_WIDGET(d->b_left), _("left margin"));
  gtk_grid_attach(bds, GTK_WIDGET(d->b_left), 0, 1, 1, 1);

  d->lock_button = GTK_TOGGLE_BUTTON(gtk_toggle_button_new_with_label(_("lock")));
  gtk_widget_set_tooltip_text(GTK_WIDGET(d->lock_button),
                              _("change all margins uniformly"));
  gtk_grid_attach(bds, GTK_WIDGET(d->lock_button), 1, 1, 1, 1);

  //d->b_right  = gtk_spin_button_new_with_range(0, 10000, 1);
  gtk_widget_set_tooltip_text(GTK_WIDGET(d->b_right), _("right margin"));
  gtk_grid_attach(bds, GTK_WIDGET(d->b_right), 2, 1, 1, 1);

  //d->b_bottom  = gtk_spin_button_new_with_range(0, 10000, 1);
  gtk_widget_set_tooltip_text(GTK_WIDGET(d->b_bottom), _("bottom margin"));
  gtk_grid_attach(bds, GTK_WIDGET(d->b_bottom), 1, 2, 1, 1);

  gtk_widget_set_halign(GTK_WIDGET(bds), GTK_ALIGN_CENTER);
  gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(bds), TRUE, TRUE, 0);

  gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->b_top), top_b);
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->b_bottom), bottom_b);
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->b_left), left_b);
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->b_right), right_b);

  g_signal_connect(G_OBJECT (d->b_top), "value-changed",
                   G_CALLBACK (_top_border_callback), self);
  g_signal_connect(G_OBJECT (d->b_bottom), "value-changed",
                   G_CALLBACK (_bottom_border_callback), self);
  g_signal_connect(G_OBJECT (d->b_left), "value-changed",
                   G_CALLBACK (_left_border_callback), self);
  g_signal_connect(G_OBJECT (d->b_right), "value-changed",
                   G_CALLBACK (_right_border_callback), self);
  g_signal_connect(G_OBJECT(d->lock_button), "toggled",
                   G_CALLBACK(_lock_callback), self);

  gtk_widget_set_halign(GTK_WIDGET(hboxdim), GTK_ALIGN_CENTER);
  gtk_widget_set_halign(GTK_WIDGET(hboxinfo), GTK_ALIGN_CENTER);

  const gboolean lock_active = dt_conf_get_bool(PRINT_CONFIG_PREFIX "lock_borders");
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->lock_button), lock_active);

  // grid & snap grid
  {
    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);

    d->grid = gtk_check_button_new_with_label(_("display grid"));
    // d->grid_size = gtk_spin_button_new_with_range(0, 100, 0.1);
    gtk_box_pack_start(GTK_BOX(hbox), GTK_WIDGET(d->grid), TRUE, TRUE, 0);
    gtk_box_pack_start(GTK_BOX(hbox), GTK_WIDGET(d->grid_size), TRUE, TRUE, 0);

    gtk_spin_button_set_value
      (GTK_SPIN_BUTTON(d->grid_size),
       dt_conf_get_float(PRINT_CONFIG_PREFIX "grid_size") * units[d->unit]);

    gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(hbox), TRUE, TRUE, 0);

    d->snap_grid = gtk_check_button_new_with_label(_("snap to grid"));
    gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(d->snap_grid), TRUE, TRUE, 0);

    gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(vbox), TRUE, TRUE, 0);

    g_signal_connect(G_OBJECT(d->grid_size), "value-changed",
                     G_CALLBACK(_grid_size_changed), self);
    g_signal_connect(G_OBJECT(d->grid), "toggled",
                     G_CALLBACK(_grid_callback), self);
    g_signal_connect(d->snap_grid, "toggled",
                     G_CALLBACK(_snap_grid_callback), (gpointer)self);
  }

  d->borderless = gtk_check_button_new_with_label(_("borderless mode required"));
  gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(d->borderless), TRUE, TRUE, 0);
  gtk_widget_set_tooltip_text(d->borderless,
                              _("indicates that the borderless mode should be activated\n"
                                "in the printer driver because the selected margins are\n"
                                "below the printer hardware margins"));
  gtk_widget_set_sensitive(d->borderless, FALSE);

  // pack image dimension hbox here

  label = dt_ui_section_label_new(C_("section", "image layout"));
  gtk_box_pack_start(GTK_BOX(self->widget), label, TRUE, TRUE, 0);
  dt_gui_add_help_link(self->widget, "print_image_layout");

  gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(hboxdim), TRUE, TRUE, 0);
  gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(hboxinfo), TRUE, TRUE, 0);

  //// alignments

  // Auto-fit: Create the 3x3 gtk table toggle button table...
  GtkGrid *bat = GTK_GRID(gtk_grid_new());
  gtk_grid_set_row_spacing(bat, DT_PIXEL_APPLY_DPI(3));
  gtk_grid_set_column_spacing(bat, DT_PIXEL_APPLY_DPI(3));
  for(int i=0; i<9; i++)
  {
    d->dtba[i]
        = DTGTK_TOGGLEBUTTON(dtgtk_togglebutton_new(dtgtk_cairo_paint_alignment,
                                                    (CPF_SPECIAL_FLAG << i), NULL));
    gtk_grid_attach (GTK_GRID (bat), GTK_WIDGET (d->dtba[i]), (i%3), i/3, 1, 1);
    g_signal_connect (G_OBJECT (d->dtba[i]), "toggled",
                      G_CALLBACK (_alignment_callback), self);
  }

  GtkWidget *hbox22 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL,0);
  GtkWidget *label4 = gtk_label_new(_("alignment"));
  gtk_box_pack_start(GTK_BOX(hbox22),GTK_WIDGET(label4),TRUE,TRUE,0);
  gtk_box_pack_start(GTK_BOX(hbox22), GTK_WIDGET(bat), TRUE, TRUE, 0);
  gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(hbox22), TRUE, TRUE, 0);

  // Manual fit
  GtkWidget *hfitbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);

  GtkWidget *mfitbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);

  GtkGrid *fitbut = GTK_GRID(gtk_grid_new());
  gtk_grid_set_row_spacing(fitbut, DT_PIXEL_APPLY_DPI(3));
  gtk_grid_set_column_spacing(fitbut, DT_PIXEL_APPLY_DPI(3));
  gtk_grid_set_column_homogeneous(fitbut, TRUE);
  gtk_grid_set_row_homogeneous(fitbut, TRUE);

  GtkWidget *bnew = dt_action_button_new
    (self, N_("new image area"),
     _page_new_area_clicked, self,
     _("add a new image area on the page\n"
       "click and drag on the page to place the area\n"
       "drag and drop image from film strip on it"), 0, 0);

  d->del = dt_action_button_new(self, N_("delete image area"),
                                _page_delete_area_clicked, self,
                                _("delete the currently selected image area"), 0, 0);
  gtk_widget_set_sensitive(d->del, FALSE);

  GtkWidget *bclear = dt_action_button_new(self, N_("clear layout"),
                                           _page_clear_area_clicked, self,
                                           _("remove all image areas from the page"),
                                           0, 0);

  gtk_grid_attach(fitbut, GTK_WIDGET(bnew), 0, 0, 2, 1);
  gtk_grid_attach(fitbut, GTK_WIDGET(d->del), 0, 1, 1, 1);
  gtk_grid_attach(fitbut, GTK_WIDGET(bclear), 1, 1, 1, 1);

  gtk_box_pack_start(GTK_BOX(mfitbox), GTK_WIDGET(fitbut), TRUE, TRUE, 0);
  gtk_box_pack_start(GTK_BOX(hfitbox), GTK_WIDGET(mfitbox), TRUE, TRUE, 0);

  // X x Y
  GtkWidget *box;
  // FIXME: add labels to x/y/width/height as otherwise are obscure --
  // and there is the horizontal space to do this

  box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL,0);
  // d->b_x = gtk_spin_button_new_with_range(0, 1000, 1);
  gtk_widget_set_tooltip_text(d->b_x, _("image area x origin (in current unit)"));
  gtk_entry_set_width_chars(GTK_ENTRY(d->b_x), 5);

  // d->b_y = gtk_spin_button_new_with_range(0, 1000, 1);
  gtk_widget_set_tooltip_text(d->b_y, _("image area y origin (in current unit)"));
  gtk_entry_set_width_chars(GTK_ENTRY(d->b_y), 5);

  gtk_box_pack_start(GTK_BOX(box), GTK_WIDGET(d->b_x), TRUE, TRUE, 0);
  gtk_box_pack_start(GTK_BOX(box), GTK_WIDGET(d->b_y), TRUE, TRUE, 0);

  gtk_box_pack_start(GTK_BOX(hfitbox), GTK_WIDGET(box), TRUE, TRUE, 0);

  // width x height
  box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL,0);
  // d->b_width = gtk_spin_button_new_with_range(0, 1000, 1);
  gtk_widget_set_tooltip_text(d->b_width, _("image area width (in current unit)"));
  gtk_entry_set_width_chars(GTK_ENTRY(d->b_width), 5);

  // d->b_height = gtk_spin_button_new_with_range(0, 1000, 1);
  gtk_widget_set_tooltip_text(d->b_height, _("image area height (in current unit)"));
  gtk_entry_set_width_chars(GTK_ENTRY(d->b_height), 5);

  gtk_box_pack_start(GTK_BOX(box), GTK_WIDGET(d->b_width), TRUE, TRUE, 0);
  gtk_box_pack_start(GTK_BOX(box), GTK_WIDGET(d->b_height), TRUE, TRUE, 0);

  gtk_box_pack_start(GTK_BOX(hfitbox), GTK_WIDGET(box), TRUE, TRUE, 0);

  gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(hfitbox), TRUE, TRUE, 0);

  gtk_widget_add_events(d->b_x, GDK_BUTTON_PRESS_MASK);
  gtk_widget_add_events(d->b_y, GDK_BUTTON_PRESS_MASK);
  gtk_widget_add_events(d->b_width, GDK_BUTTON_PRESS_MASK);
  gtk_widget_add_events(d->b_height, GDK_BUTTON_PRESS_MASK);

  g_signal_connect(G_OBJECT(d->b_x), "value-changed",
                   G_CALLBACK(_x_changed), (gpointer)d);
  g_signal_connect(G_OBJECT(d->b_y), "value-changed",
                   G_CALLBACK(_y_changed), (gpointer)d);
  g_signal_connect(G_OBJECT(d->b_width), "value-changed",
                   G_CALLBACK(_width_changed), (gpointer)d);
  g_signal_connect(G_OBJECT(d->b_height), "value-changed",
                   G_CALLBACK(_height_changed), (gpointer)d);

  ////////////////////////// PRINT SETTINGS

  label = dt_ui_section_label_new(C_("section", "print settings"));
  gtk_box_pack_start(GTK_BOX(self->widget), label, TRUE, TRUE, 0);
  dt_gui_add_help_link(self->widget, "print_settings");

  d->profile = dt_bauhaus_combobox_new_action(DT_ACTION(self));
  dt_bauhaus_widget_set_label(d->profile, NULL, N_("profile"));
  gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(d->profile), TRUE, TRUE, 0);
  //  Add export profile combo
#ifdef _WIN32
  _rebuild_image_profile_combo(d, combo_idx == 0);   // combo_idx here refers to d->pprofile's just-established initial position
#else
  dt_bauhaus_combobox_add(d->profile, _("image settings"));

  const int icctype = dt_conf_get_int(PRINT_CONFIG_PREFIX "icctype");
  const gchar *iccprofile = dt_conf_get_string_const(PRINT_CONFIG_PREFIX "iccprofile");
  combo_idx = -1;
  n = 0;

  for(const GList *l = d->profiles; l; l = g_list_next(l))
  {
    dt_lib_export_profile_t *prof = l->data;
    dt_bauhaus_combobox_add(d->profile, prof->name);
    prof->pos = ++n;
    if(prof->type == icctype
       && (prof->type != DT_COLORSPACE_FILE || !g_strcmp0(prof->filename, iccprofile)))
    {
      g_free(d->v_iccprofile);
      d->v_icctype = icctype;
      d->v_iccprofile = g_strdup(iccprofile);
      combo_idx = n;
    }
  }

  if(combo_idx == -1)
  {
    dt_conf_set_int(PRINT_CONFIG_PREFIX "icctype", DT_COLORSPACE_NONE);
    dt_conf_set_string(PRINT_CONFIG_PREFIX "iccprofile", "");
    g_free(d->v_iccprofile);
    d->v_icctype = DT_COLORSPACE_NONE;
    d->v_iccprofile = g_strdup("");
    combo_idx = 0;
  }

  dt_bauhaus_combobox_set(d->profile, combo_idx);
#endif

  tooltip = dt_ioppr_get_location_tooltip("out", _("output ICC profiles"));
  gtk_widget_set_tooltip_markup(d->profile, tooltip);
  g_free(tooltip);

  g_signal_connect(G_OBJECT(d->profile), "value-changed",
                   G_CALLBACK(_profile_changed), (gpointer)self);

  //  Add export intent combo

  DT_BAUHAUS_COMBOBOX_NEW_FULL(d->intent, self, NULL, N_("intent"), NULL,
                               dt_conf_get_int(PRINT_CONFIG_PREFIX "iccintent") + 1,
                               _intent_callback, self,
                               N_("image settings"),
                               N_("perceptual"),
                               N_("relative colorimetric"),
                               NC_("rendering intent", "saturation"),
                               N_("absolute colorimetric"));
  gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(d->intent), TRUE, TRUE, 0);

  //  Add export style combo

  const char *current_style_name = dt_conf_get_string_const(PRINT_CONFIG_PREFIX "style");

  GtkWidget *styles_button = dtgtk_button_new_full(dtgtk_cairo_paint_styles, 0, NULL,
      &(dtgtk_button_config_t){
        .tooltip = _("select style to be applied on printing"),
        .clicked_cb = G_CALLBACK(_style_popupmenu_callback),
        .clicked_data = (gpointer)d,
      });
  gtk_widget_set_halign(styles_button,GTK_ALIGN_END);
  GtkBox *style_box = (GtkBox*)gtk_box_new(GTK_ORIENTATION_HORIZONTAL,0);
  gtk_widget_set_tooltip_text(GTK_WIDGET(style_box), _("temporary style to use while printing"));
  GtkWidget *styles_label = gtk_label_new(_("style"));
  gtk_box_pack_start(style_box, styles_label, FALSE, FALSE, 0);
  GtkWidget *current_style = gtk_label_new("");
  gtk_widget_set_halign(current_style,GTK_ALIGN_END);
  gtk_label_set_justify(GTK_LABEL(current_style), GTK_JUSTIFY_RIGHT);
  gtk_label_set_ellipsize(GTK_LABEL(current_style), PANGO_ELLIPSIZE_MIDDLE);
  gtk_box_pack_start(style_box, current_style, TRUE, TRUE, 0);
  gtk_box_pack_start(style_box, styles_button, FALSE, FALSE, 0);

  d->style = GTK_WIDGET(current_style);
  gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(style_box), FALSE, TRUE, 0);

  //  Whether to add/replace style items

  d->v_style_append = dt_conf_get_bool(PRINT_CONFIG_PREFIX "style_append");
  DT_BAUHAUS_COMBOBOX_NEW_FULL
    (d->style_mode, self, NULL, N_("mode"),
     _("whether the style items are appended to the history or replacing the history"),
     d->v_style_append?1:0, _style_mode_changed, self,
     N_("replace history"), N_("append history"));
  gtk_widget_set_no_show_all(d->style_mode, TRUE);
  const gboolean is_style_set = _is_style_set(current_style_name);
  gtk_widget_set_visible(GTK_WIDGET(d->style_mode), is_style_set);
  gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(d->style_mode), TRUE, TRUE, 0);

  _update_style_label(d, current_style_name ? current_style_name : "");

#ifdef _WIN32
  GtkWidget *settings_button = dt_action_button_new(self, N_("print settings"),
                                           _print_settings_button_clicked, self,
                                           _("open printer settings"),
                                           GDK_KEY_s, GDK_CONTROL_MASK);
  d->print_settings_button = GTK_BUTTON(settings_button);
  gtk_box_pack_start(GTK_BOX(self->widget), settings_button, TRUE, TRUE, 0);
  // dt_gui_add_help_link(settings_button, "print_settings_button");
#endif

  // Print button
  // We use context here because it is an action button, and translators might
  // translate "print" as a verb, whereas elsewhere in the UI it is a noun.
  GtkWidget *button = dt_action_button_new(self, NC_("actionbutton", "print"),
                                           _print_button_clicked, self,
                                           _("print with current settings"),
                                           GDK_KEY_p, GDK_CONTROL_MASK);
  d->print_button = GTK_BUTTON(button);
  gtk_box_pack_start(GTK_BOX(self->widget), button, TRUE, TRUE, 0);
  dt_gui_add_help_link(button, "print_settings_button");

  // Let's start the printer discovery now

#ifdef _WIN32
  // Start discovery with both callbacks
  dt_win_printers_discovery(_new_printer_callback, _printer_ready_cb, self);
#else
  // Linux path: just the discovery callback
  dt_printers_discovery(_new_printer_callback, self);
#endif
}

void *legacy_params(dt_lib_module_t *self,
                    const void *const old_params,
                    const size_t old_params_size,
                    const int old_version,
                    int *new_version,
                    size_t *new_size)
{
  if(old_version == 1)
  {
    // we added the profile type
    //
    // old format:
    //   char *printer
    //   char *paper
    //   int32_t landscape
    //   char *f_profile
    //   int32_t intent
    //   char *f_pprofile
    //   <rest>
    //
    // new format:
    //   char *printer
    //   char *paper
    //   int32_t landscape
    //   int32_t f_profile_type
    //   char *f_profile
    //   int32_t intent
    //   int32_t f_pprofile_type
    //   char *f_pprofile
    //   <rest>

    const char *buf = (const char *)old_params;

    // printer
    const char *printer = buf;
    const int32_t printer_len = strlen(printer) + 1;
    buf += printer_len;

    // paper
    const char *paper = buf;
    const int32_t paper_len = strlen(paper) + 1;
    buf += paper_len;

    // landscape
    const int32_t landscape = *(int32_t *)buf;
    buf +=  sizeof(int32_t);

    // profile
    const char *profile = buf;
    const int32_t profile_len = strlen(profile) + 1;
    buf += profile_len;

    // intent
    const int32_t intent = *(int32_t *)buf;
    buf += sizeof(int32_t);

    // pprofile
    const char *pprofile = buf;
    const int32_t pprofile_len = strlen(pprofile) + 1;
    buf += pprofile_len;


    // now we got all fields from the start of the buffer and buf
    // points to the beginning or <rest>

    // find the new values for the two profiles
    dt_colorspaces_color_profile_type_t profile_type, pprofile_type;
    const char *profile_filename = "", *pprofile_filename = "";

    if(*profile == '\0' || !g_strcmp0(profile, "none"))
    {
      profile_type = DT_COLORSPACE_NONE;
    }
    else if(!g_strcmp0(profile, "sRGB"))
    {
      profile_type = DT_COLORSPACE_SRGB;
    }
    else if(!g_strcmp0(profile, "adobergb"))
    {
      profile_type = DT_COLORSPACE_ADOBERGB;
    }
    else
    {
      profile_type = DT_COLORSPACE_FILE;
      profile_filename = &profile[1]; // the old code had a '/' in the beginning
    }

    // in theory pprofile can't be srgb or adobergb, but checking for them won't hurt
    if(*pprofile == '\0')
    {
      pprofile_type = DT_COLORSPACE_NONE;
    }
    else if(!g_strcmp0(pprofile, "sRGB"))
    {
      pprofile_type = DT_COLORSPACE_SRGB;
    }
    else if(!g_strcmp0(pprofile, "adobergb"))
    {
      pprofile_type = DT_COLORSPACE_ADOBERGB;
    }
    else
    {
      pprofile_type = DT_COLORSPACE_FILE;
      pprofile_filename = &pprofile[1]; // the old code had a '/' in the beginning
    }

    const int32_t new_profile_len = strlen(profile_filename) + 1;
    const int32_t new_pprofile_len = strlen(pprofile_filename) + 1;

    // now we got everything to reassemble the new params
    size_t new_params_size = old_params_size - profile_len - pprofile_len;
    new_params_size += 2 * sizeof(dt_colorspaces_color_profile_type_t);
    new_params_size += new_profile_len + new_pprofile_len;
    void *new_params = malloc(new_params_size);

    size_t pos = 0;
    //   char *printer
    memcpy((uint8_t *)new_params + pos, printer, printer_len);
    pos += printer_len;
    //   char *paper
    memcpy((uint8_t *)new_params + pos, paper, paper_len);
    pos += paper_len;
    //   int32_t landscape
    memcpy((uint8_t *)new_params + pos, &landscape, sizeof(int32_t));
    pos += sizeof(int32_t);
    //   int32_t f_profile_type
    memcpy((uint8_t *)new_params + pos, &profile_type, sizeof(int32_t));
    pos += sizeof(int32_t);
    //   char *f_profile
    memcpy((uint8_t *)new_params + pos, profile_filename, new_profile_len);
    pos += new_profile_len;
    //   int32_t intent
    memcpy((uint8_t *)new_params + pos, &intent, sizeof(int32_t));
    pos += sizeof(int32_t);
    //   int32_t f_pprofile_type
    memcpy((uint8_t *)new_params + pos, &pprofile_type, sizeof(int32_t));
    pos += sizeof(int32_t);
    //   char *f_pprofile
    memcpy((uint8_t *)new_params + pos, pprofile_filename, new_pprofile_len);
    pos += new_pprofile_len;
    //   <rest>
    memcpy((uint8_t *)new_params + pos,
           buf,
           old_params_size - ((char *)buf - (char *)old_params));

    *new_size = new_params_size;
    *new_version = 2;
    return new_params;
  }
  else if(old_version == 2)
  {
    // add upscale to params
    size_t new_params_size = old_params_size + 1;
    void *new_params = calloc(1, new_params_size);

    memcpy(new_params, old_params, old_params_size);
    // no media type specified
    ((char *)new_params)[old_params_size] = '\0';

    *new_size = new_params_size;
    *new_version = 3;
    return new_params;
  }
  else if(old_version == 3)
  {
    // no box
    size_t new_params_size = old_params_size + sizeof(int32_t) + 4 * sizeof(float);
    void *new_params = calloc(1, new_params_size);

    memcpy(new_params, old_params, old_params_size);

    // single image box specified (there is no way to create a box on the size
    // of the page at this stage).
    int32_t idx = old_params_size;
    *(int32_t *)((uint8_t *)new_params + idx) = 1;
    idx += sizeof(int32_t);
    *(float *)((uint8_t *)new_params + idx) = 0.05f;
    idx += sizeof(float);
    *(float *)((uint8_t *)new_params + idx) = 0.05f;
    idx += sizeof(float);
    *(float *)((uint8_t *)new_params + idx) = 0.90f;
    idx += sizeof(float);
    *(float *)((uint8_t *)new_params + idx) = 0.90f;
    // idx += sizeof(float);

    *new_size = new_params_size;
    *new_version = 4;
    return new_params;
  }

  return NULL;
}

int set_params(dt_lib_module_t *self,
               const void *params,
               const int size)
{
  dt_lib_print_settings_t *ps = self->data;

  if(!params) return 1;

  // get the parameters buffer
  const char *buf = (char *)params;

  // get individual items
  const char *printer = buf;
  if(!printer) return 1;
  const int32_t printer_len = strlen(printer) + 1;
  buf += printer_len;

  const char *paper = buf;
  if(!paper) return 1;
  const int32_t paper_len = strlen(paper) + 1;
  buf += paper_len;

  const int32_t landscape = *(int32_t *)buf;
  buf +=  sizeof(int32_t);

  const int32_t f_profile_type = *(int32_t *)buf;
  buf +=  sizeof(int32_t);

  const char *f_profile = buf;
  if(!f_profile) return 1;
  const int32_t profile_len = strlen(f_profile) + 1;
  buf += profile_len;

  const int32_t intent = *(int32_t *)buf;
  buf += sizeof(int32_t);

  const int32_t f_pprofile_type = *(int32_t *)buf;
  buf +=  sizeof(int32_t);

  const char *f_pprofile = buf;
  if(!f_pprofile) return 1;
  const int32_t pprofile_len = strlen(f_pprofile) + 1;
  buf += pprofile_len;

  const int32_t pintent = *(int32_t *)buf;
  buf += sizeof(int32_t);

  const int32_t bpc = *(int32_t *)buf;
  buf += sizeof(int32_t);

  const char *style = buf;
  if(!style) return 1;
  const int32_t style_len = strlen(style) + 1;
  buf += style_len;

  const int32_t style_mode = *(int32_t *)buf;
  buf += sizeof(int32_t);

  const double b_top = *(double *)buf;
  buf += sizeof(double);

  const double b_bottom = *(double *)buf;
  buf += sizeof(double);

  const double b_left = *(double *)buf;
  buf += sizeof(double);

  const double b_right = *(double *)buf;
  buf += sizeof(double);

  const int32_t alignment = *(int32_t *)buf;
  buf += sizeof(int32_t);

  const char *media = buf;
  if(!media) return 1;
  const int32_t media_len = strlen(media) + 1;
  buf += media_len;

  ps->imgs.count = *(int32_t *)buf;
  buf += sizeof(int32_t);

  for(int k=0; k<ps->imgs.count; k++)
  {
    ps->imgs.box[k].pos.x = *(float *)buf;
    buf += sizeof(float);
    ps->imgs.box[k].pos.y = *(float *)buf;
    buf += sizeof(float);
    ps->imgs.box[k].pos.width = *(float *)buf;
    buf += sizeof(float);
    ps->imgs.box[k].pos.height = *(float *)buf;
    buf += sizeof(float);
  }

  // ensure that the size is correct
  if(size != printer_len + paper_len + media_len + profile_len
     + pprofile_len + style_len
     + 8 * sizeof(int32_t)
     + 4 * sizeof(double)
     + sizeof(int32_t)
     + (ps->imgs.count * 4 * sizeof(float)))
    return 1;

  // set the GUI with corresponding values
  if(printer[0] != '\0')
    dt_bauhaus_combobox_set_from_text(ps->printers, printer);

  if(paper[0] != '\0')
    dt_bauhaus_combobox_set_from_text(ps->papers, paper);

  if(media[0] != '\0')
    dt_bauhaus_combobox_set_from_text(ps->media, media);

  dt_bauhaus_combobox_set (ps->orientation, landscape);

  dt_bauhaus_combobox_set(ps->profile, 0);
  for(GList *iter = ps->profiles; iter; iter = g_list_next(iter))
  {
    dt_lib_export_profile_t *p = iter->data;
    if(f_profile_type == p->type && (f_profile_type != DT_COLORSPACE_FILE
                                     || !g_strcmp0(f_profile, p->filename)))
    {
      dt_bauhaus_combobox_set(ps->profile, p->pos);
      break;
    }
  }

  dt_bauhaus_combobox_set (ps->intent, intent);

  dt_bauhaus_combobox_set(ps->pprofile, 0);
  for(GList *iter = ps->profiles; iter; iter = g_list_next(iter))
  {
    dt_lib_export_profile_t *p = iter->data;
    if(f_pprofile_type == p->type && (f_pprofile_type != DT_COLORSPACE_FILE
                                      || !g_strcmp0(f_pprofile, p->filename)))
    {
      dt_bauhaus_combobox_set(ps->pprofile, p->ppos);
      break;
    }
  }

  dt_bauhaus_combobox_set (ps->pintent, pintent);
  ps->prt.printer.intent = pintent;

  _update_style_label(ps,style);
  dt_bauhaus_combobox_set (ps->style_mode, style_mode);

  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_top), b_top * units[ps->unit]);
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_bottom), b_bottom * units[ps->unit]);
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_left), b_left * units[ps->unit]);
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_right), b_right * units[ps->unit]);

  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ps->dtba[alignment]), TRUE);
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ps->black_point_compensation), bpc);

  dt_control_queue_redraw_center();

  return 0;
}

void *get_params(dt_lib_module_t *self, int *size)
{
  const dt_lib_print_settings_t *ps = self->data;

  // get the data
  const char *printer = dt_bauhaus_combobox_get_text(ps->printers);
  const char *paper = dt_bauhaus_combobox_get_text(ps->papers);
  const char *media = dt_bauhaus_combobox_get_text(ps->media);
  const int32_t profile_pos = dt_bauhaus_combobox_get(ps->profile);
  const int32_t intent =  dt_bauhaus_combobox_get(ps->intent);
  const char *style = gtk_label_get_text(GTK_LABEL(ps->style));
  const int32_t style_mode = dt_bauhaus_combobox_get(ps->style_mode);
  const int32_t pprofile_pos = dt_bauhaus_combobox_get(ps->pprofile);
  const int32_t pintent =  dt_bauhaus_combobox_get(ps->pintent);
  const int32_t landscape = dt_bauhaus_combobox_get(ps->orientation);
  const int32_t bpc = gtk_toggle_button_get_active
    (GTK_TOGGLE_BUTTON(ps->black_point_compensation));
  const double b_top = ps->prt.page.margin_top;
  const double b_bottom = ps->prt.page.margin_bottom;
  const double b_left = ps->prt.page.margin_left;
  const double b_right = ps->prt.page.margin_right;
  const int32_t alignment = 0;

  dt_colorspaces_color_profile_type_t profile_type =
    DT_COLORSPACE_NONE, pprofile_type = DT_COLORSPACE_NONE;
  const char *profile = "", *pprofile = "";
  for(GList *iter = ps->profiles; iter; iter = g_list_next(iter))
  {
    dt_lib_export_profile_t *p = iter->data;
    if(p->pos == profile_pos)
    {
      profile_type = p->type;
      profile = p->filename;
    }
    if(p->ppos == pprofile_pos)
    {
      pprofile_type = p->type;
      pprofile = p->filename;
    }
  }

  // these will be NULL when no printer is connected/found
  if(!printer) printer = "";
  if(!paper) paper = "";
  if(!media) media = "";

  // compute the size of individual items, always get the \0 for strings
  const int32_t printer_len = strlen (printer) + 1;
  const int32_t paper_len = strlen (paper) + 1;
  const int32_t media_len = strlen (media) + 1;
  const int32_t profile_len = strlen (profile) + 1;
  const int32_t pprofile_len = strlen (pprofile) + 1;
  const int32_t style_len = strlen (style) + 1;

  // compute the size of all parameters
  *size = printer_len + paper_len + media_len
    + profile_len + pprofile_len + style_len
    + 8 * sizeof(int32_t)
    + 4 * sizeof(double)
    + sizeof(int32_t)
    + (ps->imgs.count * 4 * sizeof(float));

  // allocate the parameter buffer
  char *params = (char *)malloc(*size);

  int pos = 0;

  memcpy(params+pos, printer, printer_len);
  pos += printer_len;
  memcpy(params+pos, paper, paper_len);
  pos += paper_len;
  memcpy(params+pos, &landscape, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params+pos, &profile_type, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params+pos, profile, profile_len);
  pos += profile_len;
  memcpy(params+pos, &intent, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params+pos, &pprofile_type, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params+pos, pprofile, pprofile_len);
  pos += pprofile_len;
  memcpy(params+pos, &pintent, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params+pos, &bpc, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params+pos, style, style_len);
  pos += style_len;
  memcpy(params+pos, &style_mode, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params+pos, &b_top, sizeof(double));
  pos += sizeof(double);
  memcpy(params+pos, &b_bottom, sizeof(double));
  pos += sizeof(double);
  memcpy(params+pos, &b_left, sizeof(double));
  pos += sizeof(double);
  memcpy(params+pos, &b_right, sizeof(double));
  pos += sizeof(double);
  memcpy(params+pos, &alignment, sizeof(int32_t));
  pos += sizeof(int32_t);
  memcpy(params+pos, media, media_len);
  pos += media_len;

  // boxes
  memcpy(params+pos, &ps->imgs.count, sizeof(int32_t));
  pos += sizeof(int32_t);

  for(int k=0; k<ps->imgs.count; k++)
  {
    memcpy(params+pos, &ps->imgs.box[k].pos.x, sizeof(float));
    pos += sizeof(int32_t);
    memcpy(params+pos, &ps->imgs.box[k].pos.y, sizeof(float));
    pos += sizeof(int32_t);
    memcpy(params+pos, &ps->imgs.box[k].pos.width, sizeof(float));
    pos += sizeof(int32_t);
    memcpy(params+pos, &ps->imgs.box[k].pos.height, sizeof(float));
    pos += sizeof(int32_t);
  }

  g_assert(pos == *size);

  return params;
}

void gui_cleanup(dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;

  // these can be called on shutdown, resulting in null-pointer
  // dereference and division by zero -- not sure what interaction
  // makes them called, but better to disconnect and not have segfault
  g_signal_handlers_disconnect_by_func(G_OBJECT(ps->b_top),
                                       G_CALLBACK(_top_border_callback), self);
  g_signal_handlers_disconnect_by_func(G_OBJECT(ps->b_bottom),
                                       G_CALLBACK(_bottom_border_callback), self);
  g_signal_handlers_disconnect_by_func(G_OBJECT(ps->b_left),
                                       G_CALLBACK(_left_border_callback), self);
  g_signal_handlers_disconnect_by_func(G_OBJECT(ps->b_right),
                                       G_CALLBACK(_right_border_callback), self);

  g_list_free_full(ps->profiles, g_free);
  g_list_free_full(ps->paper_list, free);
  g_list_free_full(ps->media_list, free);

  g_free(ps->v_iccprofile);
  g_free(ps->v_piccprofile);
  g_free(ps->v_style);

#ifdef _WIN32
  g_free(ps->waiting_for_printer);

  if(ps->settings_ctx) 
  {
    dt_win32_print_ctx_free(ps->settings_ctx);
    ps->settings_ctx = NULL;   // important!
  }
#endif

  free(self->data);
  self->data = NULL;
}

void gui_reset(dt_lib_module_t *self)
{
  dt_lib_print_settings_t *ps = self->data;

  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_top), 17 * units[ps->unit]);
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_bottom), 17 * units[ps->unit]);
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_left), 17 * units[ps->unit]);
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->b_right), 17 * units[ps->unit]);
  gtk_spin_button_set_value(GTK_SPIN_BUTTON(ps->grid_size), 10 * units[ps->unit]);

  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ps->dtba[ALIGNMENT_CENTER]), TRUE);
  ps->prt.printer.intent = DT_INTENT_PERCEPTUAL;
  dt_bauhaus_combobox_set(ps->profile, 0);
  dt_bauhaus_combobox_set(ps->pprofile, 0);
  dt_bauhaus_combobox_set(ps->pintent, 0);
  _update_style_label(ps,"");
  dt_bauhaus_combobox_set(ps->intent, 0);
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(ps->black_point_compensation), TRUE);
  gtk_widget_set_sensitive(GTK_WIDGET(ps->pintent), TRUE);
  gtk_widget_set_sensitive(GTK_WIDGET(ps->black_point_compensation), FALSE);
  gtk_widget_set_sensitive(GTK_WIDGET(ps->style_mode), FALSE);

  // reset page orientation to fit the picture if a single one is displayed

  const dt_imgid_t imgid = (ps->imgs.count > 0) ? ps->imgs.box[0].imgid : NO_IMGID;
  dt_printing_clear_boxes(&ps->imgs);
  ps->imgs.imgid_to_load = imgid;

  ps->creation = ps->dragging = FALSE;
  ps->selected = -1;
  ps->last_selected = -1;
  ps->has_changed = FALSE;

  dt_control_queue_redraw_center();
}

// clang-format off
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
// vim: shiftwidth=2 expandtab tabstop=2 cindent
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
// clang-format on