/* This file is part of darktable, Copyright (C) 2011-2026 darktable developers. darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. darktable is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with darktable. If not, see . */ #include "common/gdk_event_utils.h" #include "common/collection.h" #include "common/darktable.h" #include "common/debug.h" #include "common/image_cache.h" #include "common/metadata.h" #include "common/tags.h" #include "common/datetime.h" #include "control/conf.h" #include "control/control.h" #include "develop/develop.h" #include "gui/accelerators.h" #include "gui/gtk.h" #include "libs/lib.h" #include "libs/lib_api.h" #include #include #ifdef GDK_WINDOWING_QUARTZ #include "osx/osx.h" #endif #ifdef USE_LUA #include "lua/call.h" #include "lua/image.h" #endif DT_MODULE(1) typedef enum dt_metadata_pref_cols_t { DT_METADATA_PREF_COL_INDEX = 0, // index DT_METADATA_PREF_COL_NAME_L, // displayed name DT_METADATA_PREF_COL_VISIBLE, // visibility DT_METADATA_PREF_NUM_COLS } dt_metadata_pref_cols_t; typedef struct dt_lib_metadata_view_t { GtkWidget *grid; GList *metadata; uint32_t metadata_count; GObject *filmroll_event; } dt_lib_metadata_view_t; typedef struct dt_lib_metadata_info_t { int index; // md_xx value or index inserted by lua int order; // display order char *name; // metadata name char *value; // metadata value char *setting; // setting name char *tooltip; // tooltip gboolean visible; uint32_t key; // metadata db-key } dt_lib_metadata_info_t; enum { /* internal */ md_internal_filmroll = 0, md_internal_imgid, md_internal_groupid, md_internal_filename, md_internal_version, md_internal_fullpath, md_internal_local_copy, md_internal_import_timestamp, md_internal_change_timestamp, md_internal_export_timestamp, md_internal_print_timestamp, md_internal_flags, /* exif */ md_exif_model, md_exif_maker, md_exif_lens, md_exif_aperture, md_exif_exposure, md_exif_exposure_bias, md_exif_exposure_program, md_exif_whitebalance, md_exif_flash, md_exif_metering_mode, md_exif_focal_length, md_exif_focal_length_ff, md_exif_crop_factor, md_exif_focus_distance, md_exif_iso, md_exif_datetime, md_exif_width, md_exif_height, /* size of final image */ md_width, md_height, /* geotagging */ md_geotagging_lat, md_geotagging_lon, md_geotagging_ele, /* tags */ md_tag_names, md_categories, /* xmp */ md_xmp_metadata // keep this at last position! }; // We have to maintain the correspondence between the displayed metadata list // and the list returned by the SQL query that counts the distinctive values // of metadata fields for the selected images. If you make changes to this // list, don't forget to make changes to the SQL query as well. static const char *_labels[] = { /* internal */ N_("filmroll"), N_("image id"), N_("group id"), N_("filename"), N_("version"), N_("full path"), N_("local copy"), N_("import timestamp"), N_("change timestamp"), N_("export timestamp"), N_("print timestamp"), N_("flags"), /* exif */ N_("model"), N_("maker"), N_("lens"), N_("aperture"), N_("exposure"), N_("exposure bias"), N_("exposure program"), N_("white balance"), N_("flash"), N_("metering mode"), N_("focal length"), N_("35mm equiv focal length"), N_("crop factor"), N_("focus distance"), N_("ISO"), N_("datetime"), N_("width"), N_("height"), N_("export width"), N_("export height"), /* geotagging */ N_("latitude"), N_("longitude"), N_("elevation"), /* tags */ N_("tags"), N_("categories"), }; const char *name(dt_lib_module_t *self) { return _("image information"); } dt_view_type_flags_t views(dt_lib_module_t *self) { return DT_VIEW_ALL; } uint32_t container(dt_lib_module_t *self) { return DT_UI_CONTAINER_PANEL_LEFT_CENTER; } int position(const dt_lib_module_t *self) { return 299; } #define NODATA_STRING "-" // initialize the metadata queue static void _lib_metadata_init_queue(dt_lib_module_t *self) { dt_lib_metadata_view_t *d = self->data; d->metadata = NULL; uint32_t i = 0; while(i < md_xmp_metadata) { dt_lib_metadata_info_t *m = g_try_malloc0(sizeof(dt_lib_metadata_info_t)); if(m) { m->name = g_strdup(_labels[i]); m->value = g_strdup(NODATA_STRING); m->index = m->order = i; m->visible = TRUE; m->setting = g_strdup(_labels[i]); m->key = -1; // we don't need an id for internal items d->metadata = g_list_prepend(d->metadata, m); } i++; } // metadata d->metadata_count = 0; dt_pthread_mutex_lock(&darktable.metadata_threadsafe); for(GList *iter = dt_metadata_get_list(); iter; iter = iter->next) { dt_metadata_t *metadata = (dt_metadata_t *)iter->data; if(!metadata->internal) { dt_lib_metadata_info_t *m = g_try_malloc0(sizeof(dt_lib_metadata_info_t)); if(m) { m->name = g_strdup(metadata->name); m->value = g_strdup(NODATA_STRING); m->index = m->order = i; m->visible = TRUE; m->setting = g_strdup(dt_metadata_get_tag_subkey(metadata->tagname)); m->key = metadata->key; d->metadata = g_list_prepend(d->metadata, m); } d->metadata_count++; i++; } } dt_pthread_mutex_unlock(&darktable.metadata_threadsafe); d->metadata = g_list_reverse(d->metadata); } // helper which eliminates non-printable characters from a string // strings which are already in valid UTF-8 are retained. static void _filter_non_printable(char *string, const size_t length) { /* explicitly tell the validator to ignore the trailixsng nulls, * otherwise this fails */ if(g_utf8_validate(string, -1, 0)) return; unsigned char *str = (unsigned char *)string; int n = 0; while(*str != '\000' && n < length) { if((*str < 0x20) || (*str >= 0x7f)) *str = '.'; str++; n++; } } static dt_lib_metadata_info_t *_get_metadata_per_index(const int index, dt_lib_module_t *self) { dt_lib_metadata_view_t *d = self->data; for(GList *meta = d->metadata; meta; meta = g_list_next(meta)) { dt_lib_metadata_info_t *m = meta->data; if(m->index == index) return m; } return NULL; } static void _metadata_update_markup(const gint32 i, const char *const format, dt_lib_module_t *self) { dt_lib_metadata_info_t *m = _get_metadata_per_index(i, self); dt_lib_metadata_view_t *d = self->data; GtkLabel *label = GTK_LABEL(gtk_grid_get_child_at(GTK_GRID(d->grid), 1, m->order)); char *markup = g_markup_printf_escaped(format, gtk_label_get_text(label)); gtk_label_set_markup(label, markup); g_free(markup); } /* helper function for updating a metadata value */ static void _metadata_update_value(const int i, const char *value, dt_lib_module_t *self) { dt_lib_metadata_view_t *d = self->data; gboolean validated = g_utf8_validate(value, -1, NULL); const gchar *str = validated ? value : NODATA_STRING; dt_lib_metadata_info_t *m = _get_metadata_per_index(i, self); if(m) { if(m->value) g_free(m->value); m->value = g_strdup(str); GtkWidget *w_value = gtk_grid_get_child_at(GTK_GRID(d->grid), 1, m->order); gtk_label_set_text(GTK_LABEL(w_value), str); const char *tooltip = m->tooltip ? m->tooltip : m->value; gtk_widget_set_tooltip_text(GTK_WIDGET(w_value), tooltip); } } static void _metadata_update_tooltip(const int i, const char *tooltip, dt_lib_module_t *self) { dt_lib_metadata_info_t *m = _get_metadata_per_index(i, self); if(m) { if(m->tooltip) g_free(m->tooltip); m->tooltip = g_strdup(tooltip); } } static void _metadata_update_timestamp(const int i, const GTimeSpan gts, dt_lib_module_t *self) { char datetime[200]; const gboolean valid = gts ? dt_datetime_gtimespan_to_local(datetime, sizeof(datetime), gts, FALSE, TRUE) : FALSE; _metadata_update_value(i, valid ? datetime : NODATA_STRING, self); } static gint _lib_metadata_sort_order(gconstpointer a, gconstpointer b) { dt_lib_metadata_info_t *ma = (dt_lib_metadata_info_t *)a; dt_lib_metadata_info_t *mb = (dt_lib_metadata_info_t *)b; return ma->order - mb->order; } static gint _lib_metadata_sort_index(gconstpointer a, gconstpointer b) { dt_lib_metadata_info_t *ma = (dt_lib_metadata_info_t *)a; dt_lib_metadata_info_t *mb = (dt_lib_metadata_info_t *)b; return ma->index - mb->index; } static void _metadata_get_flags(const dt_image_t *const img, char *const text, char *const tooltip, const size_t tooltip_size) { #define EMPTY_FIELD '.' #define FALSE_FIELD '.' #define TRUE_FIELD '!' #define FLAG_NB 15 char *flags_tooltip = NULL; char *flag_descriptions[] = { N_("unused"), N_("unused/deprecated"), N_("LDR"), N_("raw"), N_("HDR"), N_("marked for deletion"), N_("auto-applying presets applied"), N_("local copy"), N_("has .txt"), N_("has .wav"), N_("monochrome") }; char *tooltip_parts[FLAG_NB] = { 0 }; int next_tooltip_part = 0; gchar *value = g_strnfill(FLAG_NB, EMPTY_FIELD); int stars = img->flags & 0x7; char *star_string = NULL; if(stars == 6 || img->flags & 0x8) { value[0] = 'x'; tooltip_parts[next_tooltip_part++] = _("image rejected"); } else { value[0] = '0' + stars; tooltip_parts[next_tooltip_part++] = star_string = g_strdup_printf(ngettext("image has %d star", "image has %d stars", stars), stars); } if(img->flags & DT_IMAGE_THUMBNAIL_DEPRECATED) { value[2] = TRUE_FIELD; tooltip_parts[next_tooltip_part++] = _(flag_descriptions[1]); } else value[2] = FALSE_FIELD; if(img->flags & DT_IMAGE_LDR) { value[3] = 'l'; tooltip_parts[next_tooltip_part++] = _(flag_descriptions[2]); } if(img->flags & DT_IMAGE_RAW) { value[4] = 'r'; tooltip_parts[next_tooltip_part++] = _(flag_descriptions[3]); } if(img->flags & DT_IMAGE_HDR) { value[5] = 'h'; tooltip_parts[next_tooltip_part++] = _(flag_descriptions[4]); } if(img->flags & DT_IMAGE_REMOVE) { value[6] = 'd'; tooltip_parts[next_tooltip_part++] = _(flag_descriptions[5]); } if(img->flags & DT_IMAGE_AUTO_PRESETS_APPLIED) { value[7] = 'a'; tooltip_parts[next_tooltip_part++] = _(flag_descriptions[6]); } if(img->flags & DT_IMAGE_LOCAL_COPY) { value[8] = 'c'; tooltip_parts[next_tooltip_part++] = _(flag_descriptions[7]); } if(img->flags & DT_IMAGE_HAS_TXT) { value[9] = 't'; tooltip_parts[next_tooltip_part++] = _(flag_descriptions[8]); } if(img->flags & DT_IMAGE_HAS_WAV) { value[10] = 'w'; tooltip_parts[next_tooltip_part++] = _(flag_descriptions[9]); } if(dt_image_monochrome_flags(img)) { value[11] = 'm'; tooltip_parts[next_tooltip_part++] = _(flag_descriptions[10]); } const int loader = (unsigned int)img->loader < LOADER_COUNT ? img->loader : 0; value[12] = loaders_info[loader].flag; char *loader_tooltip = g_strdup_printf(_("loader: %s"), _(loaders_info[loader].tooltip)); tooltip_parts[next_tooltip_part++] = loader_tooltip; value[13] = '\0'; flags_tooltip = g_strjoinv("\n", tooltip_parts); g_free(loader_tooltip); (void)g_strlcpy(text, value, FLAG_NB); (void)g_strlcpy(tooltip, flags_tooltip, tooltip_size); g_free(value); g_free(star_string); g_free(flags_tooltip); #undef EMPTY_FIELD #undef FALSE_FIELD #undef TRUE_FIELD #undef FLAG_NB } #ifdef USE_LUA static int lua_update_metadata(lua_State*L); #endif /* update all values to reflect mouse over image id or no data at all */ void gui_update(dt_lib_module_t *self) { dt_lib_metadata_view_t *d = self->data; dt_imgid_t mouse_over_id = dt_control_get_mouse_over_id(); int32_t count = 0; gchar *images = NULL; if(!dt_is_valid_imgid(mouse_over_id)) { if(dt_view_get_current() == DT_VIEW_DARKROOM) { mouse_over_id = darktable.develop->image_storage.id; } else { images = dt_act_on_get_query(FALSE); count = dt_image_count_in_query(images, &mouse_over_id); // Still 0 => no selection in progress if(count == 0) { goto fill_minuses; } } } else // over an image { count = 1; } gboolean *skip = g_malloc0((md_xmp_metadata + d->metadata_count) * sizeof(gboolean)); if(count > 1) { if(!images) images = dt_act_on_get_query(FALSE); gchar *metadata_subquery = g_strdup(""); dt_pthread_mutex_lock(&darktable.metadata_threadsafe); for(GList *iter = dt_metadata_get_list(); iter; iter = iter->next) { dt_metadata_t *metadata = (dt_metadata_t *)iter->data; if(!metadata->internal) { dt_util_str_cat(&metadata_subquery, "(SELECT COUNT(DISTINCT IFNULL(value,''))" " FROM images" " LEFT JOIN meta_data" " ON meta_data.id = images.id" " AND key = %d WHERE images.id in (%s)), ", metadata->key, images); } } dt_pthread_mutex_unlock(&darktable.metadata_threadsafe); if(strlen(metadata_subquery) > 0) { // remove last comma gchar *c; if((c = g_strrstr(metadata_subquery, ", ")) != NULL) *c = '\0'; } sqlite3_stmt *stmt = NULL; // We have to maintain a correspondence between the list of fields in this SQL query // and the list of metadata fields defined by the enum and _labels[] array above. // clang-format off gchar *query = g_strdup_printf("SELECT COUNT(DISTINCT film_id), " "2, " //id always different "COUNT(DISTINCT group_id), " "COUNT(DISTINCT filename), " "COUNT(DISTINCT version), " "COUNT(DISTINCT film_id || '/' || filename), " //path "COUNT(DISTINCT flags & 2048), " //local copy "COUNT(DISTINCT import_timestamp), " "COUNT(DISTINCT change_timestamp), " "COUNT(DISTINCT export_timestamp), " "COUNT(DISTINCT print_timestamp), " "COUNT(DISTINCT flags), " "COUNT(DISTINCT model_id), " "COUNT(DISTINCT maker_id), " "COUNT(DISTINCT lens_id), " "COUNT(DISTINCT aperture), " "COUNT(DISTINCT exposure), " "COUNT(DISTINCT IFNULL(exposure_bias, '')), " "COUNT(DISTINCT IFNULL(exposure_program_id, '')), " "COUNT(DISTINCT IFNULL(whitebalance_id, '')), " "COUNT(DISTINCT IFNULL(flash_id, '')), " "COUNT(DISTINCT IFNULL(metering_mode_id, '')), " "COUNT(DISTINCT focal_length), " "COUNT(DISTINCT focal_length) + COUNT(DISTINCT crop), " "COUNT(DISTINCT crop), " "COUNT(DISTINCT focus_distance), " "COUNT(DISTINCT iso), " "COUNT(DISTINCT datetime_taken), " "COUNT(DISTINCT width), " "COUNT(DISTINCT height), " "COUNT(DISTINCT IFNULL(output_width, '')), " //exported width "COUNT(DISTINCT IFNULL(output_height, '')), " //exported height "COUNT(DISTINCT IFNULL(latitude, '')), " "COUNT(DISTINCT IFNULL(longitude, '')), " "COUNT(DISTINCT IFNULL(altitude, '')), " "2, " // placeholder for tags "2, " // placeholder for categories "%s " // metadata_subquery "FROM main.images " "WHERE id IN (%s)", metadata_subquery, images); // clang-format on g_free(metadata_subquery); DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL); g_free(query); if(sqlite3_step(stmt) == SQLITE_ROW) { for(int32_t md = 0; md < md_xmp_metadata + d->metadata_count; md++) { const int md_count = sqlite3_column_int(stmt, md); if(md == md_exif_focal_length_ff) skip[md] = md_count > 2; else skip[md] = md_count > 1; } } sqlite3_finalize(stmt); // Tags and categories management gboolean same_tags = TRUE; gboolean same_categories = TRUE; GList *tag_counts = dt_tag_get_flags_counts(images); for(GList *iter = tag_counts; iter; iter = g_list_next(iter)) { const dt_tag_flags_count_t *fc = iter->data; if(fc->flags & DT_TF_CATEGORY) { same_categories &= (fc->count == count); } else { same_tags &= (fc->count == count); } } g_list_free_full(tag_counts, g_free); skip[md_tag_names] = !same_tags; skip[md_categories] = !same_categories; } g_free(images); dt_imgid_t img_id = mouse_over_id; const dt_image_t *img = dt_image_cache_get(img_id, 'r'); if(!img) goto fill_minuses; if(img->film_id == -1) { dt_image_cache_read_release(img); goto fill_minuses; } // Update the metadata values for(int32_t md = 0; md < md_xmp_metadata + d->metadata_count; md++) { if(skip[md]) { if(md == md_internal_flags) { _metadata_update_tooltip(md, NULL, self); } if(md == md_internal_filmroll) { _metadata_update_tooltip(md, NULL, self); } _metadata_update_value(md, _(""), self); _metadata_update_markup(md, "%s", self); continue; } char text[PATH_MAX] = {0}; switch(md) { case md_internal_filmroll: { char tooltip_filmroll[300] = {0}; dt_image_film_roll(img, text, sizeof(text)); snprintf(tooltip_filmroll, sizeof(tooltip_filmroll), _("double-click to jump to film roll\n%s"), text); _metadata_update_tooltip(md_internal_filmroll, tooltip_filmroll, self); _metadata_update_value(md_internal_filmroll, text, self); } break; case md_internal_imgid: (void)g_snprintf(text, sizeof(text), "%d", img->id); _metadata_update_value(md_internal_imgid, text, self); break; case md_internal_groupid: (void)g_snprintf(text, sizeof(text), "%d", img->group_id); _metadata_update_value(md_internal_groupid, text, self); break; case md_internal_filename: _metadata_update_value(md_internal_filename, img->filename, self); break; case md_internal_version: (void)g_snprintf(text, sizeof(text), "%d", img->version); _metadata_update_value(md_internal_version, text, self); break; case md_internal_fullpath: { dt_image_full_path(img->id, text, sizeof(text), NULL); _metadata_update_value(md_internal_fullpath, text, self); } break; case md_internal_local_copy: (void)g_strlcpy(text, (img->flags & DT_IMAGE_LOCAL_COPY) ? _("yes") : _("no"), sizeof(text)); _metadata_update_value(md_internal_local_copy, text, self); break; case md_internal_import_timestamp: _metadata_update_timestamp(md_internal_import_timestamp, img->import_timestamp, self); break; case md_internal_change_timestamp: _metadata_update_timestamp(md_internal_change_timestamp, img->change_timestamp, self); break; case md_internal_export_timestamp: _metadata_update_timestamp(md_internal_export_timestamp, img->export_timestamp, self); break; case md_internal_print_timestamp: _metadata_update_timestamp(md_internal_print_timestamp, img->print_timestamp, self); break; case md_internal_flags: { char tooltip_flags[300] = {0}; _metadata_get_flags(img, text, tooltip_flags, sizeof(tooltip_flags)); _metadata_update_tooltip(md_internal_flags, tooltip_flags, self); _metadata_update_value(md_internal_flags, text, self); } break; case md_exif_model: _metadata_update_value(md_exif_model, img->camera_alias, self); break; case md_exif_maker: _metadata_update_value(md_exif_maker, img->camera_maker, self); break; case md_exif_lens: _metadata_update_value(md_exif_lens, img->exif_lens, self); break; case md_exif_aperture: (void)g_snprintf(text, sizeof(text), "f/%.1f", (double)img->exif_aperture); _metadata_update_value(md_exif_aperture, text, self); break; case md_exif_exposure: { gchar *const str = dt_util_format_exposure(img->exif_exposure); _metadata_update_value(md_exif_exposure, str, self); g_free(str); } break; case md_exif_exposure_bias: g_strlcpy(text, NODATA_STRING, sizeof(text)); if(img->exif_exposure_bias != DT_EXIF_TAG_UNINITIALIZED) { (void)g_snprintf(text, sizeof(text), _("%+.2f EV"), (double)img->exif_exposure_bias); } _metadata_update_value(md_exif_exposure_bias, text, self); break; case md_exif_exposure_program: _metadata_update_value(md_exif_exposure_program, img->exif_exposure_program, self); break; case md_exif_whitebalance: _metadata_update_value(md_exif_whitebalance, img->exif_whitebalance, self); break; case md_exif_flash: _metadata_update_value(md_exif_flash, img->exif_flash, self); break; case md_exif_metering_mode: _metadata_update_value(md_exif_metering_mode, img->exif_metering_mode, self); break; case md_exif_focal_length: (void)g_snprintf(text, sizeof(text), _("%.1f mm"), (double)img->exif_focal_length); _metadata_update_value(md_exif_focal_length, text, self); break; case md_exif_focal_length_ff: if(!(img->exif_crop)) { _metadata_update_value(md_exif_focal_length_ff, NODATA_STRING, self); break; } if(img->exif_crop != 1.0f) { (void)g_snprintf(text, sizeof(text), _("%.1f mm"), (double)img->exif_crop * img->exif_focal_length); } else { (void)g_snprintf(text, sizeof(text), _("%.1f mm"), (double)img->exif_focal_length); } _metadata_update_value(md_exif_focal_length_ff, text, self); break; case md_exif_crop_factor: if(img->exif_crop) { (void)g_snprintf(text, sizeof(text), _("%.1f"), (double)img->exif_crop); _metadata_update_value(md_exif_crop_factor, text, self); } else { _metadata_update_value(md_exif_crop_factor, NODATA_STRING, self); } break; case md_exif_focus_distance: (void)g_strlcpy(text, NODATA_STRING, sizeof(text)); // Actually we want to check for 0xFFFFFFFF (this value in the SubjectDistance tag means "infinity"). // But we store this tag as a float and there is a concern that the equality check may not be 100% reliable. // See discussion at https://github.com/darktable-org/darktable/pull/12398 if(img->exif_focus_distance >= (float)0xFFFFFF00) { (void)g_snprintf(text, sizeof(text), _("infinity")); } else if(!(dt_isnan(img->exif_focus_distance) || (fpclassify(img->exif_focus_distance) == FP_ZERO) )) { (void)g_snprintf(text, sizeof(text), _("%.2f m"), (double)img->exif_focus_distance); } _metadata_update_value(md_exif_focus_distance, text, self); break; case md_exif_iso: (void)g_snprintf(text, sizeof(text), "%.0f", (double)img->exif_iso); _metadata_update_value(md_exif_iso, text, self); break; case md_exif_datetime: { char datetime[200]; const gboolean milliseconds = dt_conf_get_bool("lighttable/ui/milliseconds"); const gboolean valid = dt_datetime_img_to_local(datetime, sizeof(datetime), img, milliseconds); _metadata_update_value(md_exif_datetime, valid ? datetime : NODATA_STRING, self); } break; case md_exif_width: if(img->p_width && (img->p_width != img->width)) { (void)g_snprintf(text, sizeof(text), "%d (%d)", img->p_width, img->width); _metadata_update_value(md_exif_width, text, self); } else { (void)g_snprintf(text, sizeof(text), "%d", img->width); _metadata_update_value(md_exif_width, text, self); } break; case md_exif_height: if(img->p_height && (img->p_height != img->height)) { (void)g_snprintf(text, sizeof(text), "%d (%d)", img->p_height, img->height); _metadata_update_value(md_exif_height, text, self); } else { (void)g_snprintf(text, sizeof(text), "%d", img->height); _metadata_update_value(md_exif_height, text, self); } break; case md_width: (void)g_strlcpy(text, NODATA_STRING, sizeof(text)); if(img->final_width > 0) { (void)g_snprintf(text, sizeof(text), "%d", img->final_width); } _metadata_update_value(md_width, text, self); break; case md_height: (void)g_strlcpy(text, NODATA_STRING, sizeof(text)); if(img->final_height > 0) { (void)g_snprintf(text, sizeof(text), "%d", img->final_height); } _metadata_update_value(md_height, text, self); break; case md_geotagging_lat: if(isnan(img->geoloc.latitude)) { _metadata_update_value(md_geotagging_lat, NODATA_STRING, self); } else { if(dt_conf_get_bool("plugins/lighttable/metadata_view/pretty_location")) { gchar *latitude = dt_util_latitude_str((float)img->geoloc.latitude); _metadata_update_value(md_geotagging_lat, latitude, self); g_free(latitude); } else { const gchar NS = img->geoloc.latitude < 0 ? 'S' : 'N'; (void)g_snprintf(text, sizeof(text), "%c %09.6f", NS, fabs(img->geoloc.latitude)); _metadata_update_value(md_geotagging_lat, text, self); } } break; case md_geotagging_lon: if(isnan(img->geoloc.longitude)) { _metadata_update_value(md_geotagging_lon, NODATA_STRING, self); } else { if(dt_conf_get_bool("plugins/lighttable/metadata_view/pretty_location")) { gchar *longitude = dt_util_longitude_str((float)img->geoloc.longitude); _metadata_update_value(md_geotagging_lon, longitude, self); g_free(longitude); } else { const gchar EW = img->geoloc.longitude < 0 ? 'W' : 'E'; (void)g_snprintf(text, sizeof(text), "%c %010.6f", EW, fabs(img->geoloc.longitude)); _metadata_update_value(md_geotagging_lon, text, self); } } break; case md_geotagging_ele: if(isnan(img->geoloc.elevation)) { _metadata_update_value(md_geotagging_ele, NODATA_STRING, self); } else { if(dt_conf_get_bool("plugins/lighttable/metadata_view/pretty_location")) { gchar *elevation = dt_util_elevation_str((float)img->geoloc.elevation); _metadata_update_value(md_geotagging_ele, elevation, self); g_free(elevation); } else { (void)g_snprintf(text, sizeof(text), "%.2f %s", img->geoloc.elevation, _("m")); _metadata_update_value(md_geotagging_ele, text, self); } } break; case md_tag_names: case md_categories: { GList *tags = NULL; char *tagstring = NULL; char *categoriesstring = NULL; if(dt_tag_get_attached(img->id, &tags, TRUE)) { uint64_t length = 0u; for(GList *taglist = tags; taglist; taglist = g_list_next(taglist)) { const char *tagname = ((dt_tag_t *)taglist->data)->leave; if(!(((dt_tag_t *)taglist->data)->flags & DT_TF_CATEGORY)) { // tags - just keywords length = length + strlen(tagname) + 2u; if(length < 45u) dt_util_str_cat(&tagstring, "%s, ", tagname); else { dt_util_str_cat(&tagstring, "\n%s, ", tagname); length = strlen(tagname) + 2u; } } else { // categories - needs parent category to make sense char *category = g_strdup(((dt_tag_t *)taglist->data)->tag); char *catend = g_strrstr(category, "|"); if(catend) { catend[0] = '\0'; char *catstart = g_strrstr(category, "|"); catstart = catstart ? catstart + 1 : category; dt_util_str_cat(&categoriesstring, categoriesstring ? "\n%s: %s " : "%s: %s ", catstart, ((dt_tag_t *)taglist->data)->leave); } else dt_util_str_cat(&categoriesstring, categoriesstring ? "\n%s" : "%s", ((dt_tag_t *)taglist->data)->leave); g_free(category); } } if(tagstring) tagstring[strlen(tagstring)-2] = '\0'; } if(md == md_tag_names) _metadata_update_value(md_tag_names, tagstring ? tagstring : NODATA_STRING, self); else _metadata_update_value(md_categories, categoriesstring ? categoriesstring : NODATA_STRING, self); g_free(tagstring); g_free(categoriesstring); dt_tag_free_result(&tags); } break; default: break; } //cases not handled by switch if(md >= md_xmp_metadata) { g_strlcpy(text, NODATA_STRING, sizeof(text)); if(md - md_xmp_metadata <= d->metadata_count) { uint32_t i = 0; dt_pthread_mutex_lock(&darktable.metadata_threadsafe); dt_metadata_t *metadata = NULL; for(GList *iter = dt_metadata_get_list(); iter; iter = iter->next) { dt_metadata_t *m = (dt_metadata_t *)iter->data; if(!m->internal) { if(i == md - md_xmp_metadata) { metadata = m; break; } i++; } } if(metadata) { GList *res = dt_metadata_get(img->id, metadata->tagname, NULL); if(res) { g_strlcpy(text, (char *)res->data, sizeof(text)); _filter_non_printable(text, sizeof(text)); g_list_free_full(res, &g_free); } } dt_pthread_mutex_unlock(&darktable.metadata_threadsafe); } _metadata_update_value(md, text, self); } } dt_image_cache_read_release(img); g_free(skip); if(dt_is_valid_imgid(mouse_over_id)) { #ifdef USE_LUA dt_lua_async_call_alien(lua_update_metadata, 0,NULL,NULL, LUA_ASYNC_TYPENAME,"void*",self, LUA_ASYNC_TYPENAME,"int32_t",mouse_over_id,LUA_ASYNC_DONE); #endif } return; /* reset */ fill_minuses: for(int k = 0; k < md_xmp_metadata + d->metadata_count; k++) _metadata_update_value(k, NODATA_STRING, self); #ifdef USE_LUA dt_lua_async_call_alien(lua_update_metadata, 0,NULL,NULL, LUA_ASYNC_TYPENAME,"void*",self, LUA_ASYNC_TYPENAME,"int32_t",-1,LUA_ASYNC_DONE); #endif } static void _jump_to() { dt_imgid_t imgid = dt_control_get_mouse_over_id(); if(!dt_is_valid_imgid(imgid)) { sqlite3_stmt *stmt; DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), "SELECT imgid FROM main.selected_images", -1, &stmt, NULL); if(sqlite3_step(stmt) == SQLITE_ROW) imgid = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); } if(dt_is_valid_imgid(imgid)) { char path[512]; const dt_image_t *img = dt_image_cache_get(imgid, 'r'); dt_image_film_roll_directory(img, path, sizeof(path)); dt_image_cache_read_release(img); char collect[1024]; snprintf(collect, sizeof(collect), "1:0:0:%s$", path); dt_collection_deserialize(collect, FALSE); } } static void _filmroll_clicked(GtkGestureSingle *gesture, gint n_press, gdouble x, gdouble y, gpointer null) { if(n_press < 2) return; _jump_to(); } static void _jump_to_accel(dt_action_t *data) { _jump_to(); } /* callback for the mouse over image change signal */ static void _mouse_over_image_callback(gpointer instance, dt_lib_module_t *self) { if(dt_control_running()) dt_lib_gui_queue_update(self); } static char *_get_current_configuration(dt_lib_module_t *self) { dt_lib_metadata_view_t *d = self->data; char *pref = NULL; d->metadata = g_list_sort(d->metadata, _lib_metadata_sort_order); for(GList *meta = d->metadata; meta; meta= g_list_next(meta)) { const dt_lib_metadata_info_t *m = meta->data; dt_util_str_cat(&pref, "%s%s,", m->visible ? "" : "|", m->setting); } if(pref) { pref[strlen(pref) - 1] = '\0'; } return pref; } static void _lib_metadata_refill_grid(dt_lib_module_t *self) { dt_lib_metadata_view_t *d = self->data; d->metadata = g_list_sort(d->metadata, _lib_metadata_sort_order); int j = 0; // initialize the grid with metadata queue content for(GList *meta = d->metadata; meta; meta = g_list_next(meta)) { dt_lib_metadata_info_t *m = meta->data; m->order = j; GtkWidget *w_name = gtk_grid_get_child_at(GTK_GRID(d->grid), 0, j); gtk_label_set_text(GTK_LABEL(w_name), _(m->name)); gtk_widget_set_tooltip_text(w_name, _(m->name)); GtkWidget *w_value = gtk_grid_get_child_at(GTK_GRID(d->grid), 1, j); gtk_label_set_text(GTK_LABEL(w_value), m->value); const char *tooltip = m->tooltip ? m->tooltip : m->value; gtk_widget_set_tooltip_text(w_value, tooltip); const int i = m->index; gtk_label_set_ellipsize(GTK_LABEL(w_value), i == md_exif_model || i == md_exif_lens || i == md_exif_maker ? PANGO_ELLIPSIZE_END : PANGO_ELLIPSIZE_MIDDLE); if(i == md_internal_filmroll) { // film roll jump to: if(d->filmroll_event && GTK_IS_WIDGET(d->filmroll_event)) g_signal_handlers_disconnect_by_func(d->filmroll_event, G_CALLBACK(_filmroll_clicked), NULL); dt_gui_connect_click(w_value, _filmroll_clicked, NULL, NULL); d->filmroll_event = G_OBJECT(w_value); } gtk_widget_set_visible(w_name, m->visible); gtk_widget_set_visible(w_value, m->visible); j++; } } static void _add_grid_row(dt_lib_metadata_info_t *m, const int row, dt_lib_module_t *self) { dt_lib_metadata_view_t *d = self->data; GtkWidget *w_name = gtk_label_new(_(m->name)); gtk_widget_set_halign(w_name, GTK_ALIGN_START); gtk_label_set_xalign(GTK_LABEL(w_name), 0.0f); gtk_label_set_ellipsize(GTK_LABEL(w_name), PANGO_ELLIPSIZE_END); gtk_widget_set_tooltip_text(w_name, _(m->name)); GtkWidget *w_value= gtk_label_new(m->value); gtk_widget_set_name(w_value, "brightbg"); gtk_label_set_selectable(GTK_LABEL(w_value), TRUE); gtk_widget_set_halign(w_value, GTK_ALIGN_FILL); gtk_label_set_xalign(GTK_LABEL(w_value), 0.0f); gtk_grid_insert_row(GTK_GRID(d->grid), row); gtk_grid_attach(GTK_GRID(d->grid), w_name, 0, row, 1, 1); gtk_grid_attach(GTK_GRID(d->grid), w_value, 1, row, 1, 1); } static void _lib_metadata_setup_grid(dt_lib_module_t *self) { dt_lib_metadata_view_t *d = self->data; int j = 0; // initialize the grid with metadata queue content for(GList *meta = d->metadata; meta; meta = g_list_next(meta)) { _add_grid_row(meta->data, j, self); j++; } } static void _free_metadata_queue(dt_lib_metadata_info_t *m) { g_free(m->name); if(m->value) g_free(m->value); if(m->tooltip) g_free(m->tooltip); g_free(m->setting); g_free(m); } static void _metadata_changed(gpointer instance, const int type, dt_lib_module_t *self) { dt_lib_metadata_view_t *d = self->data; if(type != DT_METADATA_SIGNAL_PREF_CHANGED) return; gboolean needs_update = FALSE; uint32_t order = 0; GList *new_metadata_keys = NULL; dt_pthread_mutex_lock(&darktable.metadata_threadsafe); for(GList *iter = dt_metadata_get_list(); iter; iter = iter->next) { const dt_metadata_t *metadata = (dt_metadata_t *)iter->data; gboolean found = FALSE; if(!metadata->internal) { for(GList *md_iter = d->metadata; md_iter; md_iter = md_iter->next) { dt_lib_metadata_info_t *m = md_iter->data; if(m->key == metadata->key) { found = TRUE; if(g_strcmp0(m->name, metadata->name)) { // display name changed g_free(m->name); m->name = g_strdup(metadata->name); needs_update = TRUE; break; } } } if(!found) // new metadata, store the new key new_metadata_keys = g_list_prepend(new_metadata_keys, GINT_TO_POINTER(metadata->key)); } } if(new_metadata_keys) { // new metadata entries found d->metadata = g_list_sort(d->metadata, _lib_metadata_sort_index); d->metadata = g_list_reverse(d->metadata); dt_lib_metadata_info_t *last = d->metadata->data; for(GList *iter = new_metadata_keys; iter; iter = iter->next) { const uint32_t key = GPOINTER_TO_INT(iter->data); const dt_metadata_t *metadata = dt_metadata_get_metadata_by_keyid(key); dt_lib_metadata_info_t *m = calloc(1, sizeof(dt_lib_metadata_info_t)); if(m) { m->name = g_strdup(metadata->name); m->value = g_strdup(NODATA_STRING); m->index = m->order = last->index + 1; m->visible = TRUE; m->setting = g_strdup(dt_metadata_get_tag_subkey(metadata->tagname)); m->key = metadata->key; d->metadata = g_list_prepend(d->metadata, m); _add_grid_row(m, 0, self); d->metadata_count++; needs_update = TRUE; } } g_list_free(new_metadata_keys); } // check for deleted metadata order = 0; GList *md_iter = d->metadata; while(md_iter) { GList *next = md_iter->next; dt_lib_metadata_info_t *m = md_iter->data; m->order = order++; if(m->key != -1) { const dt_metadata_t *metadata = dt_metadata_get_metadata_by_keyid(m->key); if(!metadata) { gtk_grid_remove_row(GTK_GRID(d->grid), m->order); d->metadata = g_list_remove_link(d->metadata, md_iter); _free_metadata_queue(m); order--; needs_update = TRUE; } } md_iter = next; } dt_pthread_mutex_unlock(&darktable.metadata_threadsafe); if(needs_update) _lib_metadata_refill_grid(self); } static void _apply_preferences(const char *prefs_list, dt_lib_module_t *self) { if(!prefs_list || !prefs_list[0]) return; dt_lib_metadata_view_t *d = self->data; GList *prefs = dt_util_str_to_glist(",", prefs_list); int k = 0; for(GList *pref = prefs; pref; pref = g_list_next(pref)) { const char *name = (char *)pref->data; gboolean visible = TRUE; if(name) { if(name[0] == '|') { name++; visible = FALSE; } for(GList *meta = d->metadata; meta; meta= g_list_next(meta)) { dt_lib_metadata_info_t *m = meta->data; if(name && !g_strcmp0(name, m->setting)) { m->order = k; m->visible = visible; break; } } } else continue; k++; } g_list_free_full(prefs, g_free); _lib_metadata_refill_grid(self); } static void _save_preferences(dt_lib_module_t *self) { char *pref = _get_current_configuration(self); dt_conf_set_string("plugins/lighttable/metadata_view/visible", pref); g_free(pref); } static void _select_toggled_callback(GtkCellRendererToggle *cell_renderer, gchar *path_str, gpointer user_data) { GtkListStore *store = (GtkListStore *)user_data; GtkTreeIter iter; GtkTreePath *path = gtk_tree_path_new_from_string(path_str); gboolean selected; gtk_tree_model_get_iter(GTK_TREE_MODEL(store), &iter, path); gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DT_METADATA_PREF_COL_VISIBLE, &selected, -1); gtk_list_store_set(store, &iter, DT_METADATA_PREF_COL_VISIBLE, !selected, -1); gtk_tree_path_free(path); } static void _menuitem_preferences(GSimpleAction *action, GVariant *parameter, gpointer user_data) { dt_lib_module_t *self = (dt_lib_module_t *)user_data; dt_lib_metadata_view_t *d = self->data; GtkWidget *win = dt_ui_main_window(darktable.gui->ui); GtkWidget *dialog = gtk_dialog_new_with_buttons(_("metadata settings"), GTK_WINDOW(win), GTK_DIALOG_DESTROY_WITH_PARENT, _("_default"), GTK_RESPONSE_YES, _("_cancel"), GTK_RESPONSE_NONE, _("_save"), GTK_RESPONSE_ACCEPT, NULL); gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); dt_gui_connect_key(dialog, dt_handle_dialog_enter, NULL); GtkListStore *store = gtk_list_store_new(DT_METADATA_PREF_NUM_COLS, G_TYPE_INT, G_TYPE_STRING, G_TYPE_BOOLEAN); GtkTreeModel *model = GTK_TREE_MODEL(store); GtkTreeIter iter; d->metadata = g_list_sort(d->metadata, _lib_metadata_sort_order); for(GList *meta = d->metadata; meta; meta = g_list_next(meta)) { dt_lib_metadata_info_t *m = meta->data; gtk_list_store_insert_with_values(store, NULL, -1, DT_METADATA_PREF_COL_INDEX, m->index, DT_METADATA_PREF_COL_NAME_L, _(m->name), DT_METADATA_PREF_COL_VISIBLE, m->visible, -1); } GtkWidget *view = gtk_tree_view_new_with_model(model); g_object_unref(model); GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes(_("metadata"), renderer, "text", DT_METADATA_PREF_COL_NAME_L, NULL); gtk_tree_view_column_set_expand(column, TRUE); gtk_tree_view_append_column(GTK_TREE_VIEW(view), column); GtkWidget *header = gtk_tree_view_column_get_button(column); gtk_widget_set_tooltip_text(header, _("drag and drop one row at a time until you get the desired order" "\nuntick to hide metadata which are not of interest for you" "\nif different settings are needed, use presets")); renderer = gtk_cell_renderer_toggle_new(); g_signal_connect(renderer, "toggled", G_CALLBACK(_select_toggled_callback), store); column = gtk_tree_view_column_new_with_attributes(_("visible"), renderer, "active", DT_METADATA_PREF_COL_VISIBLE, NULL); gtk_tree_view_append_column(GTK_TREE_VIEW(view), column); GtkTreePath *first = gtk_tree_path_new_first (); gtk_tree_view_set_cursor(GTK_TREE_VIEW(view), first, column, FALSE); gtk_tree_path_free(first); // drag & drop gtk_tree_view_set_reorderable(GTK_TREE_VIEW(view), TRUE); GtkWidget *w = dt_gui_scroll_wrap(view); gtk_widget_set_size_request(w, -1, DT_PIXEL_APPLY_DPI(600)); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS); gtk_scrolled_window_set_overlay_scrolling(GTK_SCROLLED_WINDOW(w), FALSE); dt_gui_dialog_add(GTK_DIALOG(dialog), w); #ifdef GDK_WINDOWING_QUARTZ dt_osx_disallow_fullscreen(dialog); #endif gtk_widget_show_all(dialog); int res = gtk_dialog_run(GTK_DIALOG(dialog)); while(res == GTK_RESPONSE_YES) { gtk_tree_model_get_iter_first(model, &iter); d->metadata = g_list_sort(d->metadata, _lib_metadata_sort_index); for(GList *meta = d->metadata; meta; meta= g_list_next(meta)) { dt_lib_metadata_info_t *m = meta->data; gtk_list_store_set(store, &iter, DT_METADATA_PREF_COL_INDEX, m->index, DT_METADATA_PREF_COL_NAME_L, _(m->name), DT_METADATA_PREF_COL_VISIBLE, TRUE, -1); gtk_tree_model_iter_next(model, &iter); } res = gtk_dialog_run(GTK_DIALOG(dialog)); } int i = 0; if(res == GTK_RESPONSE_ACCEPT) { gboolean valid = gtk_tree_model_get_iter_first(model, &iter); while(valid) { gboolean visible; uint32_t index; gtk_tree_model_get(model, &iter, DT_METADATA_PREF_COL_INDEX, &index, DT_METADATA_PREF_COL_VISIBLE, &visible, -1); for(GList *meta = d->metadata; meta; meta= g_list_next(meta)) { dt_lib_metadata_info_t *m = meta->data; if(m->index == index) { m->order = i; m->visible = visible; break; } } i++; valid = gtk_tree_model_iter_next(model, &iter); } _lib_metadata_refill_grid(self); _save_preferences(self); } gtk_widget_destroy(dialog); } void set_preferences(GMenu *menu, GActionGroup *action_group, dt_lib_module_t *self) { GSimpleAction *action = g_simple_action_new("preferences", NULL); g_signal_connect(action, "activate", G_CALLBACK(_menuitem_preferences), self); g_action_map_add_action(G_ACTION_MAP(action_group), G_ACTION(action)); g_menu_append(menu, _("preferences..."), "presets.preferences"); } void *get_params(dt_lib_module_t *self, int *size) { *size = 0; char *params = _get_current_configuration(self); if(params) *size =strlen(params) + 1; return params; } int set_params(dt_lib_module_t *self, const void *params, const int size) { if(!params) return 1; _apply_preferences(params, self); _save_preferences(self); return 0; } static void _display_default(dt_lib_module_t *self) { dt_lib_metadata_view_t *d = self->data; for(GList *meta = d->metadata; meta; meta= g_list_next(meta)) { dt_lib_metadata_info_t *m = meta->data; m->order = m->index; m->visible = TRUE; } _lib_metadata_refill_grid(self); } void gui_init(dt_lib_module_t *self) { /* initialize ui */ dt_lib_metadata_view_t *d = g_malloc0(sizeof(dt_lib_metadata_view_t)); self->data = (void *)d; _lib_metadata_init_queue(self); d->grid = gtk_grid_new(); gtk_grid_set_column_spacing(GTK_GRID(d->grid), DT_PIXEL_APPLY_DPI(5)); self->widget = dt_gui_vbox (dt_ui_resize_wrap(d->grid, 200, "plugins/lighttable/metadata_view/windowheight")); gtk_widget_show_all(d->grid); gtk_widget_set_no_show_all(d->grid, TRUE); _lib_metadata_setup_grid(self); const char *pref = dt_conf_get_string_const("plugins/lighttable/metadata_view/visible"); if(!strlen(pref)) _display_default(self); _apply_preferences(pref, self); /* lets signup for mouse over image change signals */ DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_MOUSE_OVER_IMAGE_CHANGE, _mouse_over_image_callback); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_SELECTION_CHANGED, _mouse_over_image_callback); /* lets signup for develop image changed signals */ DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_IMAGE_CHANGED, _mouse_over_image_callback); /* signup for develop initialize to update info of current image in darkroom when enter */ DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_INITIALIZE, _mouse_over_image_callback); /* signup for tags changes */ DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_TAG_CHANGED, _mouse_over_image_callback); /* signup for metadata changes */ DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_METADATA_UPDATE, _mouse_over_image_callback); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_METADATA_CHANGED, _metadata_changed); dt_action_register(DT_ACTION(self), N_("jump to film roll"), _jump_to_accel, GDK_KEY_j, GDK_CONTROL_MASK); } void gui_cleanup(dt_lib_module_t *self) { dt_lib_metadata_view_t *d = self->data; g_list_free_full(d->metadata, (GDestroyNotify)_free_metadata_queue); g_free(self->data); self->data = NULL; } void gui_reset(dt_lib_module_t *self) { dt_lib_metadata_view_t *d = self->data; for(GList *meta = d->metadata; meta; meta= g_list_next(meta)) { dt_lib_metadata_info_t *m = meta->data; m->order = m->index; m->visible = TRUE; } _lib_metadata_refill_grid(self); _save_preferences(self); } #ifdef USE_LUA static int lua_update_values(lua_State *L) { dt_lib_module_t *self = lua_touserdata(L, 1); dt_lua_module_entry_push(L, "lib", self->plugin_name); lua_getiuservalue(L, 2, 1); lua_getfield(L, 3, "values"); lua_getfield(L, 3, "indexes"); lua_pushnil(L); while(lua_next(L, 4) != 0) { lua_getfield(L, 5, lua_tostring(L,-2)); int index = lua_tointeger(L, -1); _metadata_update_value(index, luaL_checkstring(L, 7), self); lua_pop(L, 2); } return 0; } static int lua_update_metadata(lua_State *L) { dt_lib_module_t *self = lua_touserdata(L, 1); const dt_imgid_t imgid = lua_tointeger(L, 2); gboolean have_updates = false; dt_lua_module_entry_push(L, "lib", self->plugin_name); lua_getiuservalue(L, -1, 1); lua_getfield(L, 4, "callbacks"); lua_getfield(L, 4, "values"); lua_pushnil(L); while(lua_next(L, 5) != 0) { have_updates = true; if(dt_is_valid_imgid(imgid)) { lua_pushvalue(L, -1); luaA_push(L, dt_lua_image_t, &imgid); lua_call(L, 1, 1); } else { lua_pushstring(L, "-"); } lua_pushvalue(L, 7); lua_pushvalue(L, 9); lua_settable(L, 6); lua_pop(L, 2); } if(have_updates) { lua_pushcfunction(L, lua_update_values); dt_lua_gtk_wrap(L); lua_pushlightuserdata(L, self); lua_call(L, 1, 0); } return 0; } static int lua_register_info(lua_State *L) { dt_lib_module_t *self = lua_touserdata(L, lua_upvalueindex(1)); dt_lua_module_entry_push(L, "lib", self->plugin_name); lua_getiuservalue(L, -1, 1); const char* key = luaL_checkstring(L, 1); luaL_checktype(L, 2, LUA_TFUNCTION); { lua_getfield(L, -1, "callbacks"); lua_pushstring(L, key); lua_pushvalue(L, 2); lua_settable(L, 5); lua_pop(L, 1); } { lua_getfield(L, -1, "values"); lua_pushstring(L, key); lua_pushstring(L, NODATA_STRING); lua_settable(L, 5); lua_pop(L, 1); } { dt_lib_metadata_view_t *d = self->data; dt_lib_metadata_info_t *m = g_malloc0(sizeof(dt_lib_metadata_info_t)); m->name = g_strdup((char *)key); m->value = g_strdup(NODATA_STRING); m->key = -1; m->setting = g_strdup((char *)key); const int index = g_list_length(d->metadata); m->index = m->order = index; m->visible = TRUE; d->metadata = g_list_append(d->metadata, m); _add_grid_row(m, 0, self); { lua_getfield(L, -1, "indexes"); lua_pushstring(L, key); lua_pushinteger(L, index); lua_settable(L, 5); lua_pop(L, 1); } // apply again preferences because it's already done const char *pref = dt_conf_get_string_const("plugins/lighttable/metadata_view/visible"); _apply_preferences(pref, self); } return 0; } static int lua_destroy_info(lua_State *L) { dt_lib_module_t *self = lua_touserdata(L, lua_upvalueindex(1)); dt_lua_module_entry_push(L, "lib", self->plugin_name); lua_getiuservalue(L, -1, 1); const char* key = luaL_checkstring(L, 1); { lua_getfield(L, -1, "callbacks"); lua_pushstring(L, key); lua_pushnil(L); lua_settable(L, 4); lua_pop(L, 1); } { lua_getfield(L, -1, "values"); lua_pushstring(L, key); lua_pushnil(L); lua_settable(L, 4); lua_pop(L, 1); } lua_getfield(L, -1, "indexes"); lua_getfield(L, -1, key); const int index = lua_tointeger(L, -1); lua_pop(L, 1); { lua_pushstring(L, key); lua_pushnil(L); lua_settable(L, 4); } // decrement all indexes > index lua_pushnil(L); while(lua_next(L, -2) != 0) { int i = lua_tointeger(L, -1); if(i > index) { lua_pop(L, 1); lua_pushvalue(L, -1); i--; lua_pushinteger(L, i); lua_settable(L, -4); } else lua_pop(L, 1); } { dt_lib_metadata_view_t *d = self->data; // find metadata key in the list and remove it GList *tbr = NULL; for(GList *meta = d->metadata; meta; meta = g_list_next(meta)) { dt_lib_metadata_info_t *m = meta->data; if(!g_strcmp0(key, m->name)) { tbr = meta; } else if(m->index > index) { m->index--; } } if(tbr) { dt_lib_metadata_info_t *m = tbr->data; d->metadata = g_list_remove_link(d->metadata, tbr); g_free(m->name); g_free(m->value); g_free(m->setting); if(m->tooltip) g_free(m->tooltip); g_free(m); g_list_free(tbr); gtk_grid_remove_row(GTK_GRID(d->grid), 0); _lib_metadata_refill_grid(self); } } return 0; } void init(struct dt_lib_module_t *self) { lua_State *L = darktable.lua_state.state; const int my_type = dt_lua_module_entry_get_type(L, "lib", self->plugin_name); lua_pushlightuserdata(L, self); lua_pushcclosure(L, lua_register_info, 1); dt_lua_gtk_wrap(L); lua_pushcclosure(L, dt_lua_type_member_common, 1); dt_lua_type_register_const_type(L, my_type, "register_info"); lua_pushlightuserdata(L, self); lua_pushcclosure(L, lua_destroy_info, 1); dt_lua_gtk_wrap(L); lua_pushcclosure(L, dt_lua_type_member_common, 1); dt_lua_type_register_const_type(L, my_type, "destroy_info"); dt_lua_module_entry_push(L,"lib",self->plugin_name); lua_getiuservalue(L, -1, 1); lua_newtable(L); lua_setfield(L, -2, "callbacks"); lua_newtable(L); lua_setfield(L, -2, "values"); lua_newtable(L); lua_setfield(L, -2, "indexes"); lua_pop(L, 2); } #endif // clang-format off // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py // vim: shiftwidth=2 expandtab tabstop=2 cindent // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified; // clang-format on