/* This file is part of darktable, Copyright (C) 2009-2021 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 . */ /** this is the view for the darkroom module. */ #include "bauhaus/bauhaus.h" #include "common/collection.h" #include "common/colorspaces.h" #include "common/darktable.h" #include "common/debug.h" #include "common/file_location.h" #include "common/focus_peaking.h" #include "common/history.h" #include "common/image_cache.h" #include "common/imageio.h" #include "common/imageio_module.h" #include "common/selection.h" #include "common/styles.h" #include "common/tags.h" #include "common/undo.h" #include "control/conf.h" #include "control/control.h" #include "control/jobs.h" #include "develop/blend.h" #include "develop/develop.h" #include "develop/imageop.h" #include "develop/masks.h" #include "dtgtk/button.h" #include "dtgtk/thumbtable.h" #include "gui/accelerators.h" #include "gui/gtk.h" #include "gui/presets.h" #include "libs/colorpicker.h" #include "libs/modulegroups.h" #include "views/view.h" #include "views/view_api.h" #ifdef GDK_WINDOWING_QUARTZ #include "osx/osx.h" #endif #ifdef USE_LUA #include "lua/image.h" #endif #include #include #include #include #include #include #include #include #include #ifndef G_SOURCE_FUNC // Defined for glib >= 2.58 #define G_SOURCE_FUNC(f) ((GSourceFunc) (void (*)(void)) (f)) #endif DT_MODULE(1) static gboolean zoom_key_accel(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data); static gboolean skip_f_key_accel_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data); static gboolean skip_b_key_accel_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data); static gboolean _toolbox_toggle_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data); static gboolean _brush_size_up_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data); static gboolean _brush_size_down_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data); static gboolean _brush_hardness_up_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data); static gboolean _brush_hardness_down_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data); static gboolean _brush_opacity_up_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data); static gboolean _brush_opacity_down_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data); static void _update_softproof_gamut_checking(dt_develop_t *d); /* signal handler for filmstrip image switching */ static void _view_darkroom_filmstrip_activate_callback(gpointer instance, int imgid, gpointer user_data); static void dt_dev_change_image(dt_develop_t *dev, const int32_t imgid); static void _darkroom_display_second_window(dt_develop_t *dev); static void _darkroom_ui_second_window_write_config(GtkWidget *widget); const char *name(const dt_view_t *self) { return _("darkroom"); } #ifdef USE_LUA static int display_image_cb(lua_State *L) { dt_develop_t *dev = darktable.develop; dt_lua_image_t imgid = -1; if(luaL_testudata(L, 1, "dt_lua_image_t")) { luaA_to(L, dt_lua_image_t, &imgid, 1); dt_dev_change_image(dev, imgid); } else { // ensure the image infos in db is up to date dt_dev_write_history(dev); } luaA_push(L, dt_lua_image_t, &dev->image_storage.id); return 1; } #endif void init(dt_view_t *self) { self->data = malloc(sizeof(dt_develop_t)); dt_dev_init((dt_develop_t *)self->data, 1); #ifdef USE_LUA lua_State *L = darktable.lua_state.state; const int my_type = dt_lua_module_entry_get_type(L, "view", self->module_name); lua_pushlightuserdata(L, self); lua_pushcclosure(L, display_image_cb, 1); dt_lua_gtk_wrap(L); lua_pushcclosure(L, dt_lua_type_member_common, 1); dt_lua_type_register_const_type(L, my_type, "display_image"); #endif } uint32_t view(const dt_view_t *self) { return DT_VIEW_DARKROOM; } void cleanup(dt_view_t *self) { dt_develop_t *dev = (dt_develop_t *)self->data; if(dev->second_window.second_wnd) { if(gtk_widget_is_visible(dev->second_window.second_wnd)) { dt_conf_set_bool("second_window/last_visible", TRUE); _darkroom_ui_second_window_write_config(dev->second_window.second_wnd); } else dt_conf_set_bool("second_window/last_visible", FALSE); gtk_widget_destroy(dev->second_window.second_wnd); dev->second_window.second_wnd = NULL; dev->second_window.widget = NULL; } else { dt_conf_set_bool("second_window/last_visible", FALSE); } dt_dev_cleanup(dev); free(dev); } static cairo_status_t _write_snapshot_data(void *closure, const unsigned char *data, unsigned int length) { const int fd = GPOINTER_TO_INT(closure); ssize_t res = write(fd, data, length); if(res != length) return CAIRO_STATUS_WRITE_ERROR; return CAIRO_STATUS_SUCCESS; } static dt_darkroom_layout_t _lib_darkroom_get_layout(dt_view_t *self) { dt_develop_t *dev = (dt_develop_t *)self->data; if(dev->iso_12646.enabled) return DT_DARKROOM_LAYOUT_EDITING; else return DT_DARKROOM_LAYOUT_EDITING; } static cairo_filter_t _get_filtering_level(dt_develop_t *dev) { const dt_dev_zoom_t zoom = dt_control_get_dev_zoom(); const int closeup = dt_control_get_dev_closeup(); const float scale = dt_dev_get_zoom_scale(dev, zoom, 1<ppd > 1.0) return CAIRO_FILTER_FAST; else return darktable.gui->dr_filter_image; } void _display_module_trouble_message_callback(gpointer instance, dt_iop_module_t *module, const char *const trouble_msg, const char *const trouble_tooltip) { GtkWidget *label_widget = NULL; if(module && module->has_trouble && module->widget) { label_widget = dt_gui_container_first_child(GTK_CONTAINER(gtk_widget_get_parent(module->widget))); if(strcmp(gtk_widget_get_name(label_widget), "iop-plugin-warning")) label_widget = NULL; } if(trouble_msg && *trouble_msg) { if(module && module->widget) { if(label_widget) { // set the warning message in the module's message area just below the header gtk_label_set_text(GTK_LABEL(label_widget), trouble_msg); } else { label_widget = gtk_label_new(trouble_msg);; gtk_label_set_line_wrap(GTK_LABEL(label_widget), TRUE); gtk_label_set_xalign(GTK_LABEL(label_widget), 0.0); gtk_widget_set_name(label_widget, "iop-plugin-warning"); GtkWidget *iopw = gtk_widget_get_parent(module->widget); gtk_box_pack_start(GTK_BOX(iopw), label_widget, TRUE, TRUE, 0); gtk_box_reorder_child(GTK_BOX(iopw), label_widget, 0); gtk_widget_show(label_widget); } gtk_widget_set_tooltip_text(GTK_WIDGET(label_widget), trouble_tooltip); // set the module's trouble flag module->has_trouble = TRUE; dt_iop_gui_update_header(module); } } else if(module && module->has_trouble) { // no more trouble, so clear the trouble flag and remove the message area module->has_trouble = FALSE; dt_iop_gui_update_header(module); if(label_widget) gtk_widget_destroy(label_widget); } } void expose( dt_view_t *self, cairo_t *cri, int32_t width, int32_t height, int32_t pointerx, int32_t pointery) { cairo_set_source_rgb(cri, .2, .2, .2); cairo_save(cri); dt_develop_t *dev = (dt_develop_t *)self->data; const int32_t tb = dev->border_size; // account for border, make it transparent for other modules called below: pointerx -= tb; pointery -= tb; if(dev->gui_synch && !dev->image_loading) { // synch module guis from gtk thread: ++darktable.gui->reset; for(const GList *modules = dev->iop; modules; modules = g_list_next(modules)) { dt_iop_module_t *module = (dt_iop_module_t *)(modules->data); dt_iop_gui_update(module); } --darktable.gui->reset; dev->gui_synch = 0; } if(dev->image_status == DT_DEV_PIXELPIPE_DIRTY || dev->image_status == DT_DEV_PIXELPIPE_INVALID || dev->pipe->input_timestamp < dev->preview_pipe->input_timestamp) { dt_dev_process_image(dev); } if(dev->preview_status == DT_DEV_PIXELPIPE_DIRTY || dev->preview_status == DT_DEV_PIXELPIPE_INVALID || dev->pipe->input_timestamp > dev->preview_pipe->input_timestamp) { dt_dev_process_preview(dev); } if(dev->preview2_status == DT_DEV_PIXELPIPE_DIRTY || dev->preview2_status == DT_DEV_PIXELPIPE_INVALID || dev->pipe->input_timestamp > dev->preview2_pipe->input_timestamp) { dt_dev_process_preview2(dev); } dt_pthread_mutex_t *mutex = NULL; int stride; const float zoom_y = dt_control_get_dev_zoom_y(); const float zoom_x = dt_control_get_dev_zoom_x(); const dt_dev_zoom_t zoom = dt_control_get_dev_zoom(); const int closeup = dt_control_get_dev_closeup(); const float backbuf_scale = dt_dev_get_zoom_scale(dev, zoom, 1.0f, 0) * darktable.gui->ppd; static cairo_surface_t *image_surface = NULL; static int image_surface_width = 0, image_surface_height = 0, image_surface_imgid = -1; if(image_surface_width != width || image_surface_height != height || image_surface == NULL) { // create double-buffered image to draw on, to make modules draw more fluently. image_surface_width = width; image_surface_height = height; if(image_surface) cairo_surface_destroy(image_surface); image_surface = dt_cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height); image_surface_imgid = -1; // invalidate old stuff } cairo_surface_t *surface; cairo_t *cr = cairo_create(image_surface); // adjust scroll bars { float zx = zoom_x, zy = zoom_y, boxw = 1., boxh = 1.; dt_dev_check_zoom_bounds(dev, &zx, &zy, zoom, closeup, &boxw, &boxh); /* If boxw and boxh very closely match the zoomed size in the darktable window we might have resizing with every expose because adding a slider will change the image area and might force a resizing in next expose. So we disable in cases close to full. */ if(boxw > 0.95f) { zx = .0f; boxw = 1.01f; } if(boxh > 0.95f) { zy = .0f; boxh = 1.01f; } dt_view_set_scrollbar(self, zx, -0.5 + boxw/2, 0.5, boxw/2, zy, -0.5+ boxh/2, 0.5, boxh/2); } if(dev->pipe->output_backbuf && // do we have an image? dev->pipe->output_imgid == dev->image_storage.id && // is the right image? dev->pipe->backbuf_scale == backbuf_scale && // is this the zoom scale we want to display? dev->pipe->backbuf_zoom_x == zoom_x && dev->pipe->backbuf_zoom_y == zoom_y) { // draw image mutex = &dev->pipe->backbuf_mutex; dt_pthread_mutex_lock(mutex); float wd = dev->pipe->output_backbuf_width; float ht = dev->pipe->output_backbuf_height; stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, wd); surface = dt_cairo_image_surface_create_for_data(dev->pipe->output_backbuf, CAIRO_FORMAT_RGB24, wd, ht, stride); wd /= darktable.gui->ppd; ht /= darktable.gui->ppd; if(dev->iso_12646.enabled) { // force middle grey in background cairo_set_source_rgb(cr, 0.5, 0.5, 0.5); } else { if(dev->full_preview) dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_DARKROOM_PREVIEW_BG); else dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_DARKROOM_BG); } cairo_paint(cr); cairo_translate(cr, ceilf(.5f * (width - wd)), ceilf(.5f * (height - ht))); if(closeup) { const double scale = 1<iso_12646.enabled) { // draw the white frame around picture cairo_rectangle(cr, -tb / 3., -tb / 3.0, wd + 2. * tb / 3., ht + 2. * tb / 3.); cairo_set_source_rgb(cr, 1., 1., 1.); cairo_fill(cr); } cairo_rectangle(cr, 0, 0, wd, ht); cairo_set_source_surface(cr, surface, 0, 0); cairo_pattern_set_filter(cairo_get_source(cr), _get_filtering_level(dev)); cairo_paint(cr); if(darktable.gui->show_focus_peaking) { cairo_save(cr); cairo_scale(cr, 1./ darktable.gui->ppd, 1. / darktable.gui->ppd); dt_focuspeaking(cr, wd, ht, cairo_image_surface_get_data(surface), cairo_image_surface_get_width(surface), cairo_image_surface_get_height(surface)); cairo_restore(cr); } cairo_surface_destroy(surface); dt_pthread_mutex_unlock(mutex); image_surface_imgid = dev->image_storage.id; } else if(dev->preview_pipe->output_backbuf && dev->preview_pipe->output_imgid == dev->image_storage.id) { // draw preview mutex = &dev->preview_pipe->backbuf_mutex; dt_pthread_mutex_lock(mutex); const float wd = dev->preview_pipe->output_backbuf_width; const float ht = dev->preview_pipe->output_backbuf_height; const float zoom_scale = dt_dev_get_zoom_scale(dev, zoom, 1<iso_12646.enabled) { // force middle grey in background cairo_set_source_rgb(cr, 0.5, 0.5, 0.5); } else { dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_DARKROOM_BG); } cairo_paint(cr); if(dev->iso_12646.enabled) { // draw the white frame around picture cairo_rectangle(cr, 2 * tb / 3., 2 * tb / 3.0, width - 4. * tb / 3., height - 4. * tb / 3.); cairo_set_source_rgb(cr, 1., 1., 1.); cairo_fill(cr); } cairo_rectangle(cr, tb, tb, width-2*tb, height-2*tb); cairo_clip(cr); stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, wd); surface = cairo_image_surface_create_for_data(dev->preview_pipe->output_backbuf, CAIRO_FORMAT_RGB24, wd, ht, stride); cairo_translate(cr, width / 2.0, height / 2.0f); cairo_scale(cr, zoom_scale, zoom_scale); cairo_translate(cr, -.5f * wd - zoom_x * wd, -.5f * ht - zoom_y * ht); cairo_rectangle(cr, 0, 0, wd, ht); cairo_set_source_surface(cr, surface, 0, 0); cairo_pattern_set_filter(cairo_get_source(cr), _get_filtering_level(dev)); cairo_fill(cr); cairo_surface_destroy(surface); dt_pthread_mutex_unlock(mutex); image_surface_imgid = dev->image_storage.id; } else if(dev->preview_pipe->output_imgid != dev->image_storage.id) { gchar *load_txt; float fontsize; if(dev->image_invalid_cnt) { fontsize = DT_PIXEL_APPLY_DPI(16); load_txt = dt_util_dstrcat( NULL, _("darktable could not load `%s', switching to lighttable now.\n\n" "please check that the camera model that produced the image is supported in darktable\n" "(list of supported cameras is at https://www.darktable.org/resources/camera-support/).\n" "if you are sure that the camera model is supported, please consider opening an issue\n" "at https://github.com/darktable-org/darktable"), dev->image_storage.filename); if(dev->image_invalid_cnt > 400) { dev->image_invalid_cnt = 0; dt_view_manager_switch(darktable.view_manager, "lighttable"); } } else { fontsize = DT_PIXEL_APPLY_DPI(14); if(dt_conf_get_bool("darkroom/ui/loading_screen")) load_txt = dt_util_dstrcat(NULL, C_("darkroom", "loading `%s' ..."), dev->image_storage.filename); else load_txt = g_strdup(dev->image_storage.filename); } if(dt_conf_get_bool("darkroom/ui/loading_screen")) { dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_DARKROOM_BG); cairo_paint(cr); // waiting message PangoRectangle ink; PangoLayout *layout; PangoFontDescription *desc = pango_font_description_copy_static(darktable.bauhaus->pango_font_desc); pango_font_description_set_absolute_size(desc, fontsize * PANGO_SCALE); pango_font_description_set_weight(desc, PANGO_WEIGHT_BOLD); layout = pango_cairo_create_layout(cr); pango_layout_set_font_description(layout, desc); pango_layout_set_text(layout, load_txt, -1); pango_layout_get_pixel_extents(layout, &ink, NULL); const float xc = width / 2.0, yc = height * 0.85 - DT_PIXEL_APPLY_DPI(10), wd = ink.width * .5f; cairo_move_to(cr, xc - wd, yc + 1. / 3. * fontsize - fontsize); pango_cairo_layout_path(cr, layout); cairo_set_line_width(cr, 2.0); dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_LOG_BG); cairo_stroke_preserve(cr); dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_LOG_FG); cairo_fill(cr); pango_font_description_free(desc); g_object_unref(layout); image_surface_imgid = dev->image_storage.id; } else { dt_toast_log("%s", load_txt); } g_free(load_txt); } cairo_restore(cri); if(image_surface_imgid == dev->image_storage.id) { cairo_destroy(cr); cairo_set_source_surface(cri, image_surface, 0, 0); cairo_paint(cri); } /* if we are in full preview mode, we don"t want anything else than the image */ if(dev->full_preview) return; /* check if we should create a snapshot of view */ if(darktable.develop->proxy.snapshot.request && !darktable.develop->image_loading) { /* reset the request */ darktable.develop->proxy.snapshot.request = FALSE; /* validation of snapshot filename */ g_assert(darktable.develop->proxy.snapshot.filename != NULL); /* Store current image surface to snapshot file. FIXME: add checks so that we don't make snapshots of preview pipe image surface. */ const int fd = g_open(darktable.develop->proxy.snapshot.filename, O_CREAT | O_WRONLY | O_BINARY, 0600); cairo_surface_write_to_png_stream(image_surface, _write_snapshot_data, GINT_TO_POINTER(fd)); close(fd); } // Displaying sample areas if enabled if(darktable.lib->proxy.colorpicker.live_samples && (darktable.lib->proxy.colorpicker.display_samples || darktable.lib->proxy.colorpicker.selected_sample)) { GSList *samples = darktable.lib->proxy.colorpicker.live_samples; dt_colorpicker_sample_t *sample = NULL; const gboolean only_selected_sample = darktable.lib->proxy.colorpicker.selected_sample && !darktable.lib->proxy.colorpicker.display_samples; cairo_save(cri); // The colorpicker samples bounding rectangle should only be displayed inside the visible image const int pwidth = (dev->pipe->output_backbuf_width<ppd; const int pheight = (dev->pipe->output_backbuf_height<ppd; const float hbar = (self->width - pwidth) * .5f; const float tbar = (self->height - pheight) * .5f; cairo_rectangle(cri, hbar, tbar, (double)pwidth, (double)pheight); cairo_clip(cri); const float wd = dev->preview_pipe->backbuf_width; const float ht = dev->preview_pipe->backbuf_height; const float zoom_scale = dt_dev_get_zoom_scale(dev, zoom, 1<data; // only display selected sample, skip if not the selected sample if(only_selected_sample && sample != darktable.lib->proxy.colorpicker.selected_sample) { continue; } cairo_set_line_width(cri, lw); if(sample == darktable.lib->proxy.colorpicker.selected_sample) cairo_set_source_rgb(cri, .2, 0, 0); else cairo_set_source_rgb(cri, 0, 0, .2); const float *box = sample->box; const float *point = sample->point; if(sample->size == DT_COLORPICKER_SIZE_BOX) { cairo_rectangle(cri, box[0] * wd + lw, box[1] * ht + lw, (box[2] - box[0]) * wd, (box[3] - box[1]) * ht); cairo_stroke(cri); if(sample == darktable.lib->proxy.colorpicker.selected_sample) cairo_set_source_rgb(cri, .8, 0, 0); else cairo_set_source_rgb(cri, 0, 0, .8); cairo_rectangle(cri, box[0] * wd + 2. * lw, box[1] * ht + 2. * lw, (box[2] - box[0]) * wd - 2. * lw, (box[3] - box[1]) * ht - 2. * lw); cairo_stroke(cri); } else { cairo_rectangle(cri, point[0] * wd - .01 * wd, point[1] * ht - .01 * wd, .02 * wd, .02 * wd); cairo_stroke(cri); if(sample == darktable.lib->proxy.colorpicker.selected_sample) cairo_set_source_rgb(cri, .8, 0, 0); else cairo_set_source_rgb(cri, 0, 0, .8); cairo_rectangle(cri, (point[0] - 0.01) * wd + lw, point[1] * ht - 0.01 * wd + lw, .02 * wd - 2. * lw, .02 * wd - 2. * lw); cairo_move_to(cri, point[0] * wd, point[1] * ht - .01 * wd + lw); cairo_line_to(cri, point[0] * wd, point[1] * ht + .01 * wd - lw); cairo_move_to(cri, point[0] * wd - .01 * wd + lw, point[1] * ht); cairo_line_to(cri, point[0] * wd + .01 * wd - lw, point[1] * ht); cairo_stroke(cri); } } cairo_restore(cri); } // display mask if we have a current module activated or if the masks manager module is expanded const gboolean display_masks = (dev->gui_module && dev->gui_module->enabled && dt_dev_modulegroups_get_activated(darktable.develop) != DT_MODULEGROUP_BASICS) || dt_lib_gui_get_expanded(dt_lib_get_module("masks")); // execute module callback hook. if(dev->gui_module && dev->gui_module->request_color_pick != DT_REQUEST_COLORPICK_OFF && dev->gui_module->enabled) { // The colorpicker bounding rectangle should only be displayed inside the visible image const int pwidth = (dev->pipe->output_backbuf_width<ppd; const int pheight = (dev->pipe->output_backbuf_height<ppd; const float hbar = (self->width - pwidth) * .5f; const float tbar = (self->height - pheight) * .5f; cairo_save(cri); cairo_rectangle(cri, hbar, tbar, (double)pwidth, (double)pheight); cairo_clip(cri); const float wd = dev->preview_pipe->backbuf_width; const float ht = dev->preview_pipe->backbuf_height; const float zoom_scale = dt_dev_get_zoom_scale(dev, zoom, 1<gui_module->color_picker_box; const float *point = dev->gui_module->color_picker_point; if(darktable.lib->proxy.colorpicker.size) { cairo_translate(cri, 1.0 / zoom_scale, 1.0 / zoom_scale); double x = box[0] * wd, y = box[1] * ht; double d = 1. / zoom_scale; cairo_set_source_rgb(cri, .0, .0, .0); for(int blackwhite = 2; blackwhite; blackwhite--) { double w = 5. / zoom_scale - d; cairo_rectangle(cri, x + d, y + d, (box[2] - box[0]) * wd - 2. * d, (box[3] - box[1]) * ht - 2. * d); cairo_rectangle(cri, x - w, y - w, 2. * w, 2. * w); cairo_rectangle(cri, x - w, box[3] * ht - w, 2. * w, 2. * w); cairo_rectangle(cri, box[2] * wd - w, y - w, 2. * w, 2. * w); cairo_rectangle(cri, box[2] * wd - w, box[3] * ht - w, 2. * w, 2. *w); cairo_stroke(cri); d = 0; cairo_set_source_rgb(cri, .8, .8, .8); } } else if(point[0] >= 0.0f && point[0] <= 1.0f && point[1] >= 0.0f && point[1] <= 1.0f) { const float size = (wd + ht) / 2.0; cairo_rectangle(cri, point[0] * wd - .01 * size, point[1] * ht - .01 * size, .02 * size, .02 * size); cairo_stroke(cri); cairo_set_source_rgb(cri, .8, .8, .8); cairo_rectangle(cri, point[0] * wd - .01 * size + 1.0 / zoom_scale, point[1] * ht - .01 * size + 1.0 / zoom_scale, .02 * size - 2. / zoom_scale, .02 * size - 2. / zoom_scale); cairo_move_to(cri, point[0] * wd, point[1] * ht - .01 * size + 1. / zoom_scale); cairo_line_to(cri, point[0] * wd, point[1] * ht + .01 * size - 1. / zoom_scale); cairo_move_to(cri, point[0] * wd - .01 * size + 1. / zoom_scale, point[1] * ht); cairo_line_to(cri, point[0] * wd + .01 * size - 1. / zoom_scale, point[1] * ht); cairo_stroke(cri); } cairo_restore(cri); } else { if(dev->form_visible && display_masks) dt_masks_events_post_expose(dev->gui_module, cri, width, height, pointerx, pointery); // module if(dev->gui_module && dev->gui_module->gui_post_expose && dt_dev_modulegroups_get_activated(darktable.develop) != DT_MODULEGROUP_BASICS) dev->gui_module->gui_post_expose(dev->gui_module, cri, width, height, pointerx, pointery); } // indicate if we are in gamut check or softproof mode if(darktable.color_profiles->mode != DT_PROFILE_NORMAL) { gchar *label = darktable.color_profiles->mode == DT_PROFILE_GAMUTCHECK ? _("gamut check") : _("soft proof"); cairo_set_source_rgba(cri, 0.5, 0.5, 0.5, 0.5); PangoLayout *layout; PangoRectangle ink; PangoFontDescription *desc = pango_font_description_copy_static(darktable.bauhaus->pango_font_desc); pango_font_description_set_weight(desc, PANGO_WEIGHT_BOLD); layout = pango_cairo_create_layout(cri); pango_font_description_set_absolute_size(desc, DT_PIXEL_APPLY_DPI(20) * PANGO_SCALE); pango_layout_set_font_description(layout, desc); pango_layout_set_text(layout, label, -1); pango_layout_get_pixel_extents(layout, &ink, NULL); cairo_move_to(cri, ink.height * 2, height - (ink.height * 3)); pango_cairo_layout_path(cri, layout); cairo_set_source_rgb(cri, 0.7, 0.7, 0.7); cairo_fill_preserve(cri); cairo_set_line_width(cri, 0.7); cairo_set_source_rgb(cri, 0.3, 0.3, 0.3); cairo_stroke(cri); pango_font_description_free(desc); g_object_unref(layout); } } void reset(dt_view_t *self) { dt_control_set_dev_zoom(DT_ZOOM_FIT); dt_control_set_dev_zoom_x(0); dt_control_set_dev_zoom_y(0); dt_control_set_dev_closeup(0); } int try_enter(dt_view_t *self) { int32_t imgid = dt_view_get_image_to_act_on(); if(imgid < 0) { // fail :( dt_control_log(_("no image to open !")); return 1; } // this loads the image from db if needed: const dt_image_t *img = dt_image_cache_get(darktable.image_cache, imgid, 'r'); // get image and check if it has been deleted from disk first! char imgfilename[PATH_MAX] = { 0 }; gboolean from_cache = TRUE; dt_image_full_path(img->id, imgfilename, sizeof(imgfilename), &from_cache); if(!g_file_test(imgfilename, G_FILE_TEST_IS_REGULAR)) { dt_control_log(_("image `%s' is currently unavailable"), img->filename); dt_image_cache_read_release(darktable.image_cache, img); return 1; } // and drop the lock again. dt_image_cache_read_release(darktable.image_cache, img); darktable.develop->image_storage.id = imgid; return 0; } static void dt_dev_change_image(dt_develop_t *dev, const int32_t imgid) { // stop crazy users from sleeping on key-repeat spacebar: if(dev->image_loading) return; // Pipe reset needed when changing image // FIXME: synch with dev_init() and dev_cleanup() instead of redoing it dev->proxy.chroma_adaptation = NULL; dev->proxy.wb_is_D65 = TRUE; dev->proxy.wb_coeffs[0] = 0.f; // change active image g_slist_free(darktable.view_manager->active_images); darktable.view_manager->active_images = g_slist_prepend(NULL, GINT_TO_POINTER(imgid)); DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_ACTIVE_IMAGES_CHANGE); // if the previous shown image is selected and the selection is unique // then we change the selected image to the new one if(dev->image_storage.id > 0) { sqlite3_stmt *stmt; DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), "SELECT m.imgid FROM memory.collected_images as m, main.selected_images as s " "WHERE m.imgid=s.imgid", -1, &stmt, NULL); gboolean follow = FALSE; if(sqlite3_step(stmt) == SQLITE_ROW) { if(sqlite3_column_int(stmt, 0) == dev->image_storage.id && sqlite3_step(stmt) != SQLITE_ROW) { follow = TRUE; } } sqlite3_finalize(stmt); if(follow) { dt_selection_select_single(darktable.selection, imgid); } } // disable color picker when changing image if(dev->gui_module) { dev->gui_module->request_color_pick = DT_REQUEST_COLORPICK_OFF; } // update aspect ratio if(dev->preview_pipe->backbuf && dev->preview_status == DT_DEV_PIXELPIPE_VALID) { double aspect_ratio = (double)dev->preview_pipe->backbuf_width / (double)dev->preview_pipe->backbuf_height; dt_image_set_aspect_ratio_to(dev->preview_pipe->image.id, aspect_ratio, TRUE); } else { dt_image_set_aspect_ratio(dev->image_storage.id, TRUE); } // clean the undo list dt_undo_clear(darktable.undo, DT_UNDO_DEVELOP); // prevent accels_window to refresh darktable.view_manager->accels_window.prevent_refresh = TRUE; // make sure we can destroy and re-setup the pixel pipes. // we acquire the pipe locks, which will block the processing threads // in darkroom mode before they touch the pipes (init buffers etc). // we don't block here, since we hold the gdk lock, which will // result in circular locking when background threads emit signals // which in turn try to acquire the gdk lock. // // worst case, it'll drop some change image events. sorry. if(dt_pthread_mutex_BAD_trylock(&dev->preview_pipe_mutex)) return; if(dt_pthread_mutex_BAD_trylock(&dev->pipe_mutex)) { dt_pthread_mutex_BAD_unlock(&dev->preview_pipe_mutex); return; } if(dt_pthread_mutex_BAD_trylock(&dev->preview2_pipe_mutex)) { dt_pthread_mutex_BAD_unlock(&dev->pipe_mutex); dt_pthread_mutex_BAD_unlock(&dev->preview_pipe_mutex); return; } // get current plugin in focus before defocus gchar *active_plugin = NULL; if(darktable.develop->gui_module) { active_plugin = g_strdup(darktable.develop->gui_module->op); } // store last active group dt_conf_set_int("plugins/darkroom/groups", dt_dev_modulegroups_get(dev)); dt_iop_request_focus(NULL); g_assert(dev->gui_attached); // commit image ops to db dt_dev_write_history(dev); // be sure light table will update the thumbnail if (!dt_history_hash_is_mipmap_synced(dev->image_storage.id)) { dt_mipmap_cache_remove(darktable.mipmap_cache, dev->image_storage.id); dt_image_update_final_size(dev->image_storage.id); dt_image_synch_xmp(dev->image_storage.id); dt_history_hash_set_mipmap(dev->image_storage.id); } // cleanup visible masks if(!dev->form_gui) { dev->form_gui = (dt_masks_form_gui_t *)calloc(1, sizeof(dt_masks_form_gui_t)); dt_masks_init_form_gui(dev->form_gui); } dt_masks_change_form_gui(NULL); while(dev->history) { // clear history of old image dt_dev_history_item_t *hist = (dt_dev_history_item_t *)(dev->history->data); dt_dev_free_history_item(hist); dev->history = g_list_delete_link(dev->history, dev->history); } // get new image: dt_dev_reload_image(dev, imgid); // make sure no signals propagate here: ++darktable.gui->reset; const guint nb_iop = g_list_length(dev->iop); dt_dev_pixelpipe_cleanup_nodes(dev->pipe); dt_dev_pixelpipe_cleanup_nodes(dev->preview_pipe); dt_dev_pixelpipe_cleanup_nodes(dev->preview2_pipe); for(int i = nb_iop - 1; i >= 0; i--) { dt_iop_module_t *module = (dt_iop_module_t *)(g_list_nth_data(dev->iop, i)); // the base module is the one with the lowest multi_priority int base_multi_priority = 0; for(const GList *l = dev->iop; l; l = g_list_next(l)) { dt_iop_module_t *mod = (dt_iop_module_t *)l->data; if(strcmp(module->op, mod->op) == 0) base_multi_priority = MIN(base_multi_priority, mod->multi_priority); } if(module->multi_priority == base_multi_priority) // if the module is the "base" instance, we keep it { module->iop_order = dt_ioppr_get_iop_order(dev->iop_order_list, module->op, module->multi_priority); module->multi_priority = 0; module->multi_name[0] = '\0'; dt_iop_reload_defaults(module); dt_iop_gui_update(module); } else // else we delete it and remove it from the panel { if(!dt_iop_is_hidden(module)) { dt_iop_gui_cleanup_module(module); gtk_widget_destroy(module->expander); } // we remove the module from the list dev->iop = g_list_remove_link(dev->iop, g_list_nth(dev->iop, i)); // we cleanup the module dt_accel_cleanup_closures_iop(module); free(module); } } dev->iop = g_list_sort(dev->iop, dt_sort_iop_by_order); // we also clear the saved modules while(dev->alliop) { dt_iop_cleanup_module((dt_iop_module_t *)dev->alliop->data); free(dev->alliop->data); dev->alliop = g_list_delete_link(dev->alliop, dev->alliop); } // and masks g_list_free_full(dev->forms, (void (*)(void *))dt_masks_free_form); dev->forms = NULL; g_list_free_full(dev->allforms, (void (*)(void *))dt_masks_free_form); dev->allforms = NULL; dt_dev_pixelpipe_create_nodes(dev->pipe, dev); dt_dev_pixelpipe_create_nodes(dev->preview_pipe, dev); if(dev->second_window.widget && GTK_IS_WIDGET(dev->second_window.widget)) dt_dev_pixelpipe_create_nodes(dev->preview2_pipe, dev); dt_dev_read_history(dev); // we have to init all module instances other than "base" instance char option[1024]; for(const GList *modules = g_list_last(dev->iop); modules; modules = g_list_previous(modules)) { dt_iop_module_t *module = (dt_iop_module_t *)(modules->data); if(module->multi_priority > 0) { if(!dt_iop_is_hidden(module)) { module->gui_init(module); /* add module to right panel */ dt_iop_gui_set_expander(module); dt_iop_gui_update_blending(module); } } else { // update the module header to ensure proper multi-name display if(!dt_iop_is_hidden(module)) { snprintf(option, sizeof(option), "plugins/darkroom/%s/expanded", module->op); module->expanded = dt_conf_get_bool(option); dt_iop_gui_update_expanded(module); if(module->change_image) module->change_image(module); dt_iop_gui_update_header(module); } } } dt_dev_pop_history_items(dev, dev->history_end); // set the module list order dt_dev_reorder_gui_module_list(dev); dt_dev_masks_list_change(dev); /* cleanup histograms */ g_list_foreach(dev->iop, (GFunc)dt_iop_cleanup_histogram, (gpointer)NULL); /* make signals work again, we can't restore the active_plugin while signals are blocked due to implementation of dt_iop_request_focus so we do it now A double history entry is not generated. */ --darktable.gui->reset; /* Now we can request focus again and write a safe plugins/darkroom/active */ if(active_plugin) { gboolean valid = FALSE; for(const GList *modules = dev->iop; modules; modules = g_list_next(modules)) { dt_iop_module_t *module = (dt_iop_module_t *)(modules->data); if(!strcmp(module->op, active_plugin)) { valid = TRUE; dt_conf_set_string("plugins/darkroom/active", active_plugin); dt_iop_request_focus(module); } } if(!valid) { dt_conf_set_string("plugins/darkroom/active", ""); } g_free(active_plugin); } // Signal develop initialize DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_DEVELOP_IMAGE_CHANGED); // release pixel pipe mutices dt_pthread_mutex_BAD_unlock(&dev->preview2_pipe_mutex); dt_pthread_mutex_BAD_unlock(&dev->preview_pipe_mutex); dt_pthread_mutex_BAD_unlock(&dev->pipe_mutex); // update hint message dt_collection_hint_message(darktable.collection); // update accels_window darktable.view_manager->accels_window.prevent_refresh = FALSE; if(darktable.view_manager->accels_window.window && darktable.view_manager->accels_window.sticky) dt_view_accels_refresh(darktable.view_manager); // just make sure at this stage we have only history info into the undo, all automatic // tagging should be ignored. dt_undo_clear(darktable.undo, DT_UNDO_TAGS); //connect iop accelerators dt_iop_connect_accels_all(); /* last set the group to update visibility of iop modules for new pipe */ dt_dev_modulegroups_set(dev, dt_conf_get_int("plugins/darkroom/groups")); } static void _view_darkroom_filmstrip_activate_callback(gpointer instance, int32_t imgid, gpointer user_data) { if(imgid > 0) { // switch images in darkroom mode: const dt_view_t *self = (dt_view_t *)user_data; dt_develop_t *dev = (dt_develop_t *)self->data; dt_dev_change_image(dev, imgid); // move filmstrip dt_thumbtable_set_offset_image(dt_ui_thumbtable(darktable.gui->ui), imgid, TRUE); // force redraw dt_control_queue_redraw(); } } static void dt_dev_jump_image(dt_develop_t *dev, int diff, gboolean by_key) { if(dev->image_loading) return; const int32_t imgid = dev->image_storage.id; int new_offset = 1; int new_id = -1; // we new offset and imgid after the jump sqlite3_stmt *stmt; gchar *query = dt_util_dstrcat(NULL, "SELECT rowid, imgid " "FROM memory.collected_images " "WHERE rowid=(SELECT rowid FROM memory.collected_images WHERE imgid=%d)+%d", imgid, diff); DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL); if(sqlite3_step(stmt) == SQLITE_ROW) { new_offset = sqlite3_column_int(stmt, 0); new_id = sqlite3_column_int(stmt, 1); } else if(diff > 0) { // if we are here, that means that the current is not anymore in the list // in this case, let's use the current offset image new_id = dt_ui_thumbtable(darktable.gui->ui)->offset_imgid; new_offset = dt_ui_thumbtable(darktable.gui->ui)->offset; } else { // if we are here, that means that the current is not anymore in the list // in this case, let's use the image before current offset new_offset = MAX(1, dt_ui_thumbtable(darktable.gui->ui)->offset - 1); sqlite3_stmt *stmt2; gchar *query2 = dt_util_dstrcat(NULL, "SELECT imgid FROM memory.collected_images WHERE rowid=%d", new_offset); DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query2, -1, &stmt2, NULL); if(sqlite3_step(stmt2) == SQLITE_ROW) { new_id = sqlite3_column_int(stmt2, 0); } else { new_id = dt_ui_thumbtable(darktable.gui->ui)->offset_imgid; new_offset = dt_ui_thumbtable(darktable.gui->ui)->offset; } g_free(query2); sqlite3_finalize(stmt2); } g_free(query); sqlite3_finalize(stmt); if(new_id < 0 || new_id == imgid) return; // if id seems valid, we change the image and move filmstrip dt_dev_change_image(dev, new_id); dt_thumbtable_set_offset(dt_ui_thumbtable(darktable.gui->ui), new_offset, TRUE); // if it's a change by key_press, we set mouse_over to the active image if(by_key) dt_control_set_mouse_over_id(new_id); } static gboolean zoom_key_accel(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { dt_develop_t *dev = darktable.develop; int zoom, closeup; float zoom_x, zoom_y; switch(GPOINTER_TO_INT(data)) { case 1: zoom = dt_control_get_dev_zoom(); zoom_x = dt_control_get_dev_zoom_x(); zoom_y = dt_control_get_dev_zoom_y(); closeup = dt_control_get_dev_closeup(); if(zoom == DT_ZOOM_1) closeup = (closeup > 0) ^ 1; // flip closeup/no closeup, no difference whether it was 1 or larger dt_dev_check_zoom_bounds(dev, &zoom_x, &zoom_y, DT_ZOOM_1, closeup, NULL, NULL); dt_control_set_dev_zoom(DT_ZOOM_1); dt_control_set_dev_zoom_x(zoom_x); dt_control_set_dev_zoom_y(zoom_y); dt_control_set_dev_closeup(closeup); break; case 2: zoom_x = zoom_y = 0.0f; dt_control_set_dev_zoom(DT_ZOOM_FILL); dt_dev_check_zoom_bounds(dev, &zoom_x, &zoom_y, DT_ZOOM_FILL, 0, NULL, NULL); dt_control_set_dev_zoom_x(zoom_x); dt_control_set_dev_zoom_y(zoom_y); dt_control_set_dev_closeup(0); break; case 3: dt_control_set_dev_zoom(DT_ZOOM_FIT); dt_control_set_dev_zoom_x(0); dt_control_set_dev_zoom_y(0); dt_control_set_dev_closeup(0); break; default: break; } dt_dev_invalidate(dev); dt_control_queue_redraw_center(); dt_control_navigation_redraw(); return TRUE; } static gboolean skip_f_key_accel_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { dt_dev_jump_image((dt_develop_t *)data, 1, TRUE); return TRUE; } static gboolean skip_b_key_accel_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { dt_dev_jump_image((dt_develop_t *)data, -1, TRUE); return TRUE; } static void _darkroom_ui_pipe_finish_signal_callback(gpointer instance, gpointer data) { dt_control_queue_redraw_center(); } static void _darkroom_ui_preview2_pipe_finish_signal_callback(gpointer instance, gpointer user_data) { dt_view_t *self = (dt_view_t *)user_data; dt_develop_t *dev = (dt_develop_t *)self->data; if(dev->second_window.widget) gtk_widget_queue_draw(dev->second_window.widget); } static void _darkroom_ui_favorite_presets_popupmenu(GtkWidget *w, gpointer user_data) { /* create favorites menu and popup */ dt_gui_favorite_presets_menu_show(); /* if we got any styles, lets popup menu for selection */ if(darktable.gui->presets_popup_menu) { gtk_widget_show_all(GTK_WIDGET(darktable.gui->presets_popup_menu)); #if GTK_CHECK_VERSION(3, 22, 0) gtk_menu_popup_at_pointer(darktable.gui->presets_popup_menu, NULL); #else gtk_menu_popup(darktable.gui->presets_popup_menu, NULL, NULL, NULL, NULL, 0, 0); #endif } else dt_control_log(_("no userdefined presets for favorite modules were found")); } static void _darkroom_ui_apply_style_activate_callback(gchar *name) { dt_control_log(_("applied style `%s' on current image"), name); /* write current history changes so nothing gets lost */ dt_dev_write_history(darktable.develop); dt_dev_undo_start_record(darktable.develop); /* apply style on image and reload*/ dt_styles_apply_to_image(name, FALSE, FALSE, darktable.develop->image_storage.id); dt_dev_reload_image(darktable.develop, darktable.develop->image_storage.id); DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_TAG_CHANGED); /* record current history state : after change (needed for undo) */ dt_dev_undo_end_record(darktable.develop); // rebuild the accelerators (style might have changed order) dt_iop_connect_accels_all(); } static void _darkroom_ui_apply_style_popupmenu(GtkWidget *w, gpointer user_data) { /* show styles popup menu */ GList *styles = dt_styles_get_list(""); GtkMenuShell *menu = NULL; if(styles) { menu = GTK_MENU_SHELL(gtk_menu_new()); for(const GList *st_iter = styles; st_iter; st_iter = g_list_next(st_iter)) { dt_style_t *style = (dt_style_t *)st_iter->data; char *items_string = dt_styles_get_item_list_as_string(style->name); gchar *tooltip = NULL; if(style->description && *style->description) { tooltip = g_strconcat("", g_markup_escape_text(style->description, -1), "\n", items_string, NULL); } else { tooltip = g_strdup(items_string); } gchar **split = g_strsplit(style->name, "|", 0); // if sub-menu, do not put leading group in final name gchar *mi_name = NULL; if(split[1]) { gsize mi_len = 1 + strlen(split[1]); for(int i=2; split[i]; i++) mi_len += strlen(split[i]) + strlen(" | "); mi_name = g_new0(gchar, mi_len); gchar* tmp_ptr = g_stpcpy(mi_name, split[1]); for(int i=2; split[i]; i++) { tmp_ptr = g_stpcpy(tmp_ptr, " | "); tmp_ptr = g_stpcpy(tmp_ptr, split[i]); } } else mi_name = g_strdup(split[0]); GtkWidget *mi = gtk_menu_item_new_with_label(mi_name); gtk_widget_set_tooltip_markup(mi, tooltip); g_free(mi_name); // check if we already have a sub-menu with this name GtkMenu *sm = NULL; GList *children = gtk_container_get_children(GTK_CONTAINER(menu)); for(const GList *child = children; child; child = g_list_next(child)) { GtkMenuItem *smi = (GtkMenuItem *)child->data; if(!g_strcmp0(split[0],gtk_menu_item_get_label(smi))) { sm = (GtkMenu *)gtk_menu_item_get_submenu(smi); break; } } g_list_free(children); GtkMenuItem *smi = NULL; // no sub-menu, but we need one if(!sm && split[1]) { smi = (GtkMenuItem *)gtk_menu_item_new_with_label(split[0]); sm = (GtkMenu *)gtk_menu_new(); gtk_menu_item_set_submenu(smi, GTK_WIDGET(sm)); } if(sm) gtk_menu_shell_append(GTK_MENU_SHELL(sm), mi); else gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi); if(smi) { gtk_menu_shell_append(GTK_MENU_SHELL(menu), GTK_WIDGET(smi)); gtk_widget_show(GTK_WIDGET(smi)); } g_signal_connect_swapped(G_OBJECT(mi), "activate", G_CALLBACK(_darkroom_ui_apply_style_activate_callback), (gpointer)g_strdup(style->name)); gtk_widget_show(mi); g_free(items_string); g_free(tooltip); g_strfreev(split); } g_list_free_full(styles, dt_style_free); } /* if we got any styles, lets popup menu for selection */ if(menu) { #if GTK_CHECK_VERSION(3, 22, 0) gtk_menu_popup_at_pointer(GTK_MENU(menu), NULL); #else gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, 0, 0); #endif } else dt_control_log(_("no styles have been created yet")); } static void _second_window_quickbutton_clicked(GtkWidget *w, dt_develop_t *dev) { if(dev->second_window.second_wnd && !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(w))) { _darkroom_ui_second_window_write_config(dev->second_window.second_wnd); gtk_widget_destroy(dev->second_window.second_wnd); dev->second_window.second_wnd = NULL; dev->second_window.widget = NULL; } else if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(w))) _darkroom_display_second_window(dev); } /** toolbar buttons */ static gboolean _toolbar_show_popup(gpointer user_data) { gtk_widget_show_all(GTK_WIDGET(user_data)); // cancel glib timeout if invoked by long button press return FALSE; } /* colour assessment */ static void _iso_12646_quickbutton_clicked(GtkWidget *w, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; if (!d->gui_attached) return; d->iso_12646.enabled = !d->iso_12646.enabled; d->width = d->orig_width; d->height = d->orig_height; if(d->iso_12646.enabled) { d->border_size = 0.125 * d->width; } else { // Reset border size from config d->border_size = DT_PIXEL_APPLY_DPI(dt_conf_get_int("plugins/darkroom/ui/border_size")); } dt_dev_configure(d, d->width, d->height); dt_ui_restore_panels(darktable.gui->ui); dt_dev_reprocess_center(d); } /* overlay color */ static void _overlay_color_quickbutton_clicked(GtkWidget *w, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; d->overlay_color.enabled = !d->overlay_color.enabled; dt_dev_reprocess_center(d); } static gboolean _overlay_color_quickbutton_pressed(GtkWidget *widget, GdkEvent *event, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; _toolbar_show_popup(d->overlay_color.floating_window); return TRUE; } static gboolean _overlay_color_quickbutton_released(GtkWidget *widget, GdkEvent *event, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; if(d->overlay_color.timeout > 0) g_source_remove(d->overlay_color.timeout); d->overlay_color.timeout = 0; return FALSE; } static void overlay_colors_callback(GtkWidget *combo, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; d->overlay_color.color = dt_bauhaus_combobox_get(combo); dt_conf_set_int("darkroom/ui/overlay_color", d->overlay_color.color); dt_dev_reprocess_center(d); } /* overexposed */ static void _overexposed_quickbutton_clicked(GtkWidget *w, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; d->overexposed.enabled = !d->overexposed.enabled; dt_dev_reprocess_center(d); } static gboolean _overexposed_quickbutton_pressed(GtkWidget *widget, GdkEvent *event, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; const GdkEventButton *e = (GdkEventButton *)event; if(e->button == 3) { _toolbar_show_popup(d->overexposed.floating_window); return TRUE; } else { d->overexposed.timeout = g_timeout_add_seconds(1, _toolbar_show_popup, d->overexposed.floating_window); return FALSE; } } static gboolean _overexposed_quickbutton_released(GtkWidget *widget, GdkEvent *event, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; if(d->overexposed.timeout > 0) g_source_remove(d->overexposed.timeout); d->overexposed.timeout = 0; return FALSE; } static void colorscheme_callback(GtkWidget *combo, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; d->overexposed.colorscheme = dt_bauhaus_combobox_get(combo); if(d->overexposed.enabled == FALSE) gtk_button_clicked(GTK_BUTTON(d->overexposed.button)); else dt_dev_reprocess_center(d); } static void lower_callback(GtkWidget *slider, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; d->overexposed.lower = dt_bauhaus_slider_get(slider); if(d->overexposed.enabled == FALSE) gtk_button_clicked(GTK_BUTTON(d->overexposed.button)); else dt_dev_reprocess_center(d); } static void upper_callback(GtkWidget *slider, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; d->overexposed.upper = dt_bauhaus_slider_get(slider); if(d->overexposed.enabled == FALSE) gtk_button_clicked(GTK_BUTTON(d->overexposed.button)); else dt_dev_reprocess_center(d); } static void mode_callback(GtkWidget *slider, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; d->overexposed.mode = dt_bauhaus_combobox_get(slider); if(d->overexposed.enabled == FALSE) gtk_button_clicked(GTK_BUTTON(d->overexposed.button)); else dt_dev_reprocess_center(d); } /* rawoverexposed */ static void _rawoverexposed_quickbutton_clicked(GtkWidget *w, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; d->rawoverexposed.enabled = !d->rawoverexposed.enabled; dt_dev_reprocess_center(d); } static gboolean _rawoverexposed_quickbutton_pressed(GtkWidget *widget, GdkEvent *event, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; const GdkEventButton *e = (GdkEventButton *)event; if(e->button == 3) { _toolbar_show_popup(d->rawoverexposed.floating_window); return TRUE; } else { d->rawoverexposed.timeout = g_timeout_add_seconds(1, _toolbar_show_popup, d->rawoverexposed.floating_window); return FALSE; } } static gboolean _rawoverexposed_quickbutton_released(GtkWidget *widget, GdkEvent *event, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; if(d->rawoverexposed.timeout > 0) g_source_remove(d->rawoverexposed.timeout); d->rawoverexposed.timeout = 0; return FALSE; } static void rawoverexposed_mode_callback(GtkWidget *combo, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; d->rawoverexposed.mode = dt_bauhaus_combobox_get(combo); if(d->rawoverexposed.enabled == FALSE) gtk_button_clicked(GTK_BUTTON(d->rawoverexposed.button)); else dt_dev_reprocess_center(d); } static void rawoverexposed_colorscheme_callback(GtkWidget *combo, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; d->rawoverexposed.colorscheme = dt_bauhaus_combobox_get(combo); if(d->rawoverexposed.enabled == FALSE) gtk_button_clicked(GTK_BUTTON(d->rawoverexposed.button)); else dt_dev_reprocess_center(d); } static void rawoverexposed_threshold_callback(GtkWidget *slider, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; d->rawoverexposed.threshold = dt_bauhaus_slider_get(slider); if(d->rawoverexposed.enabled == FALSE) gtk_button_clicked(GTK_BUTTON(d->rawoverexposed.button)); else dt_dev_reprocess_center(d); } static gboolean _toolbox_toggle_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { gtk_button_clicked(GTK_BUTTON(data)); return TRUE; } /* softproof */ static void _softproof_quickbutton_clicked(GtkWidget *w, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; if(darktable.color_profiles->mode == DT_PROFILE_SOFTPROOF) darktable.color_profiles->mode = DT_PROFILE_NORMAL; else darktable.color_profiles->mode = DT_PROFILE_SOFTPROOF; _update_softproof_gamut_checking(d); dt_dev_reprocess_center(d); } static gboolean _softproof_quickbutton_pressed(GtkWidget *widget, GdkEvent *event, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; GdkEventButton *e = (GdkEventButton *)event; gtk_popover_set_relative_to(GTK_POPOVER(d->profile.floating_window), d->profile.softproof_button); if(e->button == 3) { _toolbar_show_popup(d->profile.floating_window); return TRUE; } else { d->profile.timeout = g_timeout_add_seconds(1, _toolbar_show_popup, d->profile.floating_window); return FALSE; } } static gboolean _second_window_quickbutton_pressed(GtkWidget *widget, GdkEvent *event, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; GdkEventButton *e = (GdkEventButton *)event; gtk_popover_set_relative_to(GTK_POPOVER(d->profile.floating_window), d->second_window.button); if(e->button == 3) { _toolbar_show_popup(d->profile.floating_window); return TRUE; } else { d->profile.timeout = g_timeout_add_seconds(1, _toolbar_show_popup, d->profile.floating_window); return FALSE; } } static gboolean _profile_quickbutton_released(GtkWidget *widget, GdkEvent *event, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; if(d->profile.timeout > 0) g_source_remove(d->profile.timeout); d->profile.timeout = 0; return FALSE; } /* gamut */ static void _gamut_quickbutton_clicked(GtkWidget *w, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; if(darktable.color_profiles->mode == DT_PROFILE_GAMUTCHECK) darktable.color_profiles->mode = DT_PROFILE_NORMAL; else darktable.color_profiles->mode = DT_PROFILE_GAMUTCHECK; _update_softproof_gamut_checking(d); dt_dev_reprocess_center(d); } static gboolean _gamut_quickbutton_pressed(GtkWidget *widget, GdkEvent *event, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; GdkEventButton *e = (GdkEventButton *)event; gtk_popover_set_relative_to(GTK_POPOVER(d->profile.floating_window), d->profile.gamut_button); if(e->button == 3) { _toolbar_show_popup(d->profile.floating_window); return TRUE; } else { d->profile.timeout = g_timeout_add_seconds(1, _toolbar_show_popup, d->profile.floating_window); return FALSE; } } /* set the gui state for both softproof and gamut checking */ static void _update_softproof_gamut_checking(dt_develop_t *d) { g_signal_handlers_block_by_func(d->profile.softproof_button, _softproof_quickbutton_clicked, d); g_signal_handlers_block_by_func(d->profile.gamut_button, _gamut_quickbutton_clicked, d); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->profile.softproof_button), darktable.color_profiles->mode == DT_PROFILE_SOFTPROOF); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->profile.gamut_button), darktable.color_profiles->mode == DT_PROFILE_GAMUTCHECK); g_signal_handlers_unblock_by_func(d->profile.softproof_button, _softproof_quickbutton_clicked, d); g_signal_handlers_unblock_by_func(d->profile.gamut_button, _gamut_quickbutton_clicked, d); } static void display_intent_callback(GtkWidget *combo, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; const int pos = dt_bauhaus_combobox_get(combo); dt_iop_color_intent_t new_intent = darktable.color_profiles->display_intent; // we are not using the int value directly so it's robust against changes on lcms' side switch(pos) { case 0: new_intent = DT_INTENT_PERCEPTUAL; break; case 1: new_intent = DT_INTENT_RELATIVE_COLORIMETRIC; break; case 2: new_intent = DT_INTENT_SATURATION; break; case 3: new_intent = DT_INTENT_ABSOLUTE_COLORIMETRIC; break; } if(new_intent != darktable.color_profiles->display_intent) { darktable.color_profiles->display_intent = new_intent; dt_dev_reprocess_all(d); } } static void display2_intent_callback(GtkWidget *combo, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; const int pos = dt_bauhaus_combobox_get(combo); dt_iop_color_intent_t new_intent = darktable.color_profiles->display2_intent; // we are not using the int value directly so it's robust against changes on lcms' side switch(pos) { case 0: new_intent = DT_INTENT_PERCEPTUAL; break; case 1: new_intent = DT_INTENT_RELATIVE_COLORIMETRIC; break; case 2: new_intent = DT_INTENT_SATURATION; break; case 3: new_intent = DT_INTENT_ABSOLUTE_COLORIMETRIC; break; } if(new_intent != darktable.color_profiles->display2_intent) { darktable.color_profiles->display2_intent = new_intent; dt_dev_reprocess_all(d); } } static void softproof_profile_callback(GtkWidget *combo, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; gboolean profile_changed = FALSE; const int pos = dt_bauhaus_combobox_get(combo); for(GList *profiles = darktable.color_profiles->profiles; profiles; profiles = g_list_next(profiles)) { dt_colorspaces_color_profile_t *pp = (dt_colorspaces_color_profile_t *)profiles->data; if(pp->out_pos == pos) { if(darktable.color_profiles->softproof_type != pp->type || (darktable.color_profiles->softproof_type == DT_COLORSPACE_FILE && strcmp(darktable.color_profiles->softproof_filename, pp->filename))) { darktable.color_profiles->softproof_type = pp->type; g_strlcpy(darktable.color_profiles->softproof_filename, pp->filename, sizeof(darktable.color_profiles->softproof_filename)); profile_changed = TRUE; } goto end; } } // profile not found, fall back to sRGB. shouldn't happen fprintf(stderr, "can't find softproof profile `%s', using sRGB instead\n", dt_bauhaus_combobox_get_text(combo)); profile_changed = darktable.color_profiles->softproof_type != DT_COLORSPACE_SRGB; darktable.color_profiles->softproof_type = DT_COLORSPACE_SRGB; darktable.color_profiles->softproof_filename[0] = '\0'; end: if(profile_changed) { DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_CONTROL_PROFILE_USER_CHANGED, DT_COLORSPACES_PROFILE_TYPE_SOFTPROOF); dt_dev_reprocess_all(d); } } static void display_profile_callback(GtkWidget *combo, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; gboolean profile_changed = FALSE; const int pos = dt_bauhaus_combobox_get(combo); for(GList *profiles = darktable.color_profiles->profiles; profiles; profiles = g_list_next(profiles)) { dt_colorspaces_color_profile_t *pp = (dt_colorspaces_color_profile_t *)profiles->data; if(pp->display_pos == pos) { if(darktable.color_profiles->display_type != pp->type || (darktable.color_profiles->display_type == DT_COLORSPACE_FILE && strcmp(darktable.color_profiles->display_filename, pp->filename))) { darktable.color_profiles->display_type = pp->type; g_strlcpy(darktable.color_profiles->display_filename, pp->filename, sizeof(darktable.color_profiles->display_filename)); profile_changed = TRUE; } goto end; } } // profile not found, fall back to system display profile. shouldn't happen fprintf(stderr, "can't find display profile `%s', using system display profile instead\n", dt_bauhaus_combobox_get_text(combo)); profile_changed = darktable.color_profiles->display_type != DT_COLORSPACE_DISPLAY; darktable.color_profiles->display_type = DT_COLORSPACE_DISPLAY; darktable.color_profiles->display_filename[0] = '\0'; end: if(profile_changed) { pthread_rwlock_rdlock(&darktable.color_profiles->xprofile_lock); dt_colorspaces_update_display_transforms(); pthread_rwlock_unlock(&darktable.color_profiles->xprofile_lock); DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_CONTROL_PROFILE_USER_CHANGED, DT_COLORSPACES_PROFILE_TYPE_DISPLAY); dt_dev_reprocess_all(d); } } static void display2_profile_callback(GtkWidget *combo, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; gboolean profile_changed = FALSE; const int pos = dt_bauhaus_combobox_get(combo); for(GList *profiles = darktable.color_profiles->profiles; profiles; profiles = g_list_next(profiles)) { dt_colorspaces_color_profile_t *pp = (dt_colorspaces_color_profile_t *)profiles->data; if(pp->display2_pos == pos) { if(darktable.color_profiles->display2_type != pp->type || (darktable.color_profiles->display2_type == DT_COLORSPACE_FILE && strcmp(darktable.color_profiles->display2_filename, pp->filename))) { darktable.color_profiles->display2_type = pp->type; g_strlcpy(darktable.color_profiles->display2_filename, pp->filename, sizeof(darktable.color_profiles->display2_filename)); profile_changed = TRUE; } goto end; } } // profile not found, fall back to system display2 profile. shouldn't happen fprintf(stderr, "can't find preview display profile `%s', using system display profile instead\n", dt_bauhaus_combobox_get_text(combo)); profile_changed = darktable.color_profiles->display2_type != DT_COLORSPACE_DISPLAY2; darktable.color_profiles->display2_type = DT_COLORSPACE_DISPLAY2; darktable.color_profiles->display2_filename[0] = '\0'; end: if(profile_changed) { pthread_rwlock_rdlock(&darktable.color_profiles->xprofile_lock); dt_colorspaces_update_display2_transforms(); pthread_rwlock_unlock(&darktable.color_profiles->xprofile_lock); DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_CONTROL_PROFILE_USER_CHANGED, DT_COLORSPACES_PROFILE_TYPE_DISPLAY2); dt_dev_reprocess_all(d); } } static void histogram_profile_callback(GtkWidget *combo, gpointer user_data) { dt_develop_t *d = (dt_develop_t *)user_data; gboolean profile_changed = FALSE; const int pos = dt_bauhaus_combobox_get(combo); for(GList *profiles = darktable.color_profiles->profiles; profiles; profiles = g_list_next(profiles)) { dt_colorspaces_color_profile_t *pp = (dt_colorspaces_color_profile_t *)profiles->data; if(pp->category_pos == pos) { if(darktable.color_profiles->histogram_type != pp->type || (darktable.color_profiles->histogram_type == DT_COLORSPACE_FILE && strcmp(darktable.color_profiles->histogram_filename, pp->filename))) { darktable.color_profiles->histogram_type = pp->type; g_strlcpy(darktable.color_profiles->histogram_filename, pp->filename, sizeof(darktable.color_profiles->histogram_filename)); profile_changed = TRUE; } goto end; } } // profile not found, fall back to export profile. shouldn't happen fprintf(stderr, "can't find histogram profile `%s', using export profile instead\n", dt_bauhaus_combobox_get_text(combo)); profile_changed = darktable.color_profiles->histogram_type != DT_COLORSPACE_WORK; darktable.color_profiles->histogram_type = DT_COLORSPACE_WORK; darktable.color_profiles->histogram_filename[0] = '\0'; end: if(profile_changed) { DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_CONTROL_PROFILE_USER_CHANGED, DT_COLORSPACES_PROFILE_TYPE_HISTOGRAM); dt_dev_reprocess_all(d); } } // FIXME: turning off lcms2 in prefs hides the widget but leaves the window sized like before -> ugly-ish static void _preference_changed(gpointer instance, gpointer user_data) { GtkWidget *display_intent = GTK_WIDGET(user_data); const int force_lcms2 = dt_conf_get_bool("plugins/lighttable/export/force_lcms2"); if(force_lcms2) { gtk_widget_set_no_show_all(display_intent, FALSE); gtk_widget_set_visible(display_intent, TRUE); } else { gtk_widget_set_no_show_all(display_intent, TRUE); gtk_widget_set_visible(display_intent, FALSE); } } static void _preference_prev_downsample_change(gpointer instance, gpointer user_data) { if(user_data != NULL) { float *ds_value = user_data; *ds_value = dt_dev_get_preview_downsampling(); } } static void _preference_changed_button_hide(gpointer instance, dt_develop_t *dev) { for(const GList *modules = dev->iop; modules; modules = g_list_next(modules)) { dt_iop_module_t *module = (dt_iop_module_t *)(modules->data); if(module->header) add_remove_mask_indicator(module, (module->blend_params->mask_mode != DEVELOP_MASK_DISABLED) && (module->blend_params->mask_mode != DEVELOP_MASK_ENABLED)); } } static void _update_display_profile_cmb(GtkWidget *cmb_display_profile) { for(const GList *l = darktable.color_profiles->profiles; l; l = g_list_next(l)) { dt_colorspaces_color_profile_t *prof = (dt_colorspaces_color_profile_t *)l->data; if(prof->display_pos > -1) { if(prof->type == darktable.color_profiles->display_type && (prof->type != DT_COLORSPACE_FILE || !strcmp(prof->filename, darktable.color_profiles->display_filename))) { if(dt_bauhaus_combobox_get(cmb_display_profile) != prof->display_pos) { dt_bauhaus_combobox_set(cmb_display_profile, prof->display_pos); break; } } } } } static void _update_display2_profile_cmb(GtkWidget *cmb_display_profile) { for(const GList *l = darktable.color_profiles->profiles; l; l = g_list_next(l)) { dt_colorspaces_color_profile_t *prof = (dt_colorspaces_color_profile_t *)l->data; if(prof->display2_pos > -1) { if(prof->type == darktable.color_profiles->display2_type && (prof->type != DT_COLORSPACE_FILE || !strcmp(prof->filename, darktable.color_profiles->display2_filename))) { if(dt_bauhaus_combobox_get(cmb_display_profile) != prof->display2_pos) { dt_bauhaus_combobox_set(cmb_display_profile, prof->display2_pos); break; } } } } } static void _display_profile_changed(gpointer instance, uint8_t profile_type, gpointer user_data) { GtkWidget *cmb_display_profile = GTK_WIDGET(user_data); _update_display_profile_cmb(cmb_display_profile); } static void _display2_profile_changed(gpointer instance, uint8_t profile_type, gpointer user_data) { GtkWidget *cmb_display_profile = GTK_WIDGET(user_data); _update_display2_profile_cmb(cmb_display_profile); } /** end of toolbox */ static gboolean _brush_size_up_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { dt_develop_t *dev = (dt_develop_t *)data; if(dev->form_visible) dt_masks_events_mouse_scrolled(dev->gui_module, 0, 0, 0, 0); return TRUE; } static gboolean _brush_size_down_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { dt_develop_t *dev = (dt_develop_t *)data; if(dev->form_visible) dt_masks_events_mouse_scrolled(dev->gui_module, 0, 0, 1, 0); return TRUE; } static gboolean _brush_hardness_up_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { dt_develop_t *dev = (dt_develop_t *)data; if(dev->form_visible) dt_masks_events_mouse_scrolled(dev->gui_module, 0, 0, 0, GDK_SHIFT_MASK); return TRUE; } static gboolean _brush_hardness_down_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { dt_develop_t *dev = (dt_develop_t *)data; if(dev->form_visible) dt_masks_events_mouse_scrolled(dev->gui_module, 0, 0, 1, GDK_SHIFT_MASK); return TRUE; } static gboolean _brush_opacity_up_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { dt_develop_t *dev = (dt_develop_t *)data; if(dev->form_visible) dt_masks_events_mouse_scrolled(dev->gui_module, 0, 0, 0, GDK_CONTROL_MASK); return TRUE; } static gboolean _brush_opacity_down_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { dt_develop_t *dev = (dt_develop_t *)data; if(dev->form_visible) dt_masks_events_mouse_scrolled(dev->gui_module, 0, 0, 1, GDK_CONTROL_MASK); return TRUE; } static gboolean _overlay_cycle_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { dt_develop_t *dev = (dt_develop_t *)data; GtkWidget * combobox = dev->overlay_color.colors; const int currentval = dt_bauhaus_combobox_get(combobox); const int nextval = currentval + 1 >= dt_bauhaus_combobox_length(combobox) ? 0 : currentval + 1; dt_bauhaus_combobox_set(combobox, nextval); dt_accel_widget_toast(combobox); return TRUE; } static gboolean _toggle_mask_visibility_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { if(darktable.gui->reset) return FALSE; dt_develop_t *dev = (dt_develop_t *)data; dt_iop_module_t *mod = dev->gui_module; //retouch and spot removal module use masks differently and have different buttons associated //keep the shortcuts independent if(mod && strcmp(mod->so->op, "spots") != 0 && strcmp(mod->so->op, "retouch") != 0) { dt_iop_gui_blend_data_t *bd = (dt_iop_gui_blend_data_t *)mod->blend_data; ++darktable.gui->reset; dt_iop_color_picker_reset(mod, TRUE); dt_masks_form_t *grp = dt_masks_get_from_id(darktable.develop, mod->blend_params->mask_id); if(grp && (grp->type & DT_MASKS_GROUP) && grp->points) { if(bd->masks_shown == DT_MASKS_EDIT_OFF) bd->masks_shown = DT_MASKS_EDIT_FULL; else bd->masks_shown = DT_MASKS_EDIT_OFF; gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(bd->masks_edit), bd->masks_shown != DT_MASKS_EDIT_OFF); dt_masks_set_edit_mode(mod, bd->masks_shown); // set all add shape buttons to inactive for(int n = 0; n < DEVELOP_MASKS_NB_SHAPES; n++) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(bd->masks_shapes[n]), FALSE); } --darktable.gui->reset; return TRUE; } else return FALSE; } void gui_init(dt_view_t *self) { dt_develop_t *dev = (dt_develop_t *)self->data; /* * Add view specific tool buttons */ /* create favorite plugin preset popup tool */ GtkWidget *favorite_presets = dtgtk_button_new(dtgtk_cairo_paint_presets, CPF_STYLE_FLAT, NULL); gtk_widget_set_tooltip_text(favorite_presets, _("quick access to presets")); g_signal_connect(G_OBJECT(favorite_presets), "clicked", G_CALLBACK(_darkroom_ui_favorite_presets_popupmenu), NULL); dt_gui_add_help_link(favorite_presets, dt_get_help_url("favorite_presets")); dt_view_manager_view_toolbox_add(darktable.view_manager, favorite_presets, DT_VIEW_DARKROOM); /* create quick styles popup menu tool */ GtkWidget *styles = dtgtk_button_new(dtgtk_cairo_paint_styles, CPF_STYLE_FLAT, NULL); g_signal_connect(G_OBJECT(styles), "clicked", G_CALLBACK(_darkroom_ui_apply_style_popupmenu), NULL); gtk_widget_set_tooltip_text(styles, _("quick access for applying any of your styles")); dt_gui_add_help_link(styles, dt_get_help_url("bottom_panel_styles")); dt_view_manager_view_toolbox_add(darktable.view_manager, styles, DT_VIEW_DARKROOM); /* create second window display button */ dev->second_window.button = dtgtk_togglebutton_new(dtgtk_cairo_paint_display2, CPF_STYLE_FLAT, NULL); g_signal_connect(G_OBJECT(dev->second_window.button), "clicked", G_CALLBACK(_second_window_quickbutton_clicked), dev); g_signal_connect(G_OBJECT(dev->second_window.button), "button-press-event", G_CALLBACK(_second_window_quickbutton_pressed), dev); g_signal_connect(G_OBJECT(dev->second_window.button), "button-release-event", G_CALLBACK(_profile_quickbutton_released), dev); gtk_widget_set_tooltip_text(dev->second_window.button, _("display a second darkroom image window")); dt_view_manager_view_toolbox_add(darktable.view_manager, dev->second_window.button, DT_VIEW_DARKROOM); const int dialog_width = 350; const int large_dialog_width = 550; // for dialog with profile names /* Enable ISO 12646-compliant colour assessment conditions */ dev->iso_12646.button = dtgtk_togglebutton_new(dtgtk_cairo_paint_bulb, CPF_STYLE_FLAT, NULL); gtk_widget_set_tooltip_text(dev->iso_12646.button, _("toggle ISO 12646 color assessment conditions")); g_signal_connect(G_OBJECT(dev->iso_12646.button), "clicked", G_CALLBACK(_iso_12646_quickbutton_clicked), dev); dt_view_manager_module_toolbox_add(darktable.view_manager, dev->iso_12646.button, DT_VIEW_DARKROOM); /* create rawoverexposed popup tool */ { // the button dev->rawoverexposed.button = dtgtk_togglebutton_new(dtgtk_cairo_paint_rawoverexposed, CPF_STYLE_FLAT, NULL); gtk_widget_set_tooltip_text(dev->rawoverexposed.button, _("toggle raw over exposed indication\nright click for options")); g_signal_connect(G_OBJECT(dev->rawoverexposed.button), "clicked", G_CALLBACK(_rawoverexposed_quickbutton_clicked), dev); g_signal_connect(G_OBJECT(dev->rawoverexposed.button), "button-press-event", G_CALLBACK(_rawoverexposed_quickbutton_pressed), dev); g_signal_connect(G_OBJECT(dev->rawoverexposed.button), "button-release-event", G_CALLBACK(_rawoverexposed_quickbutton_released), dev); dt_view_manager_module_toolbox_add(darktable.view_manager, dev->rawoverexposed.button, DT_VIEW_DARKROOM); dt_gui_add_help_link(dev->rawoverexposed.button, dt_get_help_url("rawoverexposed")); // and the popup window dev->rawoverexposed.floating_window = gtk_popover_new(dev->rawoverexposed.button); gtk_widget_set_size_request(GTK_WIDGET(dev->rawoverexposed.floating_window), dialog_width, -1); #if GTK_CHECK_VERSION(3, 16, 0) g_object_set(G_OBJECT(dev->rawoverexposed.floating_window), "transitions-enabled", FALSE, NULL); #endif GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_container_add(GTK_CONTAINER(dev->rawoverexposed.floating_window), vbox); /** let's fill the encapsulating widgets */ /* mode of operation */ GtkWidget *mode = dt_bauhaus_combobox_new(NULL); dt_bauhaus_widget_set_label(mode, NULL, N_("mode")); dt_bauhaus_combobox_add(mode, _("mark with CFA color")); dt_bauhaus_combobox_add(mode, _("mark with solid color")); dt_bauhaus_combobox_add(mode, _("false color")); dt_bauhaus_combobox_set(mode, dev->rawoverexposed.mode); gtk_widget_set_tooltip_text(mode, _("select how to mark the clipped pixels")); g_signal_connect(G_OBJECT(mode), "value-changed", G_CALLBACK(rawoverexposed_mode_callback), dev); gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(mode), TRUE, TRUE, 0); gtk_widget_set_state_flags(mode, GTK_STATE_FLAG_SELECTED, TRUE); /* color scheme */ GtkWidget *colorscheme = dt_bauhaus_combobox_new(NULL); dt_bauhaus_widget_set_label(colorscheme, NULL, N_("color scheme")); dt_bauhaus_combobox_add(colorscheme, C_("solidcolor", "red")); dt_bauhaus_combobox_add(colorscheme, C_("solidcolor", "green")); dt_bauhaus_combobox_add(colorscheme, C_("solidcolor", "blue")); dt_bauhaus_combobox_add(colorscheme, C_("solidcolor", "black")); dt_bauhaus_combobox_set(colorscheme, dev->rawoverexposed.colorscheme); gtk_widget_set_tooltip_text( colorscheme, _("select the solid color to indicate over exposure.\nwill only be used if mode = mark with solid color")); g_signal_connect(G_OBJECT(colorscheme), "value-changed", G_CALLBACK(rawoverexposed_colorscheme_callback), dev); gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(colorscheme), TRUE, TRUE, 0); gtk_widget_set_state_flags(colorscheme, GTK_STATE_FLAG_SELECTED, TRUE); /* threshold */ GtkWidget *threshold = dt_bauhaus_slider_new_with_range(NULL, 0.0, 2.0, 0.01, 1.0, 3); dt_bauhaus_slider_set(threshold, dev->rawoverexposed.threshold); dt_bauhaus_widget_set_label(threshold, NULL, N_("clipping threshold")); gtk_widget_set_tooltip_text( threshold, _("threshold of what shall be considered overexposed\n1.0 - white level\n0.0 - black level")); g_signal_connect(G_OBJECT(threshold), "value-changed", G_CALLBACK(rawoverexposed_threshold_callback), dev); gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(threshold), TRUE, TRUE, 0); } /* create overexposed popup tool */ { // the button dev->overexposed.button = dtgtk_togglebutton_new(dtgtk_cairo_paint_overexposed, CPF_STYLE_FLAT, NULL); gtk_widget_set_tooltip_text(dev->overexposed.button, _("toggle clipping indication\nright click for options")); g_signal_connect(G_OBJECT(dev->overexposed.button), "clicked", G_CALLBACK(_overexposed_quickbutton_clicked), dev); g_signal_connect(G_OBJECT(dev->overexposed.button), "button-press-event", G_CALLBACK(_overexposed_quickbutton_pressed), dev); g_signal_connect(G_OBJECT(dev->overexposed.button), "button-release-event", G_CALLBACK(_overexposed_quickbutton_released), dev); dt_view_manager_module_toolbox_add(darktable.view_manager, dev->overexposed.button, DT_VIEW_DARKROOM); dt_gui_add_help_link(dev->overexposed.button, dt_get_help_url("overexposed")); // and the popup window dev->overexposed.floating_window = gtk_popover_new(dev->overexposed.button); gtk_widget_set_size_request(GTK_WIDGET(dev->overexposed.floating_window), dialog_width, -1); #if GTK_CHECK_VERSION(3, 16, 0) g_object_set(G_OBJECT(dev->overexposed.floating_window), "transitions-enabled", FALSE, NULL); #endif GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_container_add(GTK_CONTAINER(dev->overexposed.floating_window), vbox); /** let's fill the encapsulating widgets */ /* preview mode */ GtkWidget *mode = dt_bauhaus_combobox_new(NULL); dt_bauhaus_widget_set_label(mode, NULL, N_("clipping preview mode")); dt_bauhaus_combobox_add(mode, _("full gamut")); dt_bauhaus_combobox_add(mode, _("any RGB channel")); dt_bauhaus_combobox_add(mode, _("luminance only")); dt_bauhaus_combobox_add(mode, _("saturation only")); dt_bauhaus_combobox_set(mode, dev->overexposed.mode); gtk_widget_set_tooltip_text(mode, _("select the metric you want to preview\n" "full gamut is the combination of all other modes\n")); g_signal_connect(G_OBJECT(mode), "value-changed", G_CALLBACK(mode_callback), dev); gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(mode), TRUE, TRUE, 0); gtk_widget_set_state_flags(mode, GTK_STATE_FLAG_SELECTED, TRUE); /* color scheme */ GtkWidget *colorscheme = dt_bauhaus_combobox_new(NULL); dt_bauhaus_widget_set_label(colorscheme, NULL, N_("color scheme")); dt_bauhaus_combobox_add(colorscheme, _("black & white")); dt_bauhaus_combobox_add(colorscheme, _("red & blue")); dt_bauhaus_combobox_add(colorscheme, _("purple & green")); dt_bauhaus_combobox_set(colorscheme, dev->overexposed.colorscheme); gtk_widget_set_tooltip_text(colorscheme, _("select colors to indicate clipping")); g_signal_connect(G_OBJECT(colorscheme), "value-changed", G_CALLBACK(colorscheme_callback), dev); gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(colorscheme), TRUE, TRUE, 0); gtk_widget_set_state_flags(colorscheme, GTK_STATE_FLAG_SELECTED, TRUE); /* lower */ GtkWidget *lower = dt_bauhaus_slider_new_with_range(NULL, -32., -4., 1., -12.69, 2); dt_bauhaus_slider_set(lower, dev->overexposed.lower); dt_bauhaus_slider_set_format(lower, _("%+.2f EV")); dt_bauhaus_widget_set_label(lower, NULL, N_("lower threshold")); gtk_widget_set_tooltip_text(lower, _("clipping threshold for the black point,\n" "in EV, relatively to white (0 EV).\n" "8 bits sRGB clips blacks at -12.69 EV,\n" "8 bits Adobe RGB clips blacks at -19.79 EV,\n" "16 bits sRGB clips blacks at -20.69 EV,\n" "typical fine-art mat prints produce black at -5.30 EV,\n" "typical color glossy prints produce black at -8.00 EV,\n" "typical B&W glossy prints produce black at -9.00 EV." )); g_signal_connect(G_OBJECT(lower), "value-changed", G_CALLBACK(lower_callback), dev); gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(lower), TRUE, TRUE, 0); /* upper */ GtkWidget *upper = dt_bauhaus_slider_new_with_range(NULL, 0.0, 100.0, 0.1, 99.99, 2); dt_bauhaus_slider_set(upper, dev->overexposed.upper); dt_bauhaus_slider_set_format(upper, "%.2f%%"); dt_bauhaus_widget_set_label(upper, NULL, N_("upper threshold")); /* xgettext:no-c-format */ gtk_widget_set_tooltip_text(upper, _("clipping threshold for the white point.\n" "100% is peak medium luminance.")); g_signal_connect(G_OBJECT(upper), "value-changed", G_CALLBACK(upper_callback), dev); gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(upper), TRUE, TRUE, 0); } /* create profile popup tool & buttons (softproof + gamut) */ { // the softproof button dev->profile.softproof_button = dtgtk_togglebutton_new(dtgtk_cairo_paint_softproof, CPF_STYLE_FLAT, NULL); gtk_widget_set_tooltip_text(dev->profile.softproof_button, _("toggle softproofing\nright click for profile options")); g_signal_connect(G_OBJECT(dev->profile.softproof_button), "clicked", G_CALLBACK(_softproof_quickbutton_clicked), dev); g_signal_connect(G_OBJECT(dev->profile.softproof_button), "button-press-event", G_CALLBACK(_softproof_quickbutton_pressed), dev); g_signal_connect(G_OBJECT(dev->profile.softproof_button), "button-release-event", G_CALLBACK(_profile_quickbutton_released), dev); dt_view_manager_module_toolbox_add(darktable.view_manager, dev->profile.softproof_button, DT_VIEW_DARKROOM); dt_gui_add_help_link(dev->profile.softproof_button, dt_get_help_url("softproof")); // the gamut check button dev->profile.gamut_button = dtgtk_togglebutton_new(dtgtk_cairo_paint_gamut_check, CPF_STYLE_FLAT, NULL); gtk_widget_set_tooltip_text(dev->profile.gamut_button, _("toggle gamut checking\nright click for profile options")); g_signal_connect(G_OBJECT(dev->profile.gamut_button), "clicked", G_CALLBACK(_gamut_quickbutton_clicked), dev); g_signal_connect(G_OBJECT(dev->profile.gamut_button), "button-press-event", G_CALLBACK(_gamut_quickbutton_pressed), dev); g_signal_connect(G_OBJECT(dev->profile.gamut_button), "button-release-event", G_CALLBACK(_profile_quickbutton_released), dev); dt_view_manager_module_toolbox_add(darktable.view_manager, dev->profile.gamut_button, DT_VIEW_DARKROOM); dt_gui_add_help_link(dev->profile.gamut_button, dt_get_help_url("gamut")); // and the popup window, which is shared between the two profile buttons dev->profile.floating_window = gtk_popover_new(NULL); gtk_widget_set_size_request(GTK_WIDGET(dev->profile.floating_window), large_dialog_width, -1); #if GTK_CHECK_VERSION(3, 16, 0) g_object_set(G_OBJECT(dev->profile.floating_window), "transitions-enabled", FALSE, NULL); #endif GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_container_add(GTK_CONTAINER(dev->profile.floating_window), vbox); /** let's fill the encapsulating widgets */ char datadir[PATH_MAX] = { 0 }; char confdir[PATH_MAX] = { 0 }; dt_loc_get_user_config_dir(confdir, sizeof(confdir)); dt_loc_get_datadir(datadir, sizeof(datadir)); const int force_lcms2 = dt_conf_get_bool("plugins/lighttable/export/force_lcms2"); GtkWidget *display_intent = dt_bauhaus_combobox_new(NULL); dt_bauhaus_widget_set_label(display_intent, NULL, N_("intent")); dt_bauhaus_combobox_add(display_intent, _("perceptual")); dt_bauhaus_combobox_add(display_intent, _("relative colorimetric")); dt_bauhaus_combobox_add(display_intent, C_("rendering intent", "saturation")); dt_bauhaus_combobox_add(display_intent, _("absolute colorimetric")); GtkWidget *display2_intent = dt_bauhaus_combobox_new(NULL); dt_bauhaus_widget_set_label(display2_intent, NULL, N_("intent")); dt_bauhaus_combobox_add(display2_intent, _("perceptual")); dt_bauhaus_combobox_add(display2_intent, _("relative colorimetric")); dt_bauhaus_combobox_add(display2_intent, C_("rendering intent", "saturation")); dt_bauhaus_combobox_add(display2_intent, _("absolute colorimetric")); if(!force_lcms2) { gtk_widget_set_no_show_all(display_intent, TRUE); gtk_widget_set_visible(display_intent, FALSE); gtk_widget_set_no_show_all(display2_intent, TRUE); gtk_widget_set_visible(display2_intent, FALSE); } GtkWidget *display_profile = dt_bauhaus_combobox_new(NULL); GtkWidget *display2_profile = dt_bauhaus_combobox_new(NULL); GtkWidget *softproof_profile = dt_bauhaus_combobox_new(NULL); GtkWidget *histogram_profile = dt_bauhaus_combobox_new(NULL); dt_bauhaus_widget_set_label(display_profile, NULL, N_("display profile")); dt_bauhaus_widget_set_label(display2_profile, NULL, N_("preview display profile")); dt_bauhaus_widget_set_label(softproof_profile, NULL, N_("softproof profile")); dt_bauhaus_widget_set_label(histogram_profile, NULL, N_("histogram profile")); dt_bauhaus_combobox_set_entries_ellipsis(display_profile, PANGO_ELLIPSIZE_MIDDLE); dt_bauhaus_combobox_set_entries_ellipsis(display2_profile, PANGO_ELLIPSIZE_MIDDLE); dt_bauhaus_combobox_set_entries_ellipsis(softproof_profile, PANGO_ELLIPSIZE_MIDDLE); dt_bauhaus_combobox_set_entries_ellipsis(histogram_profile, PANGO_ELLIPSIZE_MIDDLE); gtk_box_pack_start(GTK_BOX(vbox), display_profile, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(vbox), display_intent, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(vbox), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(vbox), display2_profile, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(vbox), display2_intent, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(vbox), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(vbox), softproof_profile, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(vbox), histogram_profile, TRUE, TRUE, 0); for(const GList *l = darktable.color_profiles->profiles; l; l = g_list_next(l)) { dt_colorspaces_color_profile_t *prof = (dt_colorspaces_color_profile_t *)l->data; if(prof->display_pos > -1) { dt_bauhaus_combobox_add(display_profile, prof->name); if(prof->type == darktable.color_profiles->display_type && (prof->type != DT_COLORSPACE_FILE || !strcmp(prof->filename, darktable.color_profiles->display_filename))) { dt_bauhaus_combobox_set(display_profile, prof->display_pos); } } if(prof->display2_pos > -1) { dt_bauhaus_combobox_add(display2_profile, prof->name); if(prof->type == darktable.color_profiles->display2_type && (prof->type != DT_COLORSPACE_FILE || !strcmp(prof->filename, darktable.color_profiles->display2_filename))) { dt_bauhaus_combobox_set(display2_profile, prof->display2_pos); } } // the system display profile is only suitable for display purposes if(prof->out_pos > -1) { dt_bauhaus_combobox_add(softproof_profile, prof->name); if(prof->type == darktable.color_profiles->softproof_type && (prof->type != DT_COLORSPACE_FILE || !strcmp(prof->filename, darktable.color_profiles->softproof_filename))) dt_bauhaus_combobox_set(softproof_profile, prof->out_pos); } if(prof->category_pos > -1) { dt_bauhaus_combobox_add(histogram_profile, prof->name); if(prof->type == darktable.color_profiles->histogram_type && (prof->type != DT_COLORSPACE_FILE || !strcmp(prof->filename, darktable.color_profiles->histogram_filename))) { dt_bauhaus_combobox_set(histogram_profile, prof->category_pos); } } } char *system_profile_dir = g_build_filename(datadir, "color", "out", NULL); char *user_profile_dir = g_build_filename(confdir, "color", "out", NULL); char *tooltip = g_strdup_printf(_("display ICC profiles in %s or %s"), user_profile_dir, system_profile_dir); gtk_widget_set_tooltip_text(display_profile, tooltip); g_free(tooltip); tooltip = g_strdup_printf(_("preview display ICC profiles in %s or %s"), user_profile_dir, system_profile_dir); gtk_widget_set_tooltip_text(display2_profile, tooltip); g_free(tooltip); tooltip = g_strdup_printf(_("softproof ICC profiles in %s or %s"), user_profile_dir, system_profile_dir); gtk_widget_set_tooltip_text(softproof_profile, tooltip); g_free(tooltip); tooltip = g_strdup_printf(_("histogram and color picker ICC profiles in %s or %s"), user_profile_dir, system_profile_dir); gtk_widget_set_tooltip_text(histogram_profile, tooltip); g_free(tooltip); g_free(system_profile_dir); g_free(user_profile_dir); g_signal_connect(G_OBJECT(display_intent), "value-changed", G_CALLBACK(display_intent_callback), dev); g_signal_connect(G_OBJECT(display_profile), "value-changed", G_CALLBACK(display_profile_callback), dev); g_signal_connect(G_OBJECT(display2_intent), "value-changed", G_CALLBACK(display2_intent_callback), dev); g_signal_connect(G_OBJECT(display2_profile), "value-changed", G_CALLBACK(display2_profile_callback), dev); g_signal_connect(G_OBJECT(softproof_profile), "value-changed", G_CALLBACK(softproof_profile_callback), dev); g_signal_connect(G_OBJECT(histogram_profile), "value-changed", G_CALLBACK(histogram_profile_callback), dev); _update_softproof_gamut_checking(dev); DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_PREFERENCES_CHANGE, G_CALLBACK(_preference_prev_downsample_change), &(dev->preview_downsampling)); // update the gui when the preferences changed (i.e. show intent when using lcms2) DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_PREFERENCES_CHANGE, G_CALLBACK(_preference_changed), (gpointer)display_intent); DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_PREFERENCES_CHANGE, G_CALLBACK(_preference_changed), (gpointer)display2_intent); // and when profiles change DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_CONTROL_PROFILE_USER_CHANGED, G_CALLBACK(_display_profile_changed), (gpointer)display_profile); DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_CONTROL_PROFILE_USER_CHANGED, G_CALLBACK(_display2_profile_changed), (gpointer)display2_profile); } /* create overlay color changer popup tool */ { // the button dev->overlay_color.button = dtgtk_togglebutton_new(dtgtk_cairo_paint_grid, CPF_STYLE_FLAT, NULL); gtk_widget_set_tooltip_text(dev->overlay_color.button, _("set the color of lines that overlay the image (drawn masks, crop and rotate guides etc.)")); g_signal_connect(G_OBJECT(dev->overlay_color.button), "clicked", G_CALLBACK(_overlay_color_quickbutton_clicked), dev); g_signal_connect(G_OBJECT(dev->overlay_color.button), "button-press-event", G_CALLBACK(_overlay_color_quickbutton_pressed), dev); g_signal_connect(G_OBJECT(dev->overlay_color.button), "button-release-event", G_CALLBACK(_overlay_color_quickbutton_released), dev); dt_view_manager_module_toolbox_add(darktable.view_manager, dev->overlay_color.button, DT_VIEW_DARKROOM); // and the popup window dev->overlay_color.floating_window = gtk_popover_new(dev->overlay_color.button); gtk_widget_set_size_request(GTK_WIDGET(dev->overlay_color.floating_window), dialog_width, -1); #if GTK_CHECK_VERSION(3, 16, 0) g_object_set(G_OBJECT(dev->overlay_color.floating_window), "transitions-enabled", FALSE, NULL); #endif GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_container_add(GTK_CONTAINER(dev->overlay_color.floating_window), vbox); /** let's fill the encapsulating widget */ GtkWidget *overlay_colors = dev->overlay_color.colors = dt_bauhaus_combobox_new(NULL); dt_bauhaus_widget_set_label(overlay_colors, NULL, N_("overlay color")); dt_bauhaus_combobox_add(overlay_colors, _("gray")); dt_bauhaus_combobox_add(overlay_colors, _("red")); dt_bauhaus_combobox_add(overlay_colors, _("green")); dt_bauhaus_combobox_add(overlay_colors, _("yellow")); dt_bauhaus_combobox_add(overlay_colors, _("cyan")); dt_bauhaus_combobox_add(overlay_colors, _("magenta")); dt_bauhaus_combobox_set(overlay_colors, dev->overlay_color.color); gtk_widget_set_tooltip_text(overlay_colors, _("set overlay color")); g_signal_connect(G_OBJECT(overlay_colors), "value-changed", G_CALLBACK(overlay_colors_callback), dev); gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(overlay_colors), TRUE, TRUE, 0); gtk_widget_set_state_flags(overlay_colors, GTK_STATE_FLAG_SELECTED, TRUE); } darktable.view_manager->proxy.darkroom.view = self; darktable.view_manager->proxy.darkroom.get_layout = _lib_darkroom_get_layout; dev->border_size = DT_PIXEL_APPLY_DPI(dt_conf_get_int("plugins/darkroom/ui/border_size")); } enum { DND_TARGET_IOP, }; /** drag and drop module list */ static const GtkTargetEntry _iop_target_list_internal[] = { { "iop", GTK_TARGET_SAME_WIDGET, DND_TARGET_IOP } }; static const guint _iop_n_targets_internal = G_N_ELEMENTS(_iop_target_list_internal); static dt_iop_module_t *_get_dnd_dest_module(GtkBox *container, gint x, gint y, dt_iop_module_t *module_src) { dt_iop_module_t *module_dest = NULL; GtkAllocation allocation_w = {0}; gtk_widget_get_allocation(module_src->header, &allocation_w); const int y_slop = allocation_w.height / 2; // after source in pixelpipe, which is before it in the widgets list gboolean after_src = TRUE; GtkWidget *widget_dest = NULL; GList *children = gtk_container_get_children(GTK_CONTAINER(container)); for(GList *l = children; l != NULL; l = g_list_next(l)) { GtkWidget *w = GTK_WIDGET(l->data); if(w) { if(w == module_src->expander) after_src = FALSE; if(gtk_widget_is_visible(w)) { gtk_widget_get_allocation(w, &allocation_w); // If dragging to later in the pixelpipe, we will insert after // the destination module. If dragging to earlier in the // pixelpipe, will insert before the destination module. This // results in two code paths here and in our caller, but can // handle all cases from inserting at the very start to the // very end. if((after_src && y <= allocation_w.y + y_slop) || (!after_src && y <= allocation_w.y + allocation_w.height + y_slop)) { widget_dest = w; break; } } } } g_list_free(children); if(widget_dest) { for(const GList *modules = darktable.develop->iop; modules; modules = g_list_next(modules)) { dt_iop_module_t *mod = (dt_iop_module_t *)modules->data; if(mod->expander == widget_dest) { module_dest = mod; break; } } } return module_dest; } static dt_iop_module_t *_get_dnd_source_module(GtkBox *container) { dt_iop_module_t *module_source = NULL; gpointer *source_data = g_object_get_data(G_OBJECT(container), "source_data"); if(source_data) module_source = (dt_iop_module_t *)source_data; return module_source; } // this will be used for a custom highlight, if ever implemented static void _on_drag_end(GtkWidget *widget, GdkDragContext *context, gpointer user_data) { } // FIXME: default highlight for the dnd is barely visible // it should be possible to configure it static void _on_drag_begin(GtkWidget *widget, GdkDragContext *context, gpointer user_data) { GtkBox *container = dt_ui_get_container(darktable.gui->ui, DT_UI_CONTAINER_PANEL_RIGHT_CENTER); dt_iop_module_t *module_src = _get_dnd_source_module(container); if(module_src && module_src->expander) { GdkWindow *window = gtk_widget_get_parent_window(module_src->header); if(window) { GtkAllocation allocation_w = {0}; gtk_widget_get_allocation(module_src->header, &allocation_w); // method from https://blog.gtk.org/2017/04/23/drag-and-drop-in-lists/ cairo_surface_t *surface = dt_cairo_image_surface_create(CAIRO_FORMAT_RGB24, allocation_w.width, allocation_w.height); cairo_t *cr = cairo_create(surface); // hack to render not transparent GtkStyleContext *style_context = gtk_widget_get_style_context(module_src->header); gtk_style_context_add_class(style_context, "iop_drag_icon"); gtk_widget_draw(module_src->header, cr); gtk_style_context_remove_class(style_context, "iop_drag_icon"); // FIXME: this centers the icon on the mouse -- instead translate such that the label doesn't jump when mouse down? cairo_surface_set_device_offset(surface, -allocation_w.width * darktable.gui->ppd / 2, -allocation_w.height * darktable.gui->ppd / 2); gtk_drag_set_icon_surface(context, surface); cairo_destroy(cr); cairo_surface_destroy(surface); } } } static void _on_drag_data_get(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *selection_data, guint info, guint time, gpointer user_data) { gpointer *target_data = g_object_get_data(G_OBJECT(widget), "target_data"); guint number_data = 0; if(target_data) number_data = GPOINTER_TO_UINT(target_data[DND_TARGET_IOP]); gtk_selection_data_set(selection_data, gdk_atom_intern("iop", TRUE), // type 32, // format (guchar*)&number_data, // data 1); // length } static gboolean _on_drag_drop(GtkWidget *widget, GdkDragContext *dc, gint x, gint y, guint time, gpointer user_data) { GdkAtom target_atom = GDK_NONE; target_atom = gdk_atom_intern("iop", TRUE); gtk_drag_get_data(widget, dc, target_atom, time); return TRUE; } static gboolean _on_drag_motion(GtkWidget *widget, GdkDragContext *dc, gint x, gint y, guint time, gpointer user_data) { gboolean can_moved = FALSE; GtkBox *container = dt_ui_get_container(darktable.gui->ui, DT_UI_CONTAINER_PANEL_RIGHT_CENTER); dt_iop_module_t *module_src = _get_dnd_source_module(container); if(!module_src) return FALSE; dt_iop_module_t *module_dest = _get_dnd_dest_module(container, x, y, module_src); if(module_src && module_dest && module_src != module_dest) { if(module_src->iop_order < module_dest->iop_order) can_moved = dt_ioppr_check_can_move_after_iop(darktable.develop->iop, module_src, module_dest); else can_moved = dt_ioppr_check_can_move_before_iop(darktable.develop->iop, module_src, module_dest); } for(const GList *modules = g_list_last(darktable.develop->iop); modules; modules = g_list_previous(modules)) { dt_iop_module_t *module = (dt_iop_module_t *)(modules->data); if(module->expander) { GtkStyleContext *context = gtk_widget_get_style_context(module->expander); gtk_style_context_remove_class(context, "iop_drop_after"); gtk_style_context_remove_class(context, "iop_drop_before"); } } if(can_moved) { GtkStyleContext *context = gtk_widget_get_style_context(module_dest->expander); if(module_src->iop_order < module_dest->iop_order) gtk_style_context_add_class(context, "iop_drop_after"); else gtk_style_context_add_class(context, "iop_drop_before"); gdk_drag_status(dc, GDK_ACTION_COPY, time); GtkWidget *w = g_object_get_data(G_OBJECT(widget), "highlighted"); if(w) gtk_drag_unhighlight(w); g_object_set_data(G_OBJECT(widget), "highlighted", (gpointer)module_dest->expander); gtk_drag_highlight(module_dest->expander); } else { gdk_drag_status(dc, 0, time); GtkWidget *w = g_object_get_data(G_OBJECT(widget), "highlighted"); if(w) { gtk_drag_unhighlight(w); g_object_set_data(G_OBJECT(widget), "highlighted", (gpointer)FALSE); } } return can_moved; } static void _on_drag_data_received(GtkWidget *widget, GdkDragContext *dc, gint x, gint y, GtkSelectionData *selection_data, guint info, guint time, gpointer user_data) { int moved = 0; GtkBox *container = dt_ui_get_container(darktable.gui->ui, DT_UI_CONTAINER_PANEL_RIGHT_CENTER); dt_iop_module_t *module_src = _get_dnd_source_module(container); dt_iop_module_t *module_dest = _get_dnd_dest_module(container, x, y, module_src); if(module_src && module_dest && module_src != module_dest) { if(module_src->iop_order < module_dest->iop_order) { /* printf("[_on_drag_data_received] moving %s %s(%f) after %s %s(%f)\n", module_src->op, module_src->multi_name, module_src->iop_order, module_dest->op, module_dest->multi_name, module_dest->iop_order); */ moved = dt_ioppr_move_iop_after(darktable.develop, module_src, module_dest); } else { /* printf("[_on_drag_data_received] moving %s %s(%f) before %s %s(%f)\n", module_src->op, module_src->multi_name, module_src->iop_order, module_dest->op, module_dest->multi_name, module_dest->iop_order); */ moved = dt_ioppr_move_iop_before(darktable.develop, module_src, module_dest); } } else { if(module_src == NULL) fprintf(stderr, "[_on_drag_data_received] can't find source module\n"); if(module_dest == NULL) fprintf(stderr, "[_on_drag_data_received] can't find destination module\n"); } for(const GList *modules = g_list_last(darktable.develop->iop); modules; modules = g_list_previous(modules)) { dt_iop_module_t *module = (dt_iop_module_t *)(modules->data); if(module->expander) { GtkStyleContext *context = gtk_widget_get_style_context(module->expander); gtk_style_context_remove_class(context, "iop_drop_after"); gtk_style_context_remove_class(context, "iop_drop_before"); } } gtk_drag_finish(dc, TRUE, FALSE, time); if(moved) { // we move the headers GValue gv = { 0, { { 0 } } }; g_value_init(&gv, G_TYPE_INT); gtk_container_child_get_property( GTK_CONTAINER(dt_ui_get_container(darktable.gui->ui, DT_UI_CONTAINER_PANEL_RIGHT_CENTER)), module_dest->expander, "position", &gv); gtk_box_reorder_child(dt_ui_get_container(darktable.gui->ui, DT_UI_CONTAINER_PANEL_RIGHT_CENTER), module_src->expander, g_value_get_int(&gv)); // we update the headers dt_dev_modules_update_multishow(module_src->dev); dt_dev_add_history_item(module_src->dev, module_src, TRUE); dt_ioppr_check_iop_order(module_src->dev, 0, "_on_drag_data_received end"); // we rebuild the pipe module_src->dev->pipe->changed |= DT_DEV_PIPE_REMOVE; module_src->dev->preview_pipe->changed |= DT_DEV_PIPE_REMOVE; module_src->dev->preview2_pipe->changed |= DT_DEV_PIPE_REMOVE; module_src->dev->pipe->cache_obsolete = 1; module_src->dev->preview_pipe->cache_obsolete = 1; module_src->dev->preview2_pipe->cache_obsolete = 1; // rebuild the accelerators dt_iop_connect_accels_multi(module_src->so); DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_DEVELOP_MODULE_MOVED); // invalidate buffers and force redraw of darkroom dt_dev_invalidate_all(module_src->dev); } } static void _on_drag_leave(GtkWidget *widget, GdkDragContext *dc, guint time, gpointer user_data) { for(const GList *modules = g_list_last(darktable.develop->iop); modules; modules = g_list_previous(modules)) { dt_iop_module_t *module = (dt_iop_module_t *)(modules->data); if(module->expander) { GtkStyleContext *context = gtk_widget_get_style_context(module->expander); gtk_style_context_remove_class(context, "iop_drop_after"); gtk_style_context_remove_class(context, "iop_drop_before"); } } GtkWidget *w = g_object_get_data(G_OBJECT(widget), "highlighted"); if(w) { gtk_drag_unhighlight(w); g_object_set_data(G_OBJECT(widget), "highlighted", (gpointer)FALSE); } } static void _register_modules_drag_n_drop(dt_view_t *self) { if(darktable.gui) { GtkWidget *container = GTK_WIDGET(dt_ui_get_container(darktable.gui->ui, DT_UI_CONTAINER_PANEL_RIGHT_CENTER)); gtk_drag_source_set(container, GDK_BUTTON1_MASK | GDK_SHIFT_MASK, _iop_target_list_internal, _iop_n_targets_internal, GDK_ACTION_COPY); g_object_set_data(G_OBJECT(container), "targetlist", (gpointer)_iop_target_list_internal); g_object_set_data(G_OBJECT(container), "ntarget", GUINT_TO_POINTER(_iop_n_targets_internal)); g_signal_connect(container, "drag-begin", G_CALLBACK(_on_drag_begin), NULL); g_signal_connect(container, "drag-data-get", G_CALLBACK(_on_drag_data_get), NULL); g_signal_connect(container, "drag-end", G_CALLBACK(_on_drag_end), NULL); gtk_drag_dest_set(container, 0, _iop_target_list_internal, _iop_n_targets_internal, GDK_ACTION_COPY); g_signal_connect(container, "drag-data-received", G_CALLBACK(_on_drag_data_received), NULL); g_signal_connect(container, "drag-drop", G_CALLBACK(_on_drag_drop), NULL); g_signal_connect(container, "drag-motion", G_CALLBACK(_on_drag_motion), NULL); g_signal_connect(container, "drag-leave", G_CALLBACK(_on_drag_leave), NULL); } } static void _unregister_modules_drag_n_drop(dt_view_t *self) { if(darktable.gui) { gtk_drag_source_unset(dt_ui_center(darktable.gui->ui)); GtkBox *container = dt_ui_get_container(darktable.gui->ui, DT_UI_CONTAINER_PANEL_RIGHT_CENTER); g_signal_handlers_disconnect_matched(container, G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA, 0, 0, NULL, G_CALLBACK(_on_drag_begin), NULL); g_signal_handlers_disconnect_matched(container, G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA, 0, 0, NULL, G_CALLBACK(_on_drag_data_get), NULL); g_signal_handlers_disconnect_matched(container, G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA, 0, 0, NULL, G_CALLBACK(_on_drag_end), NULL); g_signal_handlers_disconnect_matched(container, G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA, 0, 0, NULL, G_CALLBACK(_on_drag_data_received), NULL); g_signal_handlers_disconnect_matched(container, G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA, 0, 0, NULL, G_CALLBACK(_on_drag_drop), NULL); g_signal_handlers_disconnect_matched(container, G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA, 0, 0, NULL, G_CALLBACK(_on_drag_motion), NULL); g_signal_handlers_disconnect_matched(container, G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA, 0, 0, NULL, G_CALLBACK(_on_drag_leave), NULL); } } void enter(dt_view_t *self) { // prevent accels_window to refresh darktable.view_manager->accels_window.prevent_refresh = TRUE; // clean the undo list dt_undo_clear(darktable.undo, DT_UNDO_DEVELOP); /* connect to ui pipe finished signal for redraw */ DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_DEVELOP_UI_PIPE_FINISHED, G_CALLBACK(_darkroom_ui_pipe_finish_signal_callback), (gpointer)self); DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_DEVELOP_PREVIEW2_PIPE_FINISHED, G_CALLBACK(_darkroom_ui_preview2_pipe_finish_signal_callback), (gpointer)self); DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_TROUBLE_MESSAGE, G_CALLBACK(_display_module_trouble_message_callback), (gpointer)self); dt_print(DT_DEBUG_CONTROL, "[run_job+] 11 %f in darkroom mode\n", dt_get_wtime()); dt_develop_t *dev = (dt_develop_t *)self->data; if(!dev->form_gui) { dev->form_gui = (dt_masks_form_gui_t *)calloc(1, sizeof(dt_masks_form_gui_t)); dt_masks_init_form_gui(dev->form_gui); } dt_masks_change_form_gui(NULL); dev->form_gui->pipe_hash = 0; dev->form_gui->formid = 0; dev->gui_leaving = 0; dev->gui_module = NULL; // change active image dt_view_active_images_reset(FALSE); dt_view_active_images_add(dev->image_storage.id, TRUE); dt_ui_thumbtable(darktable.gui->ui)->mouse_inside = FALSE; // consider mouse outside filmstrip by default dt_control_set_dev_zoom(DT_ZOOM_FIT); dt_control_set_dev_zoom_x(0); dt_control_set_dev_zoom_y(0); dt_control_set_dev_closeup(0); // take a copy of the image struct for convenience. dt_dev_load_image(darktable.develop, dev->image_storage.id); /* * add IOP modules to plugin list */ char option[1024]; for(const GList *modules = g_list_last(dev->iop); modules; modules = g_list_previous(modules)) { dt_iop_module_t *module = (dt_iop_module_t *)(modules->data); /* initialize gui if iop have one defined */ if(!dt_iop_is_hidden(module)) { dt_iop_gui_init(module); /* add module to right panel */ dt_iop_gui_set_expander(module); if(module->multi_priority == 0) { snprintf(option, sizeof(option), "plugins/darkroom/%s/expanded", module->op); module->expanded = dt_conf_get_bool(option); dt_iop_gui_update_expanded(module); } dt_iop_reload_defaults(module); } } /* signal that darktable.develop is initialized and ready to be used */ DT_DEBUG_CONTROL_SIGNAL_RAISE(darktable.signals, DT_SIGNAL_DEVELOP_INITIALIZE); // synch gui and flag pipe as dirty // this is done here and not in dt_read_history, as it would else be triggered before module->gui_init. dt_dev_pop_history_items(dev, dev->history_end); /* ensure that filmstrip shows current image */ dt_thumbtable_set_offset_image(dt_ui_thumbtable(darktable.gui->ui), dev->image_storage.id, TRUE); // get last active plugin: gchar *active_plugin = dt_conf_get_string("plugins/darkroom/active"); if(active_plugin) { for(const GList *modules = dev->iop; modules; modules = g_list_next(modules)) { dt_iop_module_t *module = (dt_iop_module_t *)(modules->data); if(!strcmp(module->op, active_plugin)) dt_iop_request_focus(module); } g_free(active_plugin); } // update module multishow state now modules are loaded dt_dev_modules_update_multishow(dev); // image should be there now. float zoom_x, zoom_y; dt_dev_check_zoom_bounds(dev, &zoom_x, &zoom_y, DT_ZOOM_FIT, 0, NULL, NULL); dt_control_set_dev_zoom_x(zoom_x); dt_control_set_dev_zoom_y(zoom_y); /* connect signal for filmstrip image activate */ DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_VIEWMANAGER_THUMBTABLE_ACTIVATE, G_CALLBACK(_view_darkroom_filmstrip_activate_callback), self); dt_collection_hint_message(darktable.collection); dt_ui_scrollbars_show(darktable.gui->ui, dt_conf_get_bool("darkroom/ui/scrollbars")); _register_modules_drag_n_drop(self); if(dt_conf_get_bool("second_window/last_visible")) { _darkroom_display_second_window(dev); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dev->second_window.button), TRUE); } // just make sure at this stage we have only history info into the undo, all automatic // tagging should be ignored. dt_undo_clear(darktable.undo, DT_UNDO_TAGS); // update accels_window darktable.view_manager->accels_window.prevent_refresh = FALSE; //connect iop accelerators dt_iop_connect_accels_all(); // switch on groups as they were last time: dt_dev_modulegroups_set(dev, dt_conf_get_int("plugins/darkroom/groups")); // connect to preference change for module header button hiding DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_PREFERENCES_CHANGE, G_CALLBACK(_preference_changed_button_hide), dev); dt_iop_color_picker_init(); } void leave(dt_view_t *self) { dt_iop_color_picker_cleanup(); _unregister_modules_drag_n_drop(self); /* disconnect from filmstrip image activate */ DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_view_darkroom_filmstrip_activate_callback), (gpointer)self); /* disconnect from pipe finish signal */ DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_darkroom_ui_pipe_finish_signal_callback), (gpointer)self); DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_darkroom_ui_preview2_pipe_finish_signal_callback), (gpointer)self); DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_display_module_trouble_message_callback), (gpointer)self); // store groups for next time: dt_conf_set_int("plugins/darkroom/groups", dt_dev_modulegroups_get(darktable.develop)); // store last active plugin: if(darktable.develop->gui_module) dt_conf_set_string("plugins/darkroom/active", darktable.develop->gui_module->op); else dt_conf_set_string("plugins/darkroom/active", ""); dt_develop_t *dev = (dt_develop_t *)self->data; DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_preference_changed_button_hide), dev); // reset color assessment mode if(dev->iso_12646.enabled) { gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dev->iso_12646.button), FALSE); dev->iso_12646.enabled = FALSE; dev->width = dev->orig_width; dev->height = dev->orig_height; dev->border_size = DT_PIXEL_APPLY_DPI(dt_conf_get_int("plugins/darkroom/ui/border_size")); } // commit image ops to db dt_dev_write_history(dev); // update aspect ratio if(dev->preview_pipe->backbuf && dev->preview_status == DT_DEV_PIXELPIPE_VALID) { double aspect_ratio = (double)dev->preview_pipe->backbuf_width / (double)dev->preview_pipe->backbuf_height; dt_image_set_aspect_ratio_to(dev->preview_pipe->image.id, aspect_ratio, FALSE); } else { dt_image_set_aspect_ratio(dev->image_storage.id, FALSE); } // be sure light table will regenerate the thumbnail: if (!dt_history_hash_is_mipmap_synced(dev->image_storage.id)) { dt_mipmap_cache_remove(darktable.mipmap_cache, dev->image_storage.id); dt_image_update_final_size(dev->image_storage.id); // dump new xmp data dt_image_synch_xmp(dev->image_storage.id); dt_history_hash_set_mipmap(dev->image_storage.id); } // clear gui. dt_pthread_mutex_lock(&dev->preview_pipe_mutex); dt_pthread_mutex_lock(&dev->preview2_pipe_mutex); dt_pthread_mutex_lock(&dev->pipe_mutex); dev->gui_leaving = 1; dt_dev_pixelpipe_cleanup_nodes(dev->pipe); dt_dev_pixelpipe_cleanup_nodes(dev->preview2_pipe); dt_dev_pixelpipe_cleanup_nodes(dev->preview_pipe); dt_pthread_mutex_lock(&dev->history_mutex); while(dev->history) { dt_dev_history_item_t *hist = (dt_dev_history_item_t *)(dev->history->data); // printf("removing history item %d - %s, data %f %f\n", hist->module->instance, hist->module->op, *(float // *)hist->params, *((float *)hist->params+1)); dt_dev_free_history_item(hist); dev->history = g_list_delete_link(dev->history, dev->history); } while(dev->iop) { dt_iop_module_t *module = (dt_iop_module_t *)(dev->iop->data); if(!dt_iop_is_hidden(module)) dt_iop_gui_cleanup_module(module); // force refresh if module has mask visualized if (module->request_mask_display || module->suppress_mask) dt_iop_refresh_center(module); dt_accel_cleanup_closures_iop(module); module->accel_closures = NULL; dt_iop_cleanup_module(module); free(module); dev->iop = g_list_delete_link(dev->iop, dev->iop); } while(dev->alliop) { dt_iop_cleanup_module((dt_iop_module_t *)dev->alliop->data); free(dev->alliop->data); dev->alliop = g_list_delete_link(dev->alliop, dev->alliop); } dt_pthread_mutex_unlock(&dev->history_mutex); dt_pthread_mutex_unlock(&dev->pipe_mutex); dt_pthread_mutex_unlock(&dev->preview2_pipe_mutex); dt_pthread_mutex_unlock(&dev->preview_pipe_mutex); // cleanup visible masks if(dev->form_gui) { dev->gui_module = NULL; // modules have already been free() dt_masks_clear_form_gui(dev); free(dev->form_gui); dev->form_gui = NULL; dt_masks_change_form_gui(NULL); } // clear masks g_list_free_full(dev->forms, (void (*)(void *))dt_masks_free_form); dev->forms = NULL; g_list_free_full(dev->allforms, (void (*)(void *))dt_masks_free_form); dev->allforms = NULL; // take care of the overexposed window if(dev->overexposed.timeout > 0) g_source_remove(dev->overexposed.timeout); gtk_widget_hide(dev->overexposed.floating_window); gtk_widget_hide(dev->profile.floating_window); dt_ui_scrollbars_show(darktable.gui->ui, FALSE); // darkroom development could have changed a collection, so update that before being back in lighttable dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_UNDEF, g_list_prepend(NULL, GINT_TO_POINTER(darktable.develop->image_storage.id))); darktable.develop->image_storage.id = -1; dt_print(DT_DEBUG_CONTROL, "[run_job-] 11 %f in darkroom mode\n", dt_get_wtime()); } void mouse_leave(dt_view_t *self) { // if we are not hovering over a thumbnail in the filmstrip -> show metadata of opened image. dt_develop_t *dev = (dt_develop_t *)self->data; dt_control_set_mouse_over_id(dev->image_storage.id); // masks int handled = dt_masks_events_mouse_leave(dev->gui_module); if(handled) return; // module if(dev->gui_module && dev->gui_module->mouse_leave) handled = dev->gui_module->mouse_leave(dev->gui_module); // reset any changes the selected plugin might have made. dt_control_change_cursor(GDK_LEFT_PTR); } /* This helper function tests for a position to be within the displayed area of an image. To avoid "border cases" we accept values to be slightly out of area too. */ static int mouse_in_imagearea(dt_view_t *self, double x, double y) { dt_develop_t *dev = (dt_develop_t *)self->data; const int closeup = dt_control_get_dev_closeup(); const int pwidth = (dev->pipe->output_backbuf_width<ppd; const int pheight = (dev->pipe->output_backbuf_height<ppd; x -= (self->width - pwidth) / 2; y -= (self->height - pheight) / 2; if((x < -3) || (x > (pwidth + 6)) || (y < -3) || (y > (pheight + 6))) return FALSE; return TRUE; } void mouse_enter(dt_view_t *self) { dt_develop_t *dev = (dt_develop_t *)self->data; // masks dt_masks_events_mouse_enter(dev->gui_module); } void mouse_moved(dt_view_t *self, double x, double y, double pressure, int which) { dt_develop_t *dev = (dt_develop_t *)self->data; const int32_t tb = dev->border_size; const int32_t capwd = self->width - 2*tb; const int32_t capht = self->height - 2*tb; // if we are not hovering over a thumbnail in the filmstrip -> show metadata of opened image. int32_t mouse_over_id = dt_control_get_mouse_over_id(); if(mouse_over_id == -1) { mouse_over_id = dev->image_storage.id; dt_control_set_mouse_over_id(mouse_over_id); } dt_control_t *ctl = darktable.control; const int32_t width_i = self->width; const int32_t height_i = self->height; float offx = 0.0f, offy = 0.0f; if(width_i > capwd) offx = (capwd - width_i) * .5f; if(height_i > capht) offy = (capht - height_i) * .5f; int handled = 0; if(dev->gui_module && dev->gui_module->request_color_pick != DT_REQUEST_COLORPICK_OFF && ctl->button_down && ctl->button_down_which == 1) { // module requested a color box if(mouse_in_imagearea(self, x, y)) { // Make sure a minimal width/height float delta_x = 1 / (float) dev->pipe->processed_width; float delta_y = 1 / (float) dev->pipe->processed_height; float zoom_x, zoom_y; dt_dev_get_pointer_zoom_pos(dev, x + offx, y + offy, &zoom_x, &zoom_y); if(darktable.lib->proxy.colorpicker.size) { dev->gui_module->color_picker_box[0] = fmaxf(0.0, fminf(dev->gui_module->color_picker_point[0], .5f + zoom_x) - delta_x); dev->gui_module->color_picker_box[1] = fmaxf(0.0, fminf(dev->gui_module->color_picker_point[1], .5f + zoom_y) - delta_y); dev->gui_module->color_picker_box[2] = fminf(1.0, fmaxf(dev->gui_module->color_picker_point[0], .5f + zoom_x) + delta_x); dev->gui_module->color_picker_box[3] = fminf(1.0, fmaxf(dev->gui_module->color_picker_point[1], .5f + zoom_y) + delta_y); } else { dev->gui_module->color_picker_point[0] = .5f + zoom_x; dev->gui_module->color_picker_point[1] = .5f + zoom_y; dev->preview_status = DT_DEV_PIXELPIPE_DIRTY; } } dt_control_queue_redraw(); return; } x += offx; y += offy; // masks handled = dt_masks_events_mouse_moved(dev->gui_module, x, y, pressure, which); if(handled) return; // module if(dev->gui_module && dev->gui_module->mouse_moved && dt_dev_modulegroups_get_activated(darktable.develop) != DT_MODULEGROUP_BASICS) handled = dev->gui_module->mouse_moved(dev->gui_module, x, y, pressure, which); if(handled) return; if(darktable.control->button_down && darktable.control->button_down_which == 1) { // depending on dev_zoom, adjust dev_zoom_x/y. const dt_dev_zoom_t zoom = dt_control_get_dev_zoom(); const int closeup = dt_control_get_dev_closeup(); int procw, proch; dt_dev_get_processed_size(dev, &procw, &proch); const float scale = dt_dev_get_zoom_scale(dev, zoom, 1<button_x - offx) / procw; float zy = old_zoom_y - (1.0 / scale) * (y - ctl->button_y - offy) / proch; dt_dev_check_zoom_bounds(dev, &zx, &zy, zoom, closeup, NULL, NULL); dt_control_set_dev_zoom_x(zx); dt_control_set_dev_zoom_y(zy); ctl->button_x = x - offx; ctl->button_y = y - offy; dt_dev_invalidate(dev); dt_control_queue_redraw_center(); dt_control_navigation_redraw(); } } int button_released(dt_view_t *self, double x, double y, int which, uint32_t state) { dt_develop_t *dev = darktable.develop; const int32_t tb = dev->border_size; const int32_t capwd = self->width - 2*tb; const int32_t capht = self->height - 2*tb; const int32_t width_i = self->width; const int32_t height_i = self->height; if(width_i > capwd) x += (capwd - width_i) * .5f; if(height_i > capht) y += (capht - height_i) * .5f; int handled = 0; if(dev->gui_module && dev->gui_module->request_color_pick != DT_REQUEST_COLORPICK_OFF && which == 1) { dev->preview_status = DT_DEV_PIXELPIPE_DIRTY; dt_control_queue_redraw(); return 1; } // masks if(dev->form_visible) handled = dt_masks_events_button_released(dev->gui_module, x, y, which, state); if(handled) return handled; // module if(dev->gui_module && dev->gui_module->button_released && dt_dev_modulegroups_get_activated(darktable.develop) != DT_MODULEGROUP_BASICS) handled = dev->gui_module->button_released(dev->gui_module, x, y, which, state); if(handled) return handled; if(which == 1) dt_control_change_cursor(GDK_LEFT_PTR); return 1; } int button_pressed(dt_view_t *self, double x, double y, double pressure, int which, int type, uint32_t state) { dt_develop_t *dev = (dt_develop_t *)self->data; const int32_t tb = dev->border_size; const int32_t capwd = self->width - 2*tb; const int32_t capht = self->height - 2*tb; const int32_t width_i = self->width; const int32_t height_i = self->height; float offx = 0.0f, offy = 0.0f; if(width_i > capwd) offx = (capwd - width_i) * .5f; if(height_i > capht) offy = (capht - height_i) * .5f; int handled = 0; if(dev->gui_module && dev->gui_module->request_color_pick != DT_REQUEST_COLORPICK_OFF && which == 1) { float zoom_x, zoom_y; dt_dev_get_pointer_zoom_pos(dev, x + offx, y + offy, &zoom_x, &zoom_y); if(mouse_in_imagearea(self, x, y)) { // The default box will be a square with 1% of the image width const float delta_x = 0.01f; const float delta_y = delta_x * (float)dev->pipe->processed_width / (float)dev->pipe->processed_height; zoom_x += 0.5f; zoom_y += 0.5f; dev->gui_module->color_picker_point[0] = zoom_x; dev->gui_module->color_picker_point[1] = zoom_y; if(darktable.lib->proxy.colorpicker.size) { gboolean on_corner_prev_box = TRUE; float opposite_x, opposite_y; if(fabsf(zoom_x - dev->gui_module->color_picker_box[0]) < .005f) opposite_x = dev->gui_module->color_picker_box[2]; else if(fabsf(zoom_x - dev->gui_module->color_picker_box[2]) < .005f) opposite_x = dev->gui_module->color_picker_box[0]; else on_corner_prev_box = FALSE; if(fabsf(zoom_y - dev->gui_module->color_picker_box[1]) < .005f) opposite_y = dev->gui_module->color_picker_box[3]; else if(fabsf(zoom_y - dev->gui_module->color_picker_box[3]) < .005f) opposite_y = dev->gui_module->color_picker_box[1]; else on_corner_prev_box = FALSE; if(on_corner_prev_box) { dev->gui_module->color_picker_point[0] = opposite_x; dev->gui_module->color_picker_point[1] = opposite_y; } else { dev->gui_module->color_picker_box[0] = fmaxf(0.0, zoom_x - delta_x); dev->gui_module->color_picker_box[1] = fmaxf(0.0, zoom_y - delta_y); dev->gui_module->color_picker_box[2] = fminf(1.0, zoom_x + delta_x); dev->gui_module->color_picker_box[3] = fminf(1.0, zoom_y + delta_y); } } else { dev->preview_status = DT_DEV_PIXELPIPE_DIRTY; } } dt_control_queue_redraw(); return 1; } if(dev->gui_module && dev->gui_module->request_color_pick != DT_REQUEST_COLORPICK_OFF && which == 3) { // default is hardcoded this way dev->gui_module->color_picker_box[0] = dev->gui_module->color_picker_box[1] = .01f; dev->gui_module->color_picker_box[2] = dev->gui_module->color_picker_box[3] = .99f; dev->preview_status = DT_DEV_PIXELPIPE_DIRTY; dt_control_queue_redraw(); return 1; } x += offx; y += offy; // masks if(dev->form_visible) handled = dt_masks_events_button_pressed(dev->gui_module, x, y, pressure, which, type, state); if(handled) return handled; // module if(dev->gui_module && dev->gui_module->button_pressed && dt_dev_modulegroups_get_activated(darktable.develop) != DT_MODULEGROUP_BASICS) handled = dev->gui_module->button_pressed(dev->gui_module, x, y, pressure, which, type, state); if(handled) return handled; if(which == 1 && type == GDK_2BUTTON_PRESS) return 0; if(which == 1) { dt_control_change_cursor(GDK_HAND1); return 1; } if(which == 2) { // zoom to 1:1 2:1 and back int procw, proch; dt_dev_zoom_t zoom = dt_control_get_dev_zoom(); int closeup = dt_control_get_dev_closeup(); float zoom_x = dt_control_get_dev_zoom_x(); float zoom_y = dt_control_get_dev_zoom_y(); dt_dev_get_processed_size(dev, &procw, &proch); float scale = dt_dev_get_zoom_scale(dev, zoom, 1<ppd; const gboolean low_ppd = (darktable.gui->ppd == 1); const float mouse_off_x = x - 0.5f * dev->width; const float mouse_off_y = y - 0.5f * dev->height; zoom_x += mouse_off_x / (procw * scale); zoom_y += mouse_off_y / (proch * scale); const float tscale = scale * ppd; closeup = 0; if((tscale > 0.95f) && (tscale < 1.05f)) // we are at 100% and switch to 200% { zoom = DT_ZOOM_1; scale = dt_dev_get_zoom_scale(dev, DT_ZOOM_1, 1.0, 0); if(low_ppd) closeup = 1; } else if((tscale > 1.95f) && (tscale < 2.05f)) // at 200% so switch to zoomfit { zoom = DT_ZOOM_FIT; scale = dt_dev_get_zoom_scale(dev, DT_ZOOM_FIT, 1.0, 0); } else // other than 100 or 200% so zoom to 100 % { if(low_ppd) { zoom = DT_ZOOM_1; scale = dt_dev_get_zoom_scale(dev, DT_ZOOM_1, 1.0, 0); } else { zoom = DT_ZOOM_FREE; scale = 1.0f / ppd; } } dt_control_set_dev_zoom_scale(scale); dt_control_set_dev_closeup(closeup); scale = dt_dev_get_zoom_scale(dev, zoom, 1<data; const int32_t tb = dev->border_size; const int32_t capwd = self->width - 2*tb; const int32_t capht = self->height - 2*tb; const int32_t width_i = self->width; const int32_t height_i = self->height; if(width_i > capwd) x += (capwd - width_i) * .5f; if(height_i > capht) y += (capht - height_i) * .5f; int handled = 0; // dynamic accels if(self->dynamic_accel_current) { GtkWidget *widget = self->dynamic_accel_current; dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget); if(w->type == DT_BAUHAUS_SLIDER) { float value = dt_bauhaus_slider_get(widget); float step = dt_bauhaus_slider_get_step(widget); float multiplier = dt_accel_get_slider_scale_multiplier(); const float min_visible = powf(10.0f, -dt_bauhaus_slider_get_digits(widget)); if(fabsf(step*multiplier) < min_visible) multiplier = min_visible / fabsf(step); if(up) dt_bauhaus_slider_set(widget, value + step * multiplier); else dt_bauhaus_slider_set(widget, value - step * multiplier); } else { const int currentval = dt_bauhaus_combobox_get(widget); if(up) { const int nextval = currentval + 1 >= dt_bauhaus_combobox_length(widget) ? 0 : currentval + 1; dt_bauhaus_combobox_set(widget, nextval); } else { const int prevval = currentval - 1 < 0 ? dt_bauhaus_combobox_length(widget) : currentval - 1; dt_bauhaus_combobox_set(widget, prevval); } } g_signal_emit_by_name(G_OBJECT(widget), "value-changed"); dt_accel_widget_toast(widget); return; } // masks if(dev->form_visible) handled = dt_masks_events_mouse_scrolled(dev->gui_module, x, y, up, state); if(handled) return; // module if(dev->gui_module && dev->gui_module->scrolled && dt_dev_modulegroups_get_activated(darktable.develop) != DT_MODULEGROUP_BASICS) handled = dev->gui_module->scrolled(dev->gui_module, x, y, up, state); if(handled) return; // free zoom int procw, proch; dt_dev_zoom_t zoom = dt_control_get_dev_zoom(); int closeup = dt_control_get_dev_closeup(); float zoom_x = dt_control_get_dev_zoom_x(); float zoom_y = dt_control_get_dev_zoom_y(); dt_dev_get_processed_size(dev, &procw, &proch); float scale = dt_dev_get_zoom_scale(dev, zoom, 1<ppd; const float fitscale = dt_dev_get_zoom_scale(dev, DT_ZOOM_FIT, 1.0, 0); const float oldscale = scale; // offset from center now (current zoom_{x,y} points there) const float mouse_off_x = x - 0.5f * dev->width; const float mouse_off_y = y - 0.5f * dev->height; zoom_x += mouse_off_x / (procw * scale); zoom_y += mouse_off_y / (proch * scale); zoom = DT_ZOOM_FREE; closeup = 0; const gboolean constrained = !dt_modifier_is(state, GDK_CONTROL_MASK); const gboolean low_ppd = (darktable.gui->ppd == 1); const float stepup = 0.1f * fabsf(1.0f - fitscale) / ppd; if(up) { if(fitscale <= 1.0f && (scale == (1.0f / ppd) || scale == (2.0f / ppd)) && constrained) return; // for large image size else if(fitscale > 1.0f && fitscale <= 2.0f && scale == (2.0f / ppd) && constrained) return; // for medium image size if((oldscale <= 1.0f / ppd) && constrained && (scale + stepup >= 1.0f / ppd)) scale = 1.0f / ppd; else if((oldscale <= 2.0f / ppd) && constrained && (scale + stepup >= 2.0f / ppd)) scale = 2.0f / ppd; // calculate new scale else if(scale >= 16.0f / ppd) return; else if(scale >= 8.0f / ppd) scale = 16.0f / ppd; else if(scale >= 4.0f / ppd) scale = 8.0f / ppd; else if(scale >= 2.0f / ppd) scale = 4.0f / ppd; else if(scale >= fitscale) scale += stepup; else scale += 0.5f * stepup; } else { if(fitscale <= 2.0f && ((scale == fitscale && constrained) || scale < 0.5 * fitscale)) return; // for large and medium image size else if(fitscale > 2.0f && scale < 1.0f / ppd) return; // for small image size // calculate new scale if(scale <= fitscale) scale -= 0.5f * stepup; else if(scale <= 2.0f / ppd) scale -= stepup; else if(scale <= 4.0f / ppd) scale = 2.0f / ppd; else if(scale <= 8.0f / ppd) scale = 4.0f / ppd; else scale = 8.0f / ppd; } if (fitscale <= 1.0f) // for large image size, stop at 1:1 and FIT levels, minimum at 0.5 * FIT { if((scale - 1.0) * (oldscale - 1.0) < 0) scale = 1.0f / ppd; if((scale - fitscale) * (oldscale - fitscale) < 0) scale = fitscale; scale = fmaxf(scale, 0.5 * fitscale); } else if (fitscale > 1.0f && fitscale <= 2.0f) // for medium image size, stop at 2:1 and FIT levels, minimum at 0.5 * FIT { if((scale - 2.0) * (oldscale - 2.0) < 0) scale = 2.0f / ppd; if((scale - fitscale) * (oldscale - fitscale) < 0) scale = fitscale; scale = fmaxf(scale, 0.5 * fitscale); } else scale = fmaxf(scale, 1.0f / ppd); // for small image size, minimum at 1:1 scale = fminf(scale, 16.0f / ppd); // for 200% zoom or more we want pixel doubling instead of interpolation if(scale > 15.9999f / ppd) { scale = dt_dev_get_zoom_scale(dev, DT_ZOOM_1, 1.0, 0); zoom = DT_ZOOM_1; closeup = low_ppd ? 4 : 3; } else if(scale > 7.9999f / ppd) { scale = dt_dev_get_zoom_scale(dev, DT_ZOOM_1, 1.0, 0); zoom = DT_ZOOM_1; closeup = low_ppd ? 3 : 2; } else if(scale > 3.9999f / ppd) { scale = dt_dev_get_zoom_scale(dev, DT_ZOOM_1, 1.0, 0); zoom = DT_ZOOM_1; closeup = low_ppd ? 2 : 1; } else if(scale > 1.9999f / ppd) { scale = dt_dev_get_zoom_scale(dev, DT_ZOOM_1, 1.0, 0); zoom = DT_ZOOM_1; if(low_ppd) closeup = 1; } if(fabsf(scale - 1.0f) < 0.001f) zoom = DT_ZOOM_1; if(fabsf(scale - fitscale) < 0.001f) zoom = DT_ZOOM_FIT; dt_control_set_dev_zoom_scale(scale); dt_control_set_dev_closeup(closeup); scale = dt_dev_get_zoom_scale(dev, zoom, 1<accels; dt_develop_t *lib = (dt_develop_t *)self->data; if(!darktable.control->key_accelerators_on) return 0; if(key == accels->darkroom_preview.accel_key && state == accels->darkroom_preview.accel_mods && lib->full_preview) { dt_ui_restore_panels(darktable.gui->ui); dt_control_set_dev_zoom(lib->full_preview_last_zoom); dt_control_set_dev_zoom_x(lib->full_preview_last_zoom_x); dt_control_set_dev_zoom_y(lib->full_preview_last_zoom_y); dt_control_set_dev_closeup(lib->full_preview_last_closeup); lib->full_preview = FALSE; dt_iop_request_focus(lib->full_preview_last_module); dt_masks_set_edit_mode(darktable.develop->gui_module, lib->full_preview_masks_state); dt_dev_invalidate(darktable.develop); dt_control_queue_redraw_center(); dt_control_navigation_redraw(); } // add an option to allow skip mouse events while editing masks if(key == accels->darkroom_skip_mouse_events.accel_key && state == accels->darkroom_skip_mouse_events.accel_mods) { darktable.develop->darkroom_skip_mouse_events = FALSE; } return 1; } int key_pressed(dt_view_t *self, guint key, guint state) { const dt_control_accels_t *accels = &darktable.control->accels; dt_develop_t *lib = (dt_develop_t *)self->data; if(!darktable.control->key_accelerators_on) return 0; if(key == accels->darkroom_preview.accel_key && state == accels->darkroom_preview.accel_mods) { // hack to avoid triggering darkroom fullpreview if user enter the view with the key already pressed if(!lib->full_preview && (lib->preview_status == DT_DEV_PIXELPIPE_DIRTY || lib->preview_status == DT_DEV_PIXELPIPE_INVALID)) { lib->full_preview = TRUE; } if(!lib->full_preview) { lib->full_preview = TRUE; // we hide all panels for(int k = 0; k < DT_UI_PANEL_SIZE; k++) dt_ui_panel_show(darktable.gui->ui, k, FALSE, FALSE); // we remember the masks edit state if(darktable.develop->gui_module) { dt_iop_gui_blend_data_t *bd = (dt_iop_gui_blend_data_t *)darktable.develop->gui_module->blend_data; if (bd) lib->full_preview_masks_state = bd->masks_shown; } // we set the zoom values to "fit" lib->full_preview_last_zoom = dt_control_get_dev_zoom(); lib->full_preview_last_zoom_x = dt_control_get_dev_zoom_x(); lib->full_preview_last_zoom_y = dt_control_get_dev_zoom_y(); lib->full_preview_last_closeup = dt_control_get_dev_closeup(); dt_control_set_dev_zoom(DT_ZOOM_FIT); dt_control_set_dev_zoom_x(0); dt_control_set_dev_zoom_y(0); dt_control_set_dev_closeup(0); // we quit the active iop if any lib->full_preview_last_module = darktable.develop->gui_module; dt_iop_request_focus(NULL); gtk_widget_grab_focus(dt_ui_center(darktable.gui->ui)); dt_dev_invalidate(darktable.develop); dt_control_queue_redraw_center(); } else return 0; } if(key == GDK_KEY_Left || key == GDK_KEY_Right || key == GDK_KEY_Up || key == GDK_KEY_Down) { dt_develop_t *dev = (dt_develop_t *)self->data; dt_dev_zoom_t zoom = dt_control_get_dev_zoom(); const int closeup = dt_control_get_dev_closeup(); float scale = dt_dev_get_zoom_scale(dev, zoom, 1<width / (procw * scale); float step_changey = dev->height / (proch * scale); float factor = 0.2f; if(dt_modifier_is(state, GDK_MOD1_MASK)) factor = 0.02f; if(dt_modifier_is(state, GDK_CONTROL_MASK)) factor = 1.0f; float old_zoom_x, old_zoom_y; old_zoom_x = dt_control_get_dev_zoom_x(); old_zoom_y = dt_control_get_dev_zoom_y(); float zx = old_zoom_x; float zy = old_zoom_y; if(key == GDK_KEY_Left) zx = zx - step_changex * factor; if(key == GDK_KEY_Right) zx = zx + step_changex * factor; if(key == GDK_KEY_Up) zy = zy - step_changey * factor; if(key == GDK_KEY_Down) zy = zy + step_changey * factor; dt_dev_check_zoom_bounds(dev, &zx, &zy, zoom, closeup, NULL, NULL); dt_control_set_dev_zoom_x(zx); dt_control_set_dev_zoom_y(zy); dt_dev_invalidate(dev); dt_control_queue_redraw_center(); dt_control_navigation_redraw(); return 1; } // add an option to allow skip mouse events while editing masks if(key == accels->darkroom_skip_mouse_events.accel_key && state == accels->darkroom_skip_mouse_events.accel_mods) { darktable.develop->darkroom_skip_mouse_events = TRUE; return 1; } return 1; } static gboolean search_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { // set focus to the search module text box dt_dev_modulegroups_search_text_focus(darktable.develop); return TRUE; } static gboolean change_slider_accel_precision(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { const int curr_precision = dt_conf_get_int("accel/slider_precision"); const int new_precision = curr_precision + 1 == 3 ? 0 : curr_precision + 1; dt_conf_set_int("accel/slider_precision", new_precision); if(new_precision == DT_IOP_PRECISION_FINE) dt_toast_log(_("keyboard shortcut slider precision: fine")); else if(new_precision == DT_IOP_PRECISION_NORMAL) dt_toast_log(_("keyboard shortcut slider precision: normal")); else dt_toast_log(_("keyboard shortcut slider precision: coarse")); return TRUE; } static gboolean zoom_in_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { dt_view_t *self = (dt_view_t *)data; dt_develop_t *dev = (dt_develop_t *)self->data; scrolled(self, dev->width / 2, dev->height / 2, 1, modifier); return TRUE; } static gboolean zoom_out_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { dt_view_t *self = (dt_view_t *)data; dt_develop_t *dev = (dt_develop_t *)self->data; scrolled(self, dev->width / 2, dev->height / 2, 0, modifier); return TRUE; } void configure(dt_view_t *self, int wd, int ht) { dt_develop_t *dev = (dt_develop_t *)self->data; dev->orig_width = wd; dev->orig_height = ht; dt_dev_configure(dev, wd, ht); } void init_key_accels(dt_view_t *self) { // Zoom shortcuts dt_accel_register_view(self, NC_("accel", "zoom close-up"), GDK_KEY_1, GDK_MOD1_MASK); dt_accel_register_view(self, NC_("accel", "zoom fill"), GDK_KEY_2, GDK_MOD1_MASK); dt_accel_register_view(self, NC_("accel", "zoom fit"), GDK_KEY_3, GDK_MOD1_MASK); // zoom in/out dt_accel_register_view(self, NC_("accel", "zoom in"), GDK_KEY_plus, GDK_CONTROL_MASK); dt_accel_register_view(self, NC_("accel", "zoom out"), GDK_KEY_minus, GDK_CONTROL_MASK); // Shortcut to skip images dt_accel_register_view(self, NC_("accel", "image forward"), GDK_KEY_space, 0); dt_accel_register_view(self, NC_("accel", "image back"), GDK_KEY_BackSpace, 0); // toggle ISO 12646 color assessment condition dt_accel_register_view(self, NC_("accel", "color assessment"), GDK_KEY_b, GDK_CONTROL_MASK); // toggle raw overexposure indication dt_accel_register_view(self, NC_("accel", "raw overexposed"), GDK_KEY_o, GDK_SHIFT_MASK); // toggle overexposure indication dt_accel_register_view(self, NC_("accel", "overexposed"), GDK_KEY_o, 0); // cycle overlay colors dt_accel_register_view(self, NC_("accel", "cycle overlay colors"), GDK_KEY_o, GDK_CONTROL_MASK); // toggle softproofing dt_accel_register_view(self, NC_("accel", "softproof"), GDK_KEY_s, GDK_CONTROL_MASK); // toggle gamut check dt_accel_register_view(self, NC_("accel", "gamut check"), GDK_KEY_g, GDK_CONTROL_MASK); // toggle visibility of drawn masks for current gui module dt_accel_register_view(self, NC_("accel", "show drawn masks"), 0, 0); // brush size +/- dt_accel_register_view(self, NC_("accel", "increase brush size"), 0, 0); dt_accel_register_view(self, NC_("accel", "decrease brush size"), 0, 0); // brush hardness +/- dt_accel_register_view(self, NC_("accel", "increase brush hardness"), GDK_KEY_braceright, 0); dt_accel_register_view(self, NC_("accel", "decrease brush hardness"), GDK_KEY_braceleft, 0); // brush opacity +/- dt_accel_register_view(self, NC_("accel", "increase brush opacity"), GDK_KEY_greater, 0); dt_accel_register_view(self, NC_("accel", "decrease brush opacity"), GDK_KEY_less, 0); // fullscreen view dt_accel_register_view(self, NC_("accel", "full preview"), GDK_KEY_w, 0); // undo/redo dt_accel_register_view(self, NC_("accel", "undo"), GDK_KEY_z, GDK_CONTROL_MASK); dt_accel_register_view(self, NC_("accel", "redo"), GDK_KEY_y, GDK_CONTROL_MASK); // add an option to allow skip mouse events while editing masks dt_accel_register_view(self, NC_("accel", "allow to pan & zoom while editing masks"), GDK_KEY_a, 0); // set focus to the search modules text box dt_accel_register_view(self, NC_("accel", "search modules"), 0, 0); // change the precision for adjusting sliders with keyboard shortcuts dt_accel_register_view(self, NC_("accel", "change keyboard shortcut slider precision"), 0, 0); } static gboolean _darkroom_undo_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { dt_undo_do_undo(darktable.undo, DT_UNDO_DEVELOP); return TRUE; } static gboolean _darkroom_redo_callback(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer data) { dt_undo_do_redo(darktable.undo, DT_UNDO_DEVELOP); return TRUE; } void connect_key_accels(dt_view_t *self) { GClosure *closure; dt_develop_t *data = (dt_develop_t *)self->data; // Zoom shortcuts closure = g_cclosure_new(G_CALLBACK(zoom_key_accel), GINT_TO_POINTER(1), NULL); dt_accel_connect_view(self, "zoom close-up", closure); closure = g_cclosure_new(G_CALLBACK(zoom_key_accel), GINT_TO_POINTER(2), NULL); dt_accel_connect_view(self, "zoom fill", closure); closure = g_cclosure_new(G_CALLBACK(zoom_key_accel), GINT_TO_POINTER(3), NULL); dt_accel_connect_view(self, "zoom fit", closure); // zoom in/out closure = g_cclosure_new(G_CALLBACK(zoom_in_callback), (gpointer)self, NULL); dt_accel_connect_view(self, "zoom in", closure); closure = g_cclosure_new(G_CALLBACK(zoom_out_callback), (gpointer)self, NULL); dt_accel_connect_view(self, "zoom out", closure); // Shortcut to skip images closure = g_cclosure_new(G_CALLBACK(skip_f_key_accel_callback), (gpointer)self->data, NULL); dt_accel_connect_view(self, "image forward", closure); closure = g_cclosure_new(G_CALLBACK(skip_b_key_accel_callback), (gpointer)self->data, NULL); dt_accel_connect_view(self, "image back", closure); // toggle ISO 12646 color assessment condition closure = g_cclosure_new(G_CALLBACK(_toolbox_toggle_callback), data->iso_12646.button, NULL); dt_accel_connect_view(self, "color assessment", closure); // toggle raw overexposure indication closure = g_cclosure_new(G_CALLBACK(_toolbox_toggle_callback), data->rawoverexposed.button, NULL); dt_accel_connect_view(self, "raw overexposed", closure); // toggle overexposure indication closure = g_cclosure_new(G_CALLBACK(_toolbox_toggle_callback), data->overexposed.button, NULL); dt_accel_connect_view(self, "overexposed", closure); // cycle through overlay colors closure = g_cclosure_new(G_CALLBACK(_overlay_cycle_callback), (gpointer)self->data, NULL); dt_accel_connect_view(self, "cycle overlay colors", closure); // toggle visibility of drawn masks for current gui module closure = g_cclosure_new(G_CALLBACK(_toggle_mask_visibility_callback), (gpointer)self->data, NULL); dt_accel_connect_view(self, "show drawn masks", closure); // toggle softproof indication closure = g_cclosure_new(G_CALLBACK(_toolbox_toggle_callback), data->profile.softproof_button, NULL); dt_accel_connect_view(self, "softproof", closure); // toggle gamut indication closure = g_cclosure_new(G_CALLBACK(_toolbox_toggle_callback), data->profile.gamut_button, NULL); dt_accel_connect_view(self, "gamut check", closure); // brush size +/- closure = g_cclosure_new(G_CALLBACK(_brush_size_up_callback), (gpointer)self->data, NULL); dt_accel_connect_view(self, "increase brush size", closure); closure = g_cclosure_new(G_CALLBACK(_brush_size_down_callback), (gpointer)self->data, NULL); dt_accel_connect_view(self, "decrease brush size", closure); // brush hardness +/- closure = g_cclosure_new(G_CALLBACK(_brush_hardness_up_callback), (gpointer)self->data, NULL); dt_accel_connect_view(self, "increase brush hardness", closure); closure = g_cclosure_new(G_CALLBACK(_brush_hardness_down_callback), (gpointer)self->data, NULL); dt_accel_connect_view(self, "decrease brush hardness", closure); // brush opacity +/- closure = g_cclosure_new(G_CALLBACK(_brush_opacity_up_callback), (gpointer)self->data, NULL); dt_accel_connect_view(self, "increase brush opacity", closure); closure = g_cclosure_new(G_CALLBACK(_brush_opacity_down_callback), (gpointer)self->data, NULL); dt_accel_connect_view(self, "decrease brush opacity", closure); // undo/redo closure = g_cclosure_new(G_CALLBACK(_darkroom_undo_callback), (gpointer)self, NULL); dt_accel_connect_view(self, "undo", closure); closure = g_cclosure_new(G_CALLBACK(_darkroom_redo_callback), (gpointer)self, NULL); dt_accel_connect_view(self, "redo", closure); // search modules closure = g_cclosure_new(G_CALLBACK(search_callback), (gpointer)self, NULL); dt_accel_connect_view(self, "search modules", closure); // change the precision for adjusting sliders with keyboard shortcuts closure = g_cclosure_new(G_CALLBACK(change_slider_accel_precision), (gpointer)self, NULL); dt_accel_connect_view(self, "change keyboard shortcut slider precision", closure); } GSList *mouse_actions(const dt_view_t *self) { GSList *lm = NULL; GSList *lm2 = NULL; lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_DOUBLE_LEFT, 0, _("switch to lighttable")); lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_SCROLL, 0, _("zoom in the image")); lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_SCROLL, GDK_CONTROL_MASK, _("unbounded zoom in the image")); lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_MIDDLE, 0, _("zoom to 100% 200% and back")); lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_SCROLL, GDK_SHIFT_MASK, _("[modules] expand module without closing others")); lm = dt_mouse_action_create_simple(lm, DT_MOUSE_ACTION_DRAG_DROP, GDK_SHIFT_MASK | GDK_CONTROL_MASK, _("[modules] change module position in pipe")); const dt_develop_t *dev = (dt_develop_t *)self->data; if(dev->form_visible) { // masks lm2 = dt_masks_mouse_actions(dev->form_visible); } else if(dev->gui_module && dev->gui_module->mouse_actions) { // modules with on canvas actions lm2 = dev->gui_module->mouse_actions(dev->gui_module); } return g_slist_concat(lm, lm2); } //----------------------------------------------------------- // second darkroom window //----------------------------------------------------------- /* helper macro that applies the DPI transformation to fixed pixel values. input should be defaulting to 96 * DPI */ #define DT_PIXEL_APPLY_DPI_2ND_WND(dev, value) ((value) * dev->second_window.dpi_factor) static void dt_second_window_change_cursor(dt_develop_t *dev, dt_cursor_t curs) { GtkWidget *widget = dev->second_window.second_wnd; GdkCursor *cursor = gdk_cursor_new_for_display(gdk_display_get_default(), curs); gdk_window_set_cursor(gtk_widget_get_window(widget), cursor); g_object_unref(cursor); } static void second_window_expose(GtkWidget *widget, dt_develop_t *dev, cairo_t *cri, int32_t width, int32_t height, int32_t pointerx, int32_t pointery) { cairo_set_source_rgb(cri, .2, .2, .2); cairo_save(cri); const int32_t tb = 0; // DT_PIXEL_APPLY_DPI(dt_conf_get_int("plugins/darkroom/ui/border_size")); // account for border, make it transparent for other modules called below: pointerx -= tb; pointery -= tb; if(dev->preview2_status == DT_DEV_PIXELPIPE_DIRTY || dev->preview2_status == DT_DEV_PIXELPIPE_INVALID || dev->pipe->input_timestamp > dev->preview2_pipe->input_timestamp) dt_dev_process_preview2(dev); dt_pthread_mutex_t *mutex = NULL; const float zoom_y = dt_second_window_get_dev_zoom_y(dev); const float zoom_x = dt_second_window_get_dev_zoom_x(dev); const dt_dev_zoom_t zoom = dt_second_window_get_dev_zoom(dev); const int closeup = dt_second_window_get_dev_closeup(dev); const float backbuf_scale = dt_second_window_get_zoom_scale(dev, zoom, 1.0f, 0) * dev->second_window.ppd; static cairo_surface_t *image_surface = NULL; static int image_surface_width = 0, image_surface_height = 0, image_surface_imgid = -1; if(image_surface_width != width || image_surface_height != height || image_surface == NULL) { // create double-buffered image to draw on, to make modules draw more fluently. image_surface_width = width; image_surface_height = height; if(image_surface) cairo_surface_destroy(image_surface); image_surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width * dev->second_window.ppd, height * dev->second_window.ppd); cairo_surface_set_device_scale(image_surface, dev->second_window.ppd, dev->second_window.ppd); image_surface_imgid = -1; // invalidate old stuff } cairo_surface_t *surface; cairo_t *cr = cairo_create(image_surface); if(dev->preview2_pipe->output_backbuf && // do we have an image? dev->preview2_pipe->backbuf_scale == backbuf_scale && // is this the zoom scale we want to display? dev->preview2_pipe->backbuf_zoom_x == zoom_x && dev->preview2_pipe->backbuf_zoom_y == zoom_y) { // draw image mutex = &dev->preview2_pipe->backbuf_mutex; dt_pthread_mutex_lock(mutex); float wd = dev->preview2_pipe->output_backbuf_width; float ht = dev->preview2_pipe->output_backbuf_height; const int stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, wd); surface = cairo_image_surface_create_for_data(dev->preview2_pipe->output_backbuf, CAIRO_FORMAT_RGB24, wd, ht, stride); cairo_surface_set_device_scale(surface, dev->second_window.ppd, dev->second_window.ppd); wd /= dev->second_window.ppd; ht /= dev->second_window.ppd; dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_DARKROOM_BG); cairo_paint(cr); cairo_translate(cr, .5f * (width - wd), .5f * (height - ht)); if(closeup) { const double scale = 1<show_focus_peaking) { cairo_save(cr); cairo_scale(cr, 1.0f / dev->second_window.ppd, 1.0f / dev->second_window.ppd); dt_focuspeaking(cr, wd, ht, cairo_image_surface_get_data(surface), cairo_image_surface_get_width(surface), cairo_image_surface_get_height(surface)); cairo_restore(cr); } cairo_surface_destroy(surface); dt_pthread_mutex_unlock(mutex); image_surface_imgid = dev->image_storage.id; } else if(dev->preview_pipe->output_backbuf) { // draw preview mutex = &dev->preview_pipe->backbuf_mutex; dt_pthread_mutex_lock(mutex); const float wd = dev->preview_pipe->output_backbuf_width; const float ht = dev->preview_pipe->output_backbuf_height; const float zoom_scale = dt_second_window_get_zoom_scale(dev, zoom, 1 << closeup, 1); dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_DARKROOM_BG); cairo_paint(cr); cairo_rectangle(cr, tb, tb, width - 2 * tb, height - 2 * tb); cairo_clip(cr); const int stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, wd); surface = cairo_image_surface_create_for_data(dev->preview_pipe->output_backbuf, CAIRO_FORMAT_RGB24, wd, ht, stride); cairo_translate(cr, width / 2.0, height / 2.0f); cairo_scale(cr, zoom_scale, zoom_scale); cairo_translate(cr, -.5f * wd - zoom_x * wd, -.5f * ht - zoom_y * ht); // avoid to draw the 1px garbage that sometimes shows up in the preview :( cairo_rectangle(cr, 0, 0, wd - 1, ht - 1); cairo_set_source_surface(cr, surface, 0, 0); cairo_pattern_set_filter(cairo_get_source(cr), _get_filtering_level(dev)); cairo_fill(cr); cairo_surface_destroy(surface); dt_pthread_mutex_unlock(mutex); image_surface_imgid = dev->image_storage.id; } cairo_restore(cri); if(image_surface_imgid == dev->image_storage.id) { cairo_destroy(cr); cairo_set_source_surface(cri, image_surface, 0, 0); cairo_paint(cri); } } static void second_window_scrolled(GtkWidget *widget, dt_develop_t *dev, double x, double y, const int up, const int state) { const int32_t tb = 0; // DT_PIXEL_APPLY_DPI(dt_conf_get_int("plugins/darkroom/ui/border_size")); const int32_t capwd = dev->second_window.width - 2 * tb; const int32_t capht = dev->second_window.height - 2 * tb; const int32_t width_i = dev->second_window.width; const int32_t height_i = dev->second_window.height; if(width_i > capwd) x += (capwd - width_i) * .5f; if(height_i > capht) y += (capht - height_i) * .5f; // free zoom dt_dev_zoom_t zoom = dt_second_window_get_dev_zoom(dev); int procw, proch; int closeup = dt_second_window_get_dev_closeup(dev); float zoom_x = dt_second_window_get_dev_zoom_x(dev); float zoom_y = dt_second_window_get_dev_zoom_y(dev); dt_second_window_get_processed_size(dev, &procw, &proch); float scale = dt_second_window_get_zoom_scale(dev, zoom, 1 << closeup, 0); const float ppd = dev->second_window.ppd; const float fitscale = dt_second_window_get_zoom_scale(dev, DT_ZOOM_FIT, 1.0, 0); const float oldscale = scale; // offset from center now (current zoom_{x,y} points there) const float mouse_off_x = x - 0.5f * dev->second_window.width; const float mouse_off_y = y - 0.5f * dev->second_window.height; zoom_x += mouse_off_x / (procw * scale); zoom_y += mouse_off_y / (proch * scale); zoom = DT_ZOOM_FREE; closeup = 0; const gboolean constrained = !dt_modifier_is(state, GDK_CONTROL_MASK); const gboolean low_ppd = (dev->second_window.ppd == 1); const float stepup = 0.1f * fabsf(1.0f - fitscale) / ppd; if(up) { if(fitscale <= 1.0f && (scale == (1.0f / ppd) || scale == (2.0f / ppd)) && constrained) return; // for large image size else if(fitscale > 1.0f && fitscale <= 2.0f && scale == (2.0f / ppd) && constrained) return; // for medium image size if((oldscale <= 1.0f / ppd) && constrained && (scale + stepup >= 1.0f / ppd)) scale = 1.0f / ppd; else if((oldscale <= 2.0f / ppd) && constrained && (scale + stepup >= 2.0f / ppd)) scale = 2.0f / ppd; // calculate new scale else if(scale >= 16.0f / ppd) return; else if(scale >= 8.0f / ppd) scale = 16.0f / ppd; else if(scale >= 4.0f / ppd) scale = 8.0f / ppd; else if(scale >= 2.0f / ppd) scale = 4.0f / ppd; else if(scale >= fitscale) scale += stepup; else scale += 0.5f * stepup; } else { if(fitscale <= 2.0f && ((scale == fitscale && constrained) || scale < 0.5 * fitscale)) return; // for large and medium image size else if(fitscale > 2.0f && scale < 1.0f / ppd) return; // for small image size // calculate new scale if(scale <= fitscale) scale -= 0.5f * stepup; else if(scale <= 2.0f / ppd) scale -= stepup; else if(scale <= 4.0f / ppd) scale = 2.0f / ppd; else if(scale <= 8.0f / ppd) scale = 4.0f / ppd; else scale = 8.0f / ppd; } if (fitscale <= 1.0f) // for large image size, stop at 1:1 and FIT levels, minimum at 0.5 * FIT { if((scale - 1.0) * (oldscale - 1.0) < 0) scale = 1.0f / ppd; if((scale - fitscale) * (oldscale - fitscale) < 0) scale = fitscale; scale = fmaxf(scale, 0.5 * fitscale); } else if (fitscale > 1.0f && fitscale <= 2.0f) // for medium image size, stop at 2:1 and FIT levels, minimum at 0.5 * FIT { if((scale - 2.0) * (oldscale - 2.0) < 0) scale = 2.0f / ppd; if((scale - fitscale) * (oldscale - fitscale) < 0) scale = fitscale; scale = fmaxf(scale, 0.5 * fitscale); } else scale = fmaxf(scale, 1.0f / ppd); // for small image size, minimum at 1:1 scale = fminf(scale, 16.0f / ppd); // for 200% zoom or more we want pixel doubling instead of interpolation if(scale > 15.9999f / ppd) { scale = dt_dev_get_zoom_scale(dev, DT_ZOOM_1, 1.0, 0); zoom = DT_ZOOM_1; closeup = low_ppd ? 4 : 3; } else if(scale > 7.9999f / ppd) { scale = dt_dev_get_zoom_scale(dev, DT_ZOOM_1, 1.0, 0); zoom = DT_ZOOM_1; closeup = low_ppd ? 3 : 2; } else if(scale > 3.9999f / ppd) { scale = dt_dev_get_zoom_scale(dev, DT_ZOOM_1, 1.0, 0); zoom = DT_ZOOM_1; closeup = low_ppd ? 2 : 1; } else if(scale > 1.9999f / ppd) { scale = dt_dev_get_zoom_scale(dev, DT_ZOOM_1, 1.0, 0); zoom = DT_ZOOM_1; if(low_ppd) closeup = 1; } if(fabsf(scale - 1.0f) < 0.001f) zoom = DT_ZOOM_1; if(fabsf(scale - fitscale) < 0.001f) zoom = DT_ZOOM_FIT; dt_second_window_set_zoom_scale(dev, scale); dt_second_window_set_dev_closeup(dev, closeup); scale = dt_second_window_get_zoom_scale(dev, zoom, 1 << closeup, 0); zoom_x -= mouse_off_x / (procw * scale); zoom_y -= mouse_off_y / (proch * scale); dt_second_window_check_zoom_bounds(dev, &zoom_x, &zoom_y, zoom, closeup, NULL, NULL); dt_second_window_set_dev_zoom(dev, zoom); dt_second_window_set_dev_zoom_x(dev, zoom_x); dt_second_window_set_dev_zoom_y(dev, zoom_y); // pipe needs to be reconstructed dev->preview2_status = DT_DEV_PIXELPIPE_DIRTY; gtk_widget_queue_draw(widget); } static void second_window_leave(dt_develop_t *dev) { // reset any changes the selected plugin might have made. dt_second_window_change_cursor(dev, GDK_LEFT_PTR); } static int second_window_button_pressed(GtkWidget *widget, dt_develop_t *dev, double x, double y, const double pressure, const int which, const int type, const uint32_t state) { const int32_t tb = 0; // DT_PIXEL_APPLY_DPI(dt_conf_get_int("plugins/darkroom/ui/border_size")); const int32_t capwd = dev->second_window.width - 2 * tb; const int32_t capht = dev->second_window.height - 2 * tb; const int32_t width_i = dev->second_window.width; const int32_t height_i = dev->second_window.height; if(width_i > capwd) x += (capwd - width_i) * .5f; if(height_i > capht) y += (capht - height_i) * .5f; dev->second_window.button_x = x - tb; dev->second_window.button_y = y - tb; if(which == 1 && type == GDK_2BUTTON_PRESS) return 0; if(which == 1) { dt_second_window_change_cursor(dev, GDK_HAND1); return 1; } if(which == 2) { // zoom to 1:1 2:1 and back int procw, proch; dt_dev_zoom_t zoom = dt_second_window_get_dev_zoom(dev); int closeup = dt_second_window_get_dev_closeup(dev); float zoom_x = dt_second_window_get_dev_zoom_x(dev); float zoom_y = dt_second_window_get_dev_zoom_y(dev); dt_second_window_get_processed_size(dev, &procw, &proch); float scale = dt_second_window_get_zoom_scale(dev, zoom, 1 << closeup, 0); const float ppd = dev->second_window.ppd; const gboolean low_ppd = dev->second_window.ppd == 1; const float mouse_off_x = x - 0.5f * dev->second_window.width; const float mouse_off_y = y - 0.5f * dev->second_window.height; zoom_x += mouse_off_x / (procw * scale); zoom_y += mouse_off_y / (proch * scale); const float tscale = scale * ppd; closeup = 0; if((tscale > 0.95f) && (tscale < 1.05f)) // we are at 100% and switch to 200% { zoom = DT_ZOOM_1; scale = dt_dev_get_zoom_scale(dev, DT_ZOOM_1, 1.0, 0); if(low_ppd) closeup = 1; } else if((tscale > 1.95f) && (tscale < 2.05f)) // at 200% so switch to zoomfit { zoom = DT_ZOOM_FIT; scale = dt_dev_get_zoom_scale(dev, DT_ZOOM_FIT, 1.0, 0); } else // other than 100 or 200% so zoom to 100 % { if(low_ppd) { zoom = DT_ZOOM_1; scale = dt_dev_get_zoom_scale(dev, DT_ZOOM_1, 1.0, 0); } else { zoom = DT_ZOOM_FREE; scale = 1.0f / ppd; } } dt_second_window_set_zoom_scale(dev, scale); dt_second_window_set_dev_closeup(dev, closeup); scale = dt_second_window_get_zoom_scale(dev, zoom, 1 << closeup, 0); zoom_x -= mouse_off_x / (procw * scale); zoom_y -= mouse_off_y / (proch * scale); dt_second_window_check_zoom_bounds(dev, &zoom_x, &zoom_y, zoom, closeup, NULL, NULL); dt_second_window_set_dev_zoom(dev, zoom); dt_second_window_set_dev_zoom_x(dev, zoom_x); dt_second_window_set_dev_zoom_y(dev, zoom_y); // pipe needs to be reconstructed dev->preview2_status = DT_DEV_PIXELPIPE_DIRTY; gtk_widget_queue_draw(widget); return 1; } return 0; } static int second_window_button_released(dt_develop_t *dev, const double x, const double y, const int which, const uint32_t state) { if(which == 1) dt_second_window_change_cursor(dev, GDK_LEFT_PTR); return 1; } static void second_window_mouse_moved(GtkWidget *widget, dt_develop_t *dev, double x, double y, const double pressure, const int which) { const int32_t tb = 0; // DT_PIXEL_APPLY_DPI(dt_conf_get_int("plugins/darkroom/ui/border_size")); const int32_t capwd = dev->second_window.width - 2 * tb; const int32_t capht = dev->second_window.height - 2 * tb; const int32_t width_i = dev->second_window.width; const int32_t height_i = dev->second_window.height; int32_t offx = 0.0f, offy = 0.0f; if(width_i > capwd) offx = (capwd - width_i) * .5f; if(height_i > capht) offy = (capht - height_i) * .5f; x += offx; y += offy; if(which & GDK_BUTTON1_MASK) { // depending on dev_zoom, adjust dev_zoom_x/y. const dt_dev_zoom_t zoom = dt_second_window_get_dev_zoom(dev); const int closeup = dt_second_window_get_dev_closeup(dev); int procw, proch; dt_second_window_get_processed_size(dev, &procw, &proch); const float scale = dt_second_window_get_zoom_scale(dev, zoom, 1 << closeup, 0); float old_zoom_x, old_zoom_y; old_zoom_x = dt_second_window_get_dev_zoom_x(dev); old_zoom_y = dt_second_window_get_dev_zoom_y(dev); float zx = old_zoom_x - (1.0 / scale) * (x - dev->second_window.button_x - offx) / procw; float zy = old_zoom_y - (1.0 / scale) * (y - dev->second_window.button_y - offy) / proch; dt_second_window_check_zoom_bounds(dev, &zx, &zy, zoom, closeup, NULL, NULL); dt_second_window_set_dev_zoom_x(dev, zx); dt_second_window_set_dev_zoom_y(dev, zy); dev->second_window.button_x = x - offx; dev->second_window.button_y = y - offy; // pipe needs to be reconstructed dev->preview2_status = DT_DEV_PIXELPIPE_DIRTY; gtk_widget_queue_draw(widget); } } static void _second_window_configure_ppd_dpi(dt_develop_t *dev) { GtkWidget *widget = dev->second_window.second_wnd; dev->second_window.ppd = dev->second_window.ppd_thb = dt_get_system_gui_ppd(widget); if(dt_conf_get_bool("ui/performance")) dev->second_window.ppd_thb *= DT_GUI_THUMBSIZE_REDUCE; // get the screen resolution float screen_dpi_overwrite = dt_conf_get_float("screen_dpi_overwrite"); if(screen_dpi_overwrite > 0.0) { dev->second_window.dpi = screen_dpi_overwrite; gdk_screen_set_resolution(gtk_widget_get_screen(widget), screen_dpi_overwrite); dt_print(DT_DEBUG_CONTROL, "[screen resolution] setting the screen resolution to %f dpi as specified in " "the configuration file\n", screen_dpi_overwrite); } else { #ifdef GDK_WINDOWING_QUARTZ dt_osx_autoset_dpi(widget); #endif dev->second_window.dpi = gdk_screen_get_resolution(gtk_widget_get_screen(widget)); if(dev->second_window.dpi < 0.0) { dev->second_window.dpi = 96.0; gdk_screen_set_resolution(gtk_widget_get_screen(widget), 96.0); dt_print(DT_DEBUG_CONTROL, "[screen resolution] setting the screen resolution to the default 96 dpi\n"); } else dt_print(DT_DEBUG_CONTROL, "[screen resolution] setting the screen resolution to %f dpi\n", dev->second_window.dpi); } dev->second_window.dpi_factor = dev->second_window.dpi / 96; // according to man xrandr and the docs of gdk_screen_set_resolution 96 is the default } static gboolean _second_window_draw_callback(GtkWidget *widget, cairo_t *crf, dt_develop_t *dev) { int pointerx, pointery; GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); const int32_t width = allocation.width; const int32_t height = allocation.height; dev->second_window.width = width; dev->second_window.height = height; #if GTK_CHECK_VERSION(3, 20, 0) gdk_window_get_device_position(gtk_widget_get_window(widget), gdk_seat_get_pointer(gdk_display_get_default_seat(gtk_widget_get_display(widget))), &pointerx, &pointery, NULL); #else GdkDevice *device = gdk_device_manager_get_client_pointer(gdk_display_get_device_manager(gtk_widget_get_display(widget))); gdk_window_get_device_position(gtk_widget_get_window(widget), device, &pointerx, &pointery, NULL); #endif second_window_expose(widget, dev, crf, width, height, pointerx, pointery); return TRUE; } static gboolean _second_window_scrolled_callback(GtkWidget *widget, GdkEventScroll *event, dt_develop_t *dev) { int delta_y; if(dt_gui_get_scroll_unit_deltas(event, NULL, &delta_y)) { second_window_scrolled(widget, dev, event->x, event->y, delta_y < 0, event->state & 0xf); gtk_widget_queue_draw(widget); } return TRUE; } static gboolean _second_window_button_pressed_callback(GtkWidget *w, GdkEventButton *event, dt_develop_t *dev) { double pressure = 1.0; GdkDevice *device = gdk_event_get_source_device((GdkEvent *)event); if(device && gdk_device_get_source(device) == GDK_SOURCE_PEN) { gdk_event_get_axis((GdkEvent *)event, GDK_AXIS_PRESSURE, &pressure); } second_window_button_pressed(w, dev, event->x, event->y, pressure, event->button, event->type, event->state & 0xf); gtk_widget_grab_focus(w); gtk_widget_queue_draw(w); return FALSE; } static gboolean _second_window_button_released_callback(GtkWidget *w, GdkEventButton *event, dt_develop_t *dev) { second_window_button_released(dev, event->x, event->y, event->button, event->state & 0xf); gtk_widget_queue_draw(w); return TRUE; } static gboolean _second_window_mouse_moved_callback(GtkWidget *w, GdkEventMotion *event, dt_develop_t *dev) { double pressure = 1.0; GdkDevice *device = gdk_event_get_source_device((GdkEvent *)event); if(device && gdk_device_get_source(device) == GDK_SOURCE_PEN) { gdk_event_get_axis((GdkEvent *)event, GDK_AXIS_PRESSURE, &pressure); } second_window_mouse_moved(w, dev, event->x, event->y, pressure, event->state); return FALSE; } static gboolean _second_window_leave_callback(GtkWidget *widget, GdkEventCrossing *event, dt_develop_t *dev) { second_window_leave(dev); return TRUE; } static gboolean _second_window_configure_callback(GtkWidget *da, GdkEventConfigure *event, dt_develop_t *dev) { static int oldw = 0; static int oldh = 0; if(oldw != event->width || oldh != event->height) { dev->second_window.width = event->width; dev->second_window.height = event->height; // pipe needs to be reconstructed dev->preview2_status = DT_DEV_PIXELPIPE_DIRTY; dev->preview2_pipe->changed |= DT_DEV_PIPE_REMOVE; dev->preview2_pipe->cache_obsolete = 1; } oldw = event->width; oldh = event->height; dt_colorspaces_set_display_profile(DT_COLORSPACE_DISPLAY2); #ifndef GDK_WINDOWING_QUARTZ _second_window_configure_ppd_dpi(dev); #endif return TRUE; } static void _darkroom_ui_second_window_init(GtkWidget *widget, dt_develop_t *dev) { const int width = MAX(10, dt_conf_get_int("second_window/window_w")); const int height = MAX(10, dt_conf_get_int("second_window/window_h")); dev->second_window.width = width; dev->second_window.height = height; const gint x = MAX(0, dt_conf_get_int("second_window/window_x")); const gint y = MAX(0, dt_conf_get_int("second_window/window_y")); gtk_window_set_default_size(GTK_WINDOW(widget), width, height); gtk_widget_show_all(widget); gtk_window_move(GTK_WINDOW(widget), x, y); gtk_window_resize(GTK_WINDOW(widget), width, height); const int fullscreen = dt_conf_get_bool("second_window/fullscreen"); if(fullscreen) gtk_window_fullscreen(GTK_WINDOW(widget)); else { gtk_window_unfullscreen(GTK_WINDOW(widget)); const int maximized = dt_conf_get_bool("second_window/maximized"); if(maximized) gtk_window_maximize(GTK_WINDOW(widget)); else gtk_window_unmaximize(GTK_WINDOW(widget)); } } static void _darkroom_ui_second_window_write_config(GtkWidget *widget) { GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); gint x, y; gtk_window_get_position(GTK_WINDOW(widget), &x, &y); dt_conf_set_int("second_window/window_x", x); dt_conf_set_int("second_window/window_y", y); dt_conf_set_int("second_window/window_w", allocation.width); dt_conf_set_int("second_window/window_h", allocation.height); dt_conf_set_bool("second_window/maximized", (gdk_window_get_state(gtk_widget_get_window(widget)) & GDK_WINDOW_STATE_MAXIMIZED)); dt_conf_set_bool("second_window/fullscreen", (gdk_window_get_state(gtk_widget_get_window(widget)) & GDK_WINDOW_STATE_FULLSCREEN)); } static gboolean _second_window_delete_callback(GtkWidget *widget, GdkEvent *event, dt_develop_t *dev) { _darkroom_ui_second_window_write_config(dev->second_window.second_wnd); dev->second_window.second_wnd = NULL; dev->second_window.widget = NULL; gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dev->second_window.button), FALSE); return FALSE; } static gboolean _second_window_key_pressed_callback(GtkWidget *widget, GdkEventKey *event, dt_develop_t *dev) { int fullscreen; GtkAccelKey key_on, key_off; char path_on[256]; char path_off[256]; dt_accel_path_global(path_on, sizeof(path_on), "toggle fullscreen"); dt_accel_path_global(path_off, sizeof(path_off), "leave fullscreen"); gtk_accel_map_lookup_entry(path_on, &key_on); gtk_accel_map_lookup_entry(path_off, &key_off); if(event->keyval == key_on.accel_key && dt_modifier_is(event->state, key_on.accel_mods)) { fullscreen = gdk_window_get_state(gtk_widget_get_window(widget)) & GDK_WINDOW_STATE_FULLSCREEN; if(fullscreen) gtk_window_unfullscreen(GTK_WINDOW(widget)); else gtk_window_fullscreen(GTK_WINDOW(widget)); } else if(event->keyval == key_off.accel_key && dt_modifier_is(event->state, key_off.accel_mods)) { gtk_window_unfullscreen(GTK_WINDOW(widget)); } else { return FALSE; } /* redraw center view */ gtk_widget_queue_draw(dev->second_window.widget); #ifdef __APPLE__ // workaround for GTK Quartz backend bug gtk_window_set_title(GTK_WINDOW(widget), _("darktable - darkroom preview")); #endif return TRUE; } static void _darkroom_display_second_window(dt_develop_t *dev) { if(dev->second_window.second_wnd == NULL) { dev->second_window.width = -1; dev->second_window.height = -1; dev->second_window.second_wnd = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_widget_set_name(dev->second_window.second_wnd, "second_window"); _second_window_configure_ppd_dpi(dev); gtk_window_set_icon_name(GTK_WINDOW(dev->second_window.second_wnd), "darktable"); gtk_window_set_title(GTK_WINDOW(dev->second_window.second_wnd), _("darktable - darkroom preview")); GtkWidget *container = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_container_add(GTK_CONTAINER(dev->second_window.second_wnd), container); GtkWidget *widget = gtk_grid_new(); gtk_box_pack_start(GTK_BOX(container), widget, TRUE, TRUE, 0); dev->second_window.widget = gtk_drawing_area_new(); gtk_widget_set_size_request(dev->second_window.widget, DT_PIXEL_APPLY_DPI_2ND_WND(dev, 50), DT_PIXEL_APPLY_DPI_2ND_WND(dev, 200)); gtk_widget_set_hexpand(dev->second_window.widget, TRUE); gtk_widget_set_vexpand(dev->second_window.widget, TRUE); gtk_widget_set_app_paintable(dev->second_window.widget, TRUE); gtk_grid_attach(GTK_GRID(widget), dev->second_window.widget, 0, 0, 1, 1); gtk_widget_set_events(dev->second_window.widget, GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | darktable.gui->scroll_mask); /* connect callbacks */ g_signal_connect(G_OBJECT(dev->second_window.widget), "draw", G_CALLBACK(_second_window_draw_callback), dev); g_signal_connect(G_OBJECT(dev->second_window.widget), "scroll-event", G_CALLBACK(_second_window_scrolled_callback), dev); g_signal_connect(G_OBJECT(dev->second_window.widget), "button-press-event", G_CALLBACK(_second_window_button_pressed_callback), dev); g_signal_connect(G_OBJECT(dev->second_window.widget), "button-release-event", G_CALLBACK(_second_window_button_released_callback), dev); g_signal_connect(G_OBJECT(dev->second_window.widget), "motion-notify-event", G_CALLBACK(_second_window_mouse_moved_callback), dev); g_signal_connect(G_OBJECT(dev->second_window.widget), "leave-notify-event", G_CALLBACK(_second_window_leave_callback), dev); g_signal_connect(G_OBJECT(dev->second_window.widget), "configure-event", G_CALLBACK(_second_window_configure_callback), dev); g_signal_connect(G_OBJECT(dev->second_window.second_wnd), "delete-event", G_CALLBACK(_second_window_delete_callback), dev); g_signal_connect(G_OBJECT(dev->second_window.second_wnd), "key-press-event", G_CALLBACK(_second_window_key_pressed_callback), dev); _darkroom_ui_second_window_init(dev->second_window.second_wnd, dev); } gtk_widget_show_all(dev->second_window.second_wnd); } // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh // vim: shiftwidth=2 expandtab tabstop=2 cindent // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;