Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
doc_screenshot.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel.
3 Copyright (C) 2026 Guillaume Stutin.
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
17*/
18
20
21#include "common/conf.h" // dt_conf_get_string(), dt_conf_set_string()
22#include "common/l10n.h" // dt_l10n_get_global(), dt_l10n_get_current_code()
23#include "gui/application.h" // dt_gui_get_ui(), dt_gui_main_window()
24#include "gui/window_manager.h" // dt_ui_t, dt_ui_center_base(), DT_UI_PANEL_*
25#include "system/macros.h" // IS_NULL_PTR
26#include "system/mem_alloc.h" // dt_free
27#include "widgets/bauhaus.h" // DT_IS_BAUHAUS_WIDGET(), dt_bauhaus_widget_get_label()
28
29#include <glib/gi18n.h>
30#include <glib/gstdio.h>
31#include <gtk/gtk.h>
32
37#define DOC_SCREENSHOT_FILENAME_ALLOWED \
38 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_."
39
41#define DOC_SCREENSHOT_PREVIEW_SIZE 360
42
44#define DOC_SCREENSHOT_MAP_FILE "screenshots.map"
45
63#define DOC_SCREENSHOT_SELECTION_CONF_KEY "doc_screenshot/selection"
64#define DOC_SCREENSHOT_SEPARATOR "\x1f"
65
73
100
103static struct
104{
107 GtkTreeStore *store;
112 GHashTable *names;
113 GHashTable *pages;
114 GHashTable *selected;
115 gchar *directory;
116 gboolean enabled;
117} _g = { 0 };
118
121{
122 const char *folder;
123 const char *code;
124 GHashTable *used;
125 int saved;
129
130
132{
133 _g.enabled = TRUE;
134}
135
136
138{
139 return _g.enabled;
140}
141
142
144{
145 dt_free(_g.directory);
146 _g.directory = g_strdup(path);
147}
148
149
151static void _load_selection(void)
152{
154 gchar **labels
155 = g_strsplit(!IS_NULL_PTR(conf) && conf[0] ? conf : "", DOC_SCREENSHOT_SEPARATOR, -1);
156 dt_free(conf);
157
158 for(gchar **label = labels; *label; label++)
159 if((*label)[0] != '\0') g_hash_table_add(_g.selected, g_strdup(*label));
160
161 g_strfreev(labels);
162}
163
164
167static void _save_selection(void)
168{
169 // Sorted: a hash table enumerates in no defined order, and an unsorted value would rewrite
170 // the conf line on every run for a selection that never changed.
171 GList *labels = g_list_sort(g_hash_table_get_keys(_g.selected), (GCompareFunc)g_strcmp0);
172 GString *value = g_string_new(NULL);
173 for(GList *item = labels; item; item = g_list_next(item))
174 {
175 if(value->len) g_string_append(value, DOC_SCREENSHOT_SEPARATOR);
176 g_string_append(value, (const char *)item->data);
177 }
178 g_list_free(labels);
179
181 g_string_free(value, TRUE);
182}
183
184
194static int _write_surface(cairo_surface_t *surface, const char *filename)
195{
196 const char *extension = strrchr(filename, '.');
197 if(IS_NULL_PTR(extension) || !g_ascii_strcasecmp(extension, ".png"))
198 return (cairo_surface_write_to_png(surface, filename) == CAIRO_STATUS_SUCCESS) ? 0 : 2;
199
200 GdkPixbuf *pixbuf = gdk_pixbuf_get_from_surface(surface, 0, 0, cairo_image_surface_get_width(surface),
201 cairo_image_surface_get_height(surface));
202 // gdk-pixbuf spells it "jpeg" where the documentation tree spells it ".jpg". JPEG drops
203 // the alpha channel, which costs nothing here: the capture is painted over an opaque
204 // background before anything else is drawn.
205 const gboolean is_jpeg = !g_ascii_strcasecmp(extension, ".jpg") || !g_ascii_strcasecmp(extension, ".jpeg");
206 const gboolean written = is_jpeg
207 ? gdk_pixbuf_save(pixbuf, filename, "jpeg", NULL, "quality", "95", NULL)
208 : gdk_pixbuf_save(pixbuf, filename, extension + 1, NULL, NULL);
209 g_object_unref(pixbuf);
210 return written ? 0 : 2;
211}
212
213
238static cairo_surface_t *_render_area(GtkWidget *widget, const GdkRectangle *area)
239{
240 // GTK reports the allocation in logical pixels: carry the monitor's integer scale factor
241 // so the capture stays as sharp as the on-screen rendering on a HiDPI screen.
242 const int scale = gtk_widget_get_scale_factor(widget);
243 cairo_surface_t *surface
244 = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, area->width * scale, area->height * scale);
245 cairo_t *cr = cairo_create(surface);
246 cairo_scale(cr, (double)scale, (double)scale);
247
248 gtk_render_background(gtk_widget_get_style_context(gtk_widget_get_toplevel(widget)), cr, 0., 0.,
249 (double)area->width, (double)area->height);
250 // The whole widget is drawn and the surface keeps only the area: a tree view paints its
251 // rows from its own draw handler, and there is no drawing one of them on its own.
252 cairo_translate(cr, (double)-area->x, (double)-area->y);
253 gtk_widget_draw(widget, cr);
254 cairo_destroy(cr);
255
256 return surface;
257}
258
259
269static gboolean _tree_row_area(GtkTreeView *view, GtkTreeRowReference *row, GdkRectangle *area)
270{
271 if(!gtk_widget_get_mapped(GTK_WIDGET(view)) || !gtk_tree_row_reference_valid(row)
272 || gtk_tree_row_reference_get_model(row) != gtk_tree_view_get_model(view))
273 return FALSE;
274
275 // A NULL column leaves x and width at 0, and a collapsed ancestor leaves the height at 0:
276 // the item is then not part of what the view lays out at all.
277 GtkTreePath *path = gtk_tree_row_reference_get_path(row);
278 GdkRectangle cell;
279 gtk_tree_view_get_background_area(view, path, NULL, &cell);
280 gtk_tree_path_free(path);
281 if(cell.height < 1) return FALSE;
282
283 GdkRectangle visible;
284 gtk_tree_view_get_visible_rect(view, &visible);
285 int unused = 0;
286 int tree_y = 0;
287 gtk_tree_view_convert_bin_window_to_tree_coords(view, 0, cell.y, &unused, &tree_y);
288 if(tree_y < visible.y || tree_y + cell.height > visible.y + visible.height) return FALSE;
289
290 // Widget coordinates are what gtk_widget_draw() paints in: below the column headers.
291 int widget_y = 0;
292 gtk_tree_view_convert_bin_window_to_widget_coords(view, 0, cell.y, &unused, &widget_y);
293 *area = (GdkRectangle){ .x = 0,
294 .y = widget_y,
295 .width = gtk_widget_get_allocated_width(GTK_WIDGET(view)),
296 .height = cell.height };
297 return area->width > 0;
298}
299
300
309static cairo_surface_t *_render_target(GtkWidget *target, GtkTreeRowReference *row)
310{
311 GdkRectangle area = { 0 };
312 if(!IS_NULL_PTR(row))
313 {
314 if(!_tree_row_area(GTK_TREE_VIEW(target), row, &area)) return NULL;
315 }
316 else
317 {
318 gtk_widget_get_allocation(target, &area);
319 area.x = area.y = 0;
320 if(!gtk_widget_get_mapped(target) || area.width < 1 || area.height < 1) return NULL;
321 }
322
323 return _render_area(target, &area);
324}
325
326
331static int _save_target_as_image(GtkWidget *target, GtkTreeRowReference *row, const char *filename)
332{
333 cairo_surface_t *surface = _render_target(target, row);
334 if(IS_NULL_PTR(surface)) return 1;
335
336 // A mapped path points into the documentation tree, whose folders need not exist yet.
337 gchar *directory = g_path_get_dirname(filename);
338 g_mkdir_with_parents(directory, 0755);
340
341 const int result = _write_surface(surface, filename);
342 cairo_surface_destroy(surface);
343 return result;
344}
345
346
360static gboolean _widget_has_text(GtkWidget *widget)
361{
362 // Widgets that own text, plus the custom drawing surfaces that paint their own and say
363 // nothing about it through any API -- bauhaus controls among them, whose numeric value
364 // carries a localised decimal separator even when their label is empty -- and the cell
365 // views, which paint their text from renderers and own no label to find. An item of a tree
366 // view has that view as its target, so it is covered by the same test.
367 if(DT_IS_BAUHAUS_WIDGET(widget) || GTK_IS_ENTRY(widget) || GTK_IS_TEXT_VIEW(widget)
368 || GTK_IS_DRAWING_AREA(widget) || GTK_IS_TREE_VIEW(widget) || GTK_IS_ICON_VIEW(widget)
369 || GTK_IS_CELL_VIEW(widget))
370 return TRUE;
371
372 if(GTK_IS_LABEL(widget)) return gtk_label_get_text(GTK_LABEL(widget))[0] != '\0';
373
374 if(!GTK_IS_CONTAINER(widget)) return FALSE;
375
376 GList *children = gtk_container_get_children(GTK_CONTAINER(widget));
377 gboolean text = FALSE;
378 for(GList *item = children; item && !text; item = g_list_next(item))
379 text = _widget_has_text(GTK_WIDGET(item->data));
380 g_list_free(children);
381
382 return text;
383}
384
385
389static gchar *_localized_path(const char *relative, const char *code)
390{
391 const char *base = strrchr(relative, '/');
392 const char *extension = strrchr(IS_NULL_PTR(base) ? relative : base, '.');
393 if(IS_NULL_PTR(extension)) return g_strdup_printf("%s.%s", relative, code);
394
395 return g_strdup_printf("%.*s.%s%s", (int)(extension - relative), relative, code, extension);
396}
397
398
412static void _load_pages(void)
413{
414 g_hash_table_remove_all(_g.pages);
415
416 gchar *folder = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(_g.folder));
417 if(IS_NULL_PTR(folder)) return;
418
419 gchar *file = g_build_filename(folder, DOC_SCREENSHOT_MAP_FILE, NULL);
420 gchar *contents = NULL;
421 const gboolean loaded = g_file_get_contents(file, &contents, NULL, NULL);
422 dt_free(file);
424 if(!loaded) return;
425
426 gchar **lines = g_strsplit(contents, "\n", -1);
427 for(int line = 0; lines[line]; line++)
428 {
429 gchar *entry = g_strstrip(lines[line]);
430 gchar *separator = strchr(entry, '=');
431 if(entry[0] == '#' || IS_NULL_PTR(separator)) continue;
432
433 *separator = '\0';
434 // The table owns both halves.
435 g_hash_table_insert(_g.pages, g_strdup(g_strstrip(entry)), g_strdup(g_strstrip(separator + 1)));
436 }
437 g_strfreev(lines);
438 dt_free(contents);
439}
440
441
448static const char *_first_text(GtkWidget *widget)
449{
450 const char *own = GTK_IS_LABEL(widget) ? gtk_label_get_text(GTK_LABEL(widget))
452 : GTK_IS_BUTTON(widget) ? gtk_button_get_label(GTK_BUTTON(widget))
453 : NULL;
454 if(!IS_NULL_PTR(own) && own[0] != '\0') return own;
455 if(!GTK_IS_CONTAINER(widget)) return NULL;
456
457 GList *children = gtk_container_get_children(GTK_CONTAINER(widget));
458 const char *text = NULL;
459 for(GList *item = children; item && IS_NULL_PTR(text); item = g_list_next(item))
460 text = _first_text(GTK_WIDGET(item->data));
461 g_list_free(children);
462
463 return text;
464}
465
466
482{
483 while(GTK_IS_CONTAINER(widget))
484 {
485 GList *children = gtk_container_get_children(GTK_CONTAINER(widget));
486 GtkWidget *only = (!IS_NULL_PTR(children) && IS_NULL_PTR(g_list_next(children)))
487 ? GTK_WIDGET(children->data)
488 : NULL;
489 g_list_free(children);
490 if(IS_NULL_PTR(only)) break;
491
492 // `only`'s row is the one being dropped, so `only` is what has to be worth dropping:
493 // pure layout, with no identity of its own. A GtkButton also holds a single child, and
494 // folding it away would delete the very row someone wants to capture.
495 const gboolean anonymous = IS_NULL_PTR(g_hash_table_lookup(_g.names, only))
496 && !g_strcmp0(gtk_widget_get_name(only), G_OBJECT_TYPE_NAME(only));
497 const gboolean layout = GTK_IS_BOX(only) || GTK_IS_GRID(only) || GTK_IS_SCROLLED_WINDOW(only)
498 || GTK_IS_VIEWPORT(only) || GTK_IS_OVERLAY(only);
499 if(!anonymous || !layout) break;
500
501 widget = only;
502 }
503
504 return widget;
505}
506
507
519static gchar *_widget_label(GtkWidget *widget, const int index)
520{
521 const char *known = (const char *)g_hash_table_lookup(_g.names, widget);
522 if(!IS_NULL_PTR(known)) return g_strdup(known);
523
524 const char *type = G_OBJECT_TYPE_NAME(widget);
525 const char *text = GTK_IS_LABEL(widget) ? gtk_label_get_text(GTK_LABEL(widget))
526 : GTK_IS_BUTTON(widget) ? gtk_button_get_label(GTK_BUTTON(widget))
528 : NULL;
529 if(!IS_NULL_PTR(text) && text[0] != '\0') return g_strdup_printf("%s (%s)", text, type);
530
531 const char *name = gtk_widget_get_name(widget);
532 if(g_strcmp0(name, type)) return g_strdup_printf("%s (%s)", name, type);
533
534 // Anonymous plumbing. What it contains identifies it far better than its rank among its
535 // siblings does; the rank is only there for the ones that contain no text at all.
536 const char *hint = _first_text(widget);
537 return IS_NULL_PTR(hint) ? g_strdup_printf("%s #%d", type, index)
538 : g_strdup_printf("%s: %s", type, hint);
539}
540
541
545static void _append_placeholder(GtkTreeIter *parent)
546{
547 GtkTreeIter placeholder;
548 gtk_tree_store_append(_g.store, &placeholder, parent);
549 gtk_tree_store_set(_g.store, &placeholder, COL_CHECKED, FALSE, COL_LABEL, "…", COL_PAGE, "", COL_TARGET, NULL,
551}
552
553
564static void _append_row(GtkTreeIter *parent, GtkWidget *target, const char *label_override, const int index)
565{
566 if(IS_NULL_PTR(target)) return;
567
568 // The row captures the outer widget but is named and unfolded from the innermost one of
569 // its wrapper chain -- see _unwrap(). _on_row_expanded() unwraps the same way, so the
570 // children it later lists are the ones this placeholder promises.
571 GtkWidget *inner = _unwrap(target);
572 gchar *label = IS_NULL_PTR(label_override) ? _widget_label(inner, index) : g_strdup(label_override);
573 const char *page = (const char *)g_hash_table_lookup(_g.pages, label);
574
575 GtkTreeIter iter;
576 gtk_tree_store_append(_g.store, &iter, parent);
577 // Checked from the remembered selection, whether or not the widget is on screen: a row
578 // greyed out here is one this pass will report as skipped, not one to silently forget.
579 gtk_tree_store_set(_g.store, &iter, COL_CHECKED, g_hash_table_contains(_g.selected, label), COL_LABEL,
580 label, COL_PAGE, IS_NULL_PTR(page) ? "" : page, COL_TARGET, target, COL_CAPTURABLE,
581 TRUE, COL_ENABLED, gtk_widget_get_mapped(target), -1);
582 dt_free(label);
583
584 if(!GTK_IS_CONTAINER(inner)) return;
585
586 // A tree view unfolds into its items as well as into its toolkit children, which are rarely
587 // more than an editing entry -- see _append_tree_rows().
588 GList *children = gtk_container_get_children(GTK_CONTAINER(inner));
589 GtkTreeModel *items = GTK_IS_TREE_VIEW(inner) ? gtk_tree_view_get_model(GTK_TREE_VIEW(inner)) : NULL;
590 if(!IS_NULL_PTR(children) || (!IS_NULL_PTR(items) && gtk_tree_model_iter_n_children(items, NULL) > 0))
591 _append_placeholder(&iter);
592 g_list_free(children);
593}
594
595
605static gchar *_tree_row_text(GtkTreeView *view, GtkTreeModel *model, GtkTreeIter *item)
606{
607 GString *text = g_string_new(NULL);
608 const gint columns = gtk_tree_view_get_n_columns(view);
609 for(gint c = 0; c < columns; c++)
610 {
611 GtkTreeViewColumn *column = gtk_tree_view_get_column(view, c);
612 if(!gtk_tree_view_column_get_visible(column)) continue;
613
614 gtk_tree_view_column_cell_set_cell_data(column, model, item, FALSE, FALSE);
615 GList *cells = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(column));
616 for(GList *cell = cells; cell; cell = g_list_next(cell))
617 {
618 GtkCellRenderer *renderer = GTK_CELL_RENDERER(cell->data);
619 if(!GTK_IS_CELL_RENDERER_TEXT(renderer) || !gtk_cell_renderer_get_visible(renderer)) continue;
620
621 gchar *value = NULL;
622 g_object_get(renderer, "text", &value, NULL);
623 if(!IS_NULL_PTR(value) && value[0] != '\0')
624 {
625 if(text->len) g_string_append(text, " | ");
626 g_string_append(text, value);
627 }
628 dt_free(value);
629 }
630 g_list_free(cells);
631 }
632
633 // A label is one line of the panel and one line of the map.
634 g_strdelimit(text->str, "\n\r\t", ' ');
635 return g_string_free(text, FALSE);
636}
637
638
641static void _append_tree_row(GtkTreeIter *parent, GtkTreeView *view, GtkTreeModel *model, GtkTreeIter *item,
642 const int index)
643{
644 // Text first, then what it is, like any other row -- which also keeps an item reading
645 // "Exposure" from taking the key of the module of that name in the map and the selection.
646 gchar *text = _tree_row_text(view, model, item);
647 gchar *label = text[0] != '\0' ? g_strdup_printf("%s (%s row)", text, G_OBJECT_TYPE_NAME(view))
648 : g_strdup_printf("%s row #%d", G_OBJECT_TYPE_NAME(view), index);
649 dt_free(text);
650 const char *page = (const char *)g_hash_table_lookup(_g.pages, label);
651
652 GtkTreePath *path = gtk_tree_model_get_path(model, item);
653 GtkTreeRowReference *row = gtk_tree_row_reference_new(model, path);
654 gtk_tree_path_free(path);
655 GdkRectangle area;
656
657 GtkTreeIter iter;
658 gtk_tree_store_append(_g.store, &iter, parent);
659 gtk_tree_store_set(_g.store, &iter, COL_CHECKED, g_hash_table_contains(_g.selected, label), COL_LABEL, label,
661 TRUE, COL_ENABLED, _tree_row_area(view, row, &area), -1);
662 gtk_tree_row_reference_free(row); // the store holds its own copy
663 dt_free(label);
664
665 if(gtk_tree_model_iter_has_child(model, item)) _append_placeholder(&iter);
666}
667
668
677static void _append_tree_rows(GtkTreeIter *parent, GtkTreeView *view, GtkTreeRowReference *under)
678{
679 GtkTreeModel *model = gtk_tree_view_get_model(view);
680 if(IS_NULL_PTR(model)) return;
681
682 GtkTreeIter item;
683 gboolean more = FALSE;
684 if(IS_NULL_PTR(under))
685 more = gtk_tree_model_get_iter_first(model, &item);
686 else if(gtk_tree_row_reference_valid(under) && gtk_tree_row_reference_get_model(under) == model)
687 {
688 GtkTreePath *path = gtk_tree_row_reference_get_path(under);
689 GtkTreeIter owner;
690 more = gtk_tree_model_get_iter(model, &owner, path) && gtk_tree_model_iter_children(model, &item, &owner);
691 gtk_tree_path_free(path);
692 }
693
694 for(int index = 1; more; index++, more = gtk_tree_model_iter_next(model, &item))
695 _append_tree_row(parent, view, model, &item, index);
696}
697
698
699/* Registered module inventories. Filled from src/darktable.c -- see the header for why the
700 * panel cannot look the modules up itself. Sections appear in registration order. */
706
707static GList *_sources = NULL; // _doc_source_t*, owned here, alive for the process
708
710{
711 if(IS_NULL_PTR(section) || IS_NULL_PTR(source)) return;
712
713 _doc_source_t *entry = (_doc_source_t *)g_malloc0(sizeof(_doc_source_t));
714 entry->section = g_strdup(section);
715 entry->fill = source;
716 _sources = g_list_append(_sources, entry);
717}
718
722void dt_gui_doc_screenshot_add_target(void *inventory, GtkWidget *widget, const char *name)
723{
724 if(IS_NULL_PTR(inventory) || IS_NULL_PTR(widget) || IS_NULL_PTR(name)) return;
725
726 g_hash_table_insert(_g.names, widget, g_strdup(name));
727 _append_row((GtkTreeIter *)inventory, widget, NULL, 0);
728}
729
731static void _append_section(const char *title, GtkTreeIter *section)
732{
733 gtk_tree_store_append(_g.store, section, NULL);
734 gtk_tree_store_set(_g.store, section, COL_CHECKED, FALSE, COL_LABEL, title, COL_PAGE, "", COL_TARGET, NULL,
736}
737
738
740static void _on_row_expanded(GtkTreeView *view, GtkTreeIter *iter, GtkTreePath *path, gpointer user_data)
741{
742 GtkTreeModel *model = GTK_TREE_MODEL(_g.store);
743 GtkTreeIter placeholder;
744 if(!gtk_tree_model_iter_children(model, &placeholder, iter)) return;
745
746 // Below the top level, only a placeholder carries no target: anything else is already built.
747 GObject *probe = NULL;
748 gtk_tree_model_get(model, &placeholder, COL_TARGET, &probe, -1);
749 if(!IS_NULL_PTR(probe))
750 {
751 g_object_unref(probe);
752 return;
753 }
754
755 GObject *target = NULL;
756 GtkTreeRowReference *row = NULL;
757 gtk_tree_model_get(model, iter, COL_TARGET, &target, COL_ROW, &row, -1);
758 if(!IS_NULL_PTR(row))
759 {
760 // A tree view item: what unfolds under it belongs to the model, not to the toolkit.
761 _append_tree_rows(iter, GTK_TREE_VIEW(target), row);
762 gtk_tree_row_reference_free(row);
763 }
764 else
765 {
766 GtkWidget *inner = _unwrap(GTK_WIDGET(target));
767 GList *children = gtk_container_get_children(GTK_CONTAINER(inner));
768 int index = 1;
769 for(GList *item = children; item; item = g_list_next(item), index++)
770 _append_row(iter, GTK_WIDGET(item->data), NULL, index);
771 g_list_free(children);
772
773 if(GTK_IS_TREE_VIEW(inner)) _append_tree_rows(iter, GTK_TREE_VIEW(inner), NULL);
774 }
775 g_object_unref(target);
776
777 // Dropped last, so the row is never momentarily childless -- which would collapse the
778 // expander arrow under the very expansion that is being served.
779 gtk_tree_store_remove(_g.store, &placeholder);
780}
781
782
789static gchar *_label_chain(GtkTreePath *path)
790{
791 GtkTreeModel *model = GTK_TREE_MODEL(_g.store);
792 gint depth = 0;
793 gint *indices = gtk_tree_path_get_indices_with_depth(path, &depth);
794
795 GString *chain = g_string_new(NULL);
796 GtkTreePath *walk = gtk_tree_path_new();
797 for(gint level = 0; level < depth; level++)
798 {
799 gtk_tree_path_append_index(walk, indices[level]);
800
801 GtkTreeIter iter;
802 if(!gtk_tree_model_get_iter(model, &iter, walk)) break;
803
804 gchar *label = NULL;
805 gtk_tree_model_get(model, &iter, COL_LABEL, &label, -1);
806 if(chain->len) g_string_append(chain, DOC_SCREENSHOT_SEPARATOR);
807 g_string_append(chain, IS_NULL_PTR(label) ? "" : label);
808 dt_free(label);
809 }
810 gtk_tree_path_free(walk);
811
812 return g_string_free(chain, FALSE);
813}
814
815
817static void _collect_expanded(GtkTreeView *view, GtkTreePath *path, gpointer user_data)
818{
819 GList **chains = (GList **)user_data;
820 *chains = g_list_prepend(*chains, _label_chain(path));
821}
822
823
834static gboolean _find_row(const char *chain, GtkTreeIter *found)
835{
836 GtkTreeModel *model = GTK_TREE_MODEL(_g.store);
837 gchar **labels = g_strsplit(chain, DOC_SCREENSHOT_SEPARATOR, -1);
838 GtkTreeIter iter;
839 gboolean have = FALSE;
840
841 for(gchar **label = labels; *label; label++)
842 {
843 if(have)
844 {
845 GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
846 gtk_tree_view_expand_row(GTK_TREE_VIEW(_g.view), path, FALSE);
847 gtk_tree_path_free(path);
848 }
849
850 GtkTreeIter child;
851 if(!gtk_tree_model_iter_children(model, &child, have ? &iter : NULL))
852 {
853 have = FALSE;
854 break;
855 }
856
857 gboolean matched = FALSE;
858 do
859 {
860 gchar *text = NULL;
861 gtk_tree_model_get(model, &child, COL_LABEL, &text, -1);
862 matched = !g_strcmp0(text, *label);
863 dt_free(text);
864 } while(!matched && gtk_tree_model_iter_next(model, &child));
865
866 if(!matched)
867 {
868 have = FALSE;
869 break;
870 }
871
872 iter = child;
873 have = TRUE;
874 }
875 g_strfreev(labels);
876
877 if(have) *found = iter;
878 return have;
879}
880
881
891static gboolean _is_application_window(GtkWindow *window)
892{
893 const char *title = gtk_window_get_title(window);
894 return GTK_WIDGET(window) != dt_gui_main_window() && GTK_WIDGET(window) != _g.window
895 && gtk_window_get_window_type(window) == GTK_WINDOW_TOPLEVEL && !IS_NULL_PTR(title)
896 && title[0] != '\0' && gtk_window_get_transient_for(window) != GTK_WINDOW(_g.window);
897}
898
899
900static gint _compare_window_titles(gconstpointer a, gconstpointer b)
901{
902 return g_utf8_collate(gtk_window_get_title(GTK_WINDOW(a)), gtk_window_get_title(GTK_WINDOW(b)));
903}
904
905
918static GList *_application_windows(void)
919{
920 GList *toplevels = gtk_window_list_toplevels();
921 GList *windows = NULL;
922 for(GList *item = toplevels; item; item = g_list_next(item))
923 if(_is_application_window(GTK_WINDOW(item->data))) windows = g_list_prepend(windows, item->data);
924 g_list_free(toplevels);
925
926 return g_list_sort(windows, _compare_window_titles);
927}
928
929
940static void _populate(GtkWidget *widget, gpointer user_data)
941{
942 // Remember what is unfolded and where the cursor sits. A refresh replaces every row, and
943 // collapsing the tree back to its roots would throw away the reader's place at exactly the
944 // moment they need it: the refresh loop is "change something in the application, refresh,
945 // look at the same row again".
946 GList *expanded = NULL;
947 gtk_tree_view_map_expanded_rows(GTK_TREE_VIEW(_g.view), _collect_expanded, &expanded);
948
949 gchar *selected = NULL;
950 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(_g.view));
951 GtkTreeModel *model = NULL;
952 GtkTreeIter cursor;
953 if(gtk_tree_selection_get_selected(selection, &model, &cursor))
954 {
955 GtkTreePath *path = gtk_tree_model_get_path(model, &cursor);
956 selected = _label_chain(path);
957 gtk_tree_path_free(path);
958 }
959
960 gtk_tree_store_clear(_g.store);
961 g_hash_table_remove_all(_g.names);
962 _load_pages();
963
964 dt_ui_t *const ui = dt_gui_get_ui();
965 GtkTreeIter section;
966
967 // Module inventories, in registration order. The panel does not know what a module is --
968 // see dt_gui_doc_screenshot_register_source() and its callers in src/darktable.c. The
969 // sections are appended even when a source yields nothing, so the inventory reads the same
970 // whichever view is current: the darkroom list is only populated while an image is open.
971 for(GList *item = _sources; item; item = g_list_next(item))
972 {
973 const _doc_source_t *const src = (const _doc_source_t *)item->data;
974 _append_section(src->section, &section);
975 src->fill(&section);
976 }
977
978 // Named before use, so drilling down into the window finds them under these names too.
979 g_hash_table_insert(_g.names, ui->top_panel, g_strdup(_("Menu bar")));
980 g_hash_table_insert(_g.names, ui->panels[DT_UI_PANEL_TOP], g_strdup(_("Top panel")));
981 g_hash_table_insert(_g.names, ui->panels[DT_UI_PANEL_LEFT], g_strdup(_("Left panel")));
982 g_hash_table_insert(_g.names, ui->panels[DT_UI_PANEL_RIGHT], g_strdup(_("Right panel")));
983 g_hash_table_insert(_g.names, ui->panels[DT_UI_PANEL_BOTTOM], g_strdup(_("Bottom panel")));
984 g_hash_table_insert(_g.names, dt_ui_center_base(ui), g_strdup(_("Center area")));
985
986 _append_section(_("Panels"), &section);
987 _append_row(&section, ui->top_panel, NULL, 0);
988 _append_row(&section, ui->panels[DT_UI_PANEL_TOP], NULL, 0);
989 _append_row(&section, ui->panels[DT_UI_PANEL_LEFT], NULL, 0);
990 _append_row(&section, ui->panels[DT_UI_PANEL_RIGHT], NULL, 0);
991 _append_row(&section, ui->panels[DT_UI_PANEL_BOTTOM], NULL, 0);
992 _append_row(&section, dt_ui_center_base(ui), NULL, 0);
993
994 // Last: the whole main window, from which everything above is reachable, then the windows
995 // it opens, which nothing above reaches. A window is named by its title, and named before
996 // use like the panels are.
997 _append_section(_("Windows"), &section);
998 _append_row(&section, dt_gui_main_window(), _("Main window"), 0);
999
1000 GList *windows = _application_windows();
1001 for(GList *item = windows; item; item = g_list_next(item))
1002 {
1003 GtkWidget *window = GTK_WIDGET(item->data);
1004 g_hash_table_insert(_g.names, window, g_strdup(gtk_window_get_title(GTK_WINDOW(window))));
1005 _append_row(&section, window, NULL, 0);
1006 }
1007 g_list_free(windows);
1008
1009 // Unfold what was unfolded. _find_row() expands each chain's ancestors on its way down, so
1010 // the order these come back in does not matter; a chain that no longer resolves -- its
1011 // module unloaded, its widget moved -- is simply dropped.
1012 for(GList *item = expanded; item; item = g_list_next(item))
1013 {
1014 GtkTreeIter iter;
1015 if(!_find_row((const char *)item->data, &iter)) continue;
1016
1017 GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(_g.store), &iter);
1018 gtk_tree_view_expand_row(GTK_TREE_VIEW(_g.view), path, FALSE);
1019 gtk_tree_path_free(path);
1020 }
1021 g_list_free_full(expanded, g_free);
1022
1023 if(IS_NULL_PTR(selected)) return;
1024
1025 // Put the cursor back and scroll to it. Selecting fires _on_row_selected(), so the preview
1026 // comes back with it -- which is the whole point of refreshing while watching one widget.
1027 GtkTreeIter iter;
1028 if(_find_row(selected, &iter))
1029 {
1030 GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(_g.store), &iter);
1031 gtk_tree_view_expand_to_path(GTK_TREE_VIEW(_g.view), path);
1032 gtk_tree_selection_select_iter(selection, &iter);
1033 gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(_g.view), path, NULL, FALSE, 0., 0.);
1034 gtk_tree_path_free(path);
1035 }
1037}
1038
1039
1046static void _on_row_selected(GtkTreeSelection *selection, gpointer user_data)
1047{
1048 gtk_image_clear(GTK_IMAGE(_g.preview));
1049 gtk_label_set_text(GTK_LABEL(_g.geometry), "");
1050
1051 GtkTreeModel *model = NULL;
1052 GtkTreeIter iter;
1053 if(!gtk_tree_selection_get_selected(selection, &model, &iter)) return;
1054
1055 GObject *target = NULL;
1056 GtkTreeRowReference *row = NULL;
1057 gtk_tree_model_get(model, &iter, COL_TARGET, &target, COL_ROW, &row, -1);
1058 if(IS_NULL_PTR(target)) return; // a section header, or a placeholder not yet unfolded
1059
1060 cairo_surface_t *surface = _render_target(GTK_WIDGET(target), row);
1061 gtk_tree_row_reference_free(row);
1062 g_object_unref(target);
1063 if(IS_NULL_PTR(surface))
1064 {
1065 gtk_label_set_text(GTK_LABEL(_g.geometry), _("not displayed right now"));
1066 return;
1067 }
1068
1069 const int width = cairo_image_surface_get_width(surface);
1070 const int height = cairo_image_surface_get_height(surface);
1071 GdkPixbuf *pixbuf = gdk_pixbuf_get_from_surface(surface, 0, 0, width, height);
1072 cairo_surface_destroy(surface);
1073
1074 // Shrunk to fit, never enlarged: a 24 px icon button has to read as a 24 px icon button.
1075 const double fit = MIN(1.0, (double)DOC_SCREENSHOT_PREVIEW_SIZE / (double)MAX(width, height));
1076 GdkPixbuf *scaled = gdk_pixbuf_scale_simple(pixbuf, MAX(1, (int)(width * fit)),
1077 MAX(1, (int)(height * fit)), GDK_INTERP_BILINEAR);
1078 gtk_image_set_from_pixbuf(GTK_IMAGE(_g.preview), scaled);
1079 g_object_unref(scaled);
1080 g_object_unref(pixbuf);
1081
1082 gchar *size = g_strdup_printf(_("%d x %d px"), width, height);
1083 gtk_label_set_text(GTK_LABEL(_g.geometry), size);
1084 dt_free(size);
1085}
1086
1087
1089static void _on_toggled(GtkCellRendererToggle *renderer, gchar *path_string, gpointer user_data)
1090{
1091 GtkTreeIter iter;
1092 if(!gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(_g.store), &iter, path_string)) return;
1093
1094 gboolean checked = FALSE;
1095 gchar *label = NULL;
1096 gtk_tree_model_get(GTK_TREE_MODEL(_g.store), &iter, COL_CHECKED, &checked, COL_LABEL, &label, -1);
1097 gtk_tree_store_set(_g.store, &iter, COL_CHECKED, !checked, -1);
1098
1099 if(checked)
1100 g_hash_table_remove(_g.selected, label);
1101 else
1102 g_hash_table_add(_g.selected, g_strdup(label));
1103
1104 dt_free(label);
1106}
1107
1108
1109static gboolean _select_row(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
1110{
1111 const _doc_screenshot_select_t mode = (_doc_screenshot_select_t)GPOINTER_TO_INT(user_data);
1112
1113 gboolean capturable = FALSE;
1114 gboolean enabled = FALSE;
1115 gchar *label = NULL;
1116 gchar *page = NULL;
1117 gtk_tree_model_get(model, iter, COL_CAPTURABLE, &capturable, COL_ENABLED, &enabled, COL_LABEL, &label,
1118 COL_PAGE, &page, -1);
1119
1120 // A row we cannot see right now is left alone by "all" and "mapped": a documentation pass
1121 // captures the lighttable modules from lighttable and the darkroom ones from darkroom, and
1122 // the second half must not drop what the first half picked. "Select none" is the eraser,
1123 // and the only way to forget rows that are off screen.
1124 const gboolean reachable = capturable && (enabled || mode == DOC_SCREENSHOT_SELECT_NONE);
1125 if(reachable)
1126 {
1127 const gboolean wanted = (mode == DOC_SCREENSHOT_SELECT_MAPPED)
1128 ? !IS_NULL_PTR(page) && page[0] != '\0'
1129 : (mode == DOC_SCREENSHOT_SELECT_ALL);
1130 gtk_tree_store_set(_g.store, iter, COL_CHECKED, wanted, -1);
1131
1132 if(wanted)
1133 g_hash_table_add(_g.selected, g_strdup(label));
1134 else
1135 g_hash_table_remove(_g.selected, label);
1136 }
1137
1138 dt_free(page);
1139 dt_free(label);
1140 return FALSE; // walk the whole model
1141}
1142
1143
1147static void _on_select(GtkButton *button, gpointer user_data)
1148{
1149 // Rows that were never unfolded are not in the model, so clearing has to go through the
1150 // remembered set directly to reach them.
1151 if(GPOINTER_TO_INT(user_data) == DOC_SCREENSHOT_SELECT_NONE) g_hash_table_remove_all(_g.selected);
1152
1153 gtk_tree_model_foreach(GTK_TREE_MODEL(_g.store), _select_row, user_data);
1155}
1156
1157
1158static gboolean _capture_row(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
1159{
1161
1162 gboolean checked = FALSE;
1163 gchar *label = NULL;
1164 gchar *page = NULL;
1165 GObject *target = NULL;
1166 GtkTreeRowReference *row = NULL;
1167 gtk_tree_model_get(model, iter, COL_CHECKED, &checked, COL_LABEL, &label, COL_PAGE, &page, COL_TARGET,
1168 &target, COL_ROW, &row, -1);
1169
1170 if(checked && !IS_NULL_PTR(target))
1171 {
1172 // Only a capture that can differ between languages gets the language code.
1173 const gboolean localized = _widget_has_text(GTK_WIDGET(target));
1174
1175 gchar *relative = NULL;
1176 if(!IS_NULL_PTR(page) && page[0] != '\0')
1177 {
1178 // A mapped row goes exactly where the documentation expects its illustration.
1179 relative = localized ? _localized_path(page, capture->code) : g_strdup(page);
1180 }
1181 else
1182 {
1183 // An unmapped one lands at the root under its own name. Two rows can legitimately
1184 // carry the same name -- an unnamed GtkBox, or a module reached both from its section
1185 // and by drilling down its panel -- so number the repeats rather than have them
1186 // overwrite each other.
1187 const int seen = GPOINTER_TO_INT(g_hash_table_lookup(capture->used, label));
1188 g_hash_table_insert(capture->used, g_strdup(label), GINT_TO_POINTER(seen + 1));
1189
1190 gchar *base = seen ? g_strdup_printf("%s_%d", label, seen + 1) : g_strdup(label);
1191 relative = localized ? g_strdup_printf("%s.%s.png", base, capture->code)
1192 : g_strconcat(base, ".png", NULL);
1193 dt_free(base);
1194 g_strcanon(relative, DOC_SCREENSHOT_FILENAME_ALLOWED, '_');
1195 }
1196
1197 // The three return codes finally earn their keep: a row whose widget is simply not on
1198 // screen is pending, not broken -- a remembered selection spans both views, and only one
1199 // of them is up at a time.
1200 gchar *file = g_build_filename(capture->folder, relative, NULL);
1201 const int result = _save_target_as_image(GTK_WIDGET(target), row, file);
1202 if(result == 1)
1203 capture->skipped++;
1204 else if(result)
1205 capture->failed++;
1206 else
1207 capture->saved++;
1208
1209 dt_free(file);
1210 dt_free(relative);
1211 }
1212
1213 dt_free(page);
1214 dt_free(label);
1215 gtk_tree_row_reference_free(row);
1216 if(!IS_NULL_PTR(target)) g_object_unref(target);
1217 return FALSE; // walk the whole model
1218}
1219
1220
1222static void _on_capture(GtkButton *button, gpointer user_data)
1223{
1224 gchar *folder = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(_g.folder));
1225 if(IS_NULL_PTR(folder))
1226 {
1227 gtk_label_set_text(GTK_LABEL(_g.status), _("Pick a destination folder first."));
1228 return;
1229 }
1230
1231 // The documentation names its English files "en"; the untranslated locale calls itself "C".
1232 const char *code = dt_l10n_get_current_code(dt_l10n_get_global());
1233 if(!g_strcmp0(code, "C")) code = "en";
1234
1236 .code = code,
1237 .used = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL),
1238 .saved = 0,
1239 .skipped = 0,
1240 .failed = 0 };
1241 gtk_tree_model_foreach(GTK_TREE_MODEL(_g.store), _capture_row, &capture);
1242 g_hash_table_destroy(capture.used);
1243
1244 gchar *report = g_strdup_printf(_("%d image(s) written for language \"%s\" in %s "
1245 "-- %d skipped (not displayed), %d failed"),
1246 capture.saved, code, folder, capture.skipped, capture.failed);
1247 gtk_label_set_text(GTK_LABEL(_g.status), report);
1248 dt_free(report);
1249 dt_free(folder);
1250}
1251
1252
1253static void _on_destroy(GtkWidget *widget, gpointer user_data)
1254{
1255 // The store is owned by the view and dies with it, releasing its references on the widgets
1256 // the rows pointed at. The destination directory outlives the window: it came from the
1257 // command line and pre-fills the chooser again next time.
1258 g_hash_table_destroy(_g.names);
1259 g_hash_table_destroy(_g.pages);
1260 g_hash_table_destroy(_g.selected);
1261 _g.names = NULL;
1262 _g.pages = NULL;
1263 _g.selected = NULL;
1264 _g.store = NULL;
1265 _g.view = NULL;
1266 _g.window = NULL;
1267 _g.folder = NULL;
1268 _g.status = NULL;
1269 _g.preview = NULL;
1270 _g.geometry = NULL;
1271}
1272
1273
1275{
1276 if(_g.window)
1277 {
1278 gtk_window_present(GTK_WINDOW(_g.window));
1279 return;
1280 }
1281
1282 _g.names = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free);
1283 _g.pages = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
1284 _g.selected = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
1286
1287 _g.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
1288 gtk_window_set_title(GTK_WINDOW(_g.window), _("Documentation screenshots"));
1289 gtk_window_set_default_size(GTK_WINDOW(_g.window), 1100, 760);
1290 gtk_window_set_transient_for(GTK_WINDOW(_g.window), GTK_WINDOW(dt_gui_main_window()));
1291 g_signal_connect(_g.window, "destroy", G_CALLBACK(_on_destroy), NULL);
1292
1293 GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
1294 gtk_container_set_border_width(GTK_CONTAINER(vbox), 8);
1295 gtk_container_add(GTK_CONTAINER(_g.window), vbox);
1296
1297 // Destination folder, and a way to rebuild the tree after a view change or an image load
1298 GtkWidget *bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
1299 gtk_box_pack_start(GTK_BOX(vbox), bar, FALSE, FALSE, 0);
1300 gtk_box_pack_start(GTK_BOX(bar), gtk_label_new(_("Documentation root")), FALSE, FALSE, 0);
1301
1302 _g.folder = gtk_file_chooser_button_new(_("Documentation root"), GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER);
1303 // Not "..." MACRO "..." inside _(): gettext extracts the source text verbatim and would
1304 // never match the concatenated string at runtime.
1305 gchar *tooltip = g_strdup_printf(_("Widget-to-page map: %s at the root of this folder, "
1306 "reloaded whenever the folder changes."),
1308 gtk_widget_set_tooltip_text(_g.folder, tooltip);
1310 const char *pictures = g_get_user_special_dir(G_USER_DIRECTORY_PICTURES);
1311 const char *destination = !IS_NULL_PTR(_g.directory) ? _g.directory
1312 : !IS_NULL_PTR(pictures) ? pictures
1313 : g_get_home_dir();
1314 gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(_g.folder), destination);
1315 // Rebuilding on "file-set" is what reloads the map: the pages shown per row must describe
1316 // the folder the capture will actually write into.
1317 g_signal_connect(_g.folder, "file-set", G_CALLBACK(_populate), NULL);
1318 gtk_box_pack_start(GTK_BOX(bar), _g.folder, TRUE, TRUE, 0);
1319
1320 GtkWidget *refresh = gtk_button_new_with_label(_("Refresh"));
1321 g_signal_connect(refresh, "clicked", G_CALLBACK(_populate), NULL);
1322 gtk_box_pack_start(GTK_BOX(bar), refresh, FALSE, FALSE, 0);
1323
1324 // The tree: the first column carries the expander arrow, the check box and the name, so a
1325 // row reads as a single line however deep it sits; the second shows what the map binds it
1326 // to, which is the difference between a stray capture and a documentation update.
1327 _g.store = gtk_tree_store_new(COL_COUNT, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_OBJECT,
1328 GTK_TYPE_TREE_ROW_REFERENCE, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN);
1329 _g.view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(_g.store));
1330 g_object_unref(_g.store); // the view owns the model from here on
1331 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(_g.view), TRUE);
1332 g_signal_connect(_g.view, "row-expanded", G_CALLBACK(_on_row_expanded), NULL);
1333
1334 GtkTreeViewColumn *widgets = gtk_tree_view_column_new();
1335 gtk_tree_view_column_set_title(widgets, _("Widget"));
1336 gtk_tree_view_column_set_expand(widgets, TRUE);
1337 GtkCellRenderer *toggle = gtk_cell_renderer_toggle_new();
1338 g_signal_connect(toggle, "toggled", G_CALLBACK(_on_toggled), NULL);
1339 gtk_tree_view_column_pack_start(widgets, toggle, FALSE);
1340 gtk_tree_view_column_add_attribute(widgets, toggle, "active", COL_CHECKED);
1341 gtk_tree_view_column_add_attribute(widgets, toggle, "visible", COL_CAPTURABLE);
1342 // Activatable on COL_CAPTURABLE, NOT on COL_ENABLED: a row is checked to say "capture this
1343 // one", and that intent is worth expressing for a module of the other view or a collapsed
1344 // module's body, neither of which is on screen at the moment the user ticks it. The
1345 // selection is remembered and the capture reports what was not displayed as skipped, so
1346 // the natural workflow -- tick everything now, switch view, capture again -- needs the box
1347 // to be clickable while the widget is not up. Only the greyed label says "not right now".
1348 gtk_tree_view_column_add_attribute(widgets, toggle, "activatable", COL_CAPTURABLE);
1349
1350 GtkCellRenderer *text = gtk_cell_renderer_text_new();
1351 gtk_tree_view_column_pack_start(widgets, text, TRUE);
1352 gtk_tree_view_column_add_attribute(widgets, text, "text", COL_LABEL);
1353 gtk_tree_view_column_add_attribute(widgets, text, "sensitive", COL_ENABLED);
1354 gtk_tree_view_append_column(GTK_TREE_VIEW(_g.view), widgets);
1355
1356 GtkCellRenderer *page = gtk_cell_renderer_text_new();
1357 GtkTreeViewColumn *pages = gtk_tree_view_column_new_with_attributes(_("Documentation page"), page, "text",
1358 COL_PAGE, "sensitive", COL_ENABLED, NULL);
1359 gtk_tree_view_append_column(GTK_TREE_VIEW(_g.view), pages);
1360
1361 g_signal_connect(gtk_tree_view_get_selection(GTK_TREE_VIEW(_g.view)), "changed",
1362 G_CALLBACK(_on_row_selected), NULL);
1363
1364 GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL);
1365 gtk_container_add(GTK_CONTAINER(scroll), _g.view);
1366
1367 // Tree and preview side by side: clicking a row answers "what is this one" immediately,
1368 // which no amount of naming can do for a stack of anonymous containers.
1369 _g.preview = gtk_image_new();
1370 _g.geometry = gtk_label_new("");
1371 GtkWidget *preview_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
1372 gtk_box_pack_start(GTK_BOX(preview_box), _g.preview, TRUE, FALSE, 0);
1373 gtk_box_pack_start(GTK_BOX(preview_box), _g.geometry, FALSE, FALSE, 0);
1374
1375 GtkWidget *preview_frame = gtk_frame_new(_("Preview"));
1376 gtk_container_set_border_width(GTK_CONTAINER(preview_box), 6);
1377 gtk_container_add(GTK_CONTAINER(preview_frame), preview_box);
1378 gtk_widget_set_size_request(preview_frame, DOC_SCREENSHOT_PREVIEW_SIZE + 24, -1);
1379
1380 GtkWidget *panes = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
1381 gtk_paned_pack1(GTK_PANED(panes), scroll, TRUE, FALSE);
1382 gtk_paned_pack2(GTK_PANED(panes), preview_frame, FALSE, FALSE);
1383 gtk_box_pack_start(GTK_BOX(vbox), panes, TRUE, TRUE, 0);
1384
1385 GtkWidget *actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
1386 gtk_box_pack_start(GTK_BOX(vbox), actions, FALSE, FALSE, 0);
1387
1388 GtkWidget *select_mapped = gtk_button_new_with_label(_("Select mapped"));
1389 gtk_widget_set_tooltip_text(select_mapped, _("check every displayed row the map names"));
1390 g_signal_connect(select_mapped, "clicked", G_CALLBACK(_on_select), GINT_TO_POINTER(2));
1391 gtk_box_pack_start(GTK_BOX(actions), select_mapped, FALSE, FALSE, 0);
1392
1393 GtkWidget *select_all = gtk_button_new_with_label(_("Select all"));
1394 gtk_widget_set_tooltip_text(select_all, _("only affects the rows already unfolded"));
1395 g_signal_connect(select_all, "clicked", G_CALLBACK(_on_select), GINT_TO_POINTER(TRUE));
1396 gtk_box_pack_start(GTK_BOX(actions), select_all, FALSE, FALSE, 0);
1397
1398 GtkWidget *select_none = gtk_button_new_with_label(_("Select none"));
1399 g_signal_connect(select_none, "clicked", G_CALLBACK(_on_select), GINT_TO_POINTER(FALSE));
1400 gtk_box_pack_start(GTK_BOX(actions), select_none, FALSE, FALSE, 0);
1401
1402 GtkWidget *capture = gtk_button_new_with_label(_("Capture"));
1403 g_signal_connect(capture, "clicked", G_CALLBACK(_on_capture), NULL);
1404 gtk_box_pack_end(GTK_BOX(actions), capture, FALSE, FALSE, 0);
1405
1406 _g.status = gtk_label_new(NULL);
1407 gtk_label_set_line_wrap(GTK_LABEL(_g.status), TRUE);
1408 gtk_widget_set_halign(_g.status, GTK_ALIGN_START);
1409 gtk_box_pack_start(GTK_BOX(vbox), _g.status, FALSE, FALSE, 0);
1410
1411 gtk_widget_show_all(_g.window);
1412 _populate(NULL, NULL);
1413}
GtkWidget * dt_gui_main_window(void)
struct dt_ui_t * dt_gui_get_ui(void)
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
const char * extension(dt_imageio_module_data_t *data)
Definition avif.c:651
const char * dt_bauhaus_widget_get_label(GtkWidget *widget)
Definition bauhaus.c:1548
#define DT_IS_BAUHAUS_WIDGET(obj)
Definition bauhaus.h:64
struct _GtkWidget GtkWidget
GtkWidget, opaque, spelled exactly as GTK spells it.
Definition colorspaces.h:98
static const int row
gchar * dt_conf_get_string(const char *name)
Read the stored string for name as a private copy.
void dt_conf_set_string(const char *name, const char *val)
static gboolean _is_application_window(GtkWindow *window)
#define DOC_SCREENSHOT_MAP_FILE
gchar * directory
documentation root given after –doc, or NULL
static gboolean _find_row(const char *chain, GtkTreeIter *found)
_doc_screenshot_column_t
@ COL_CHECKED
the capture check box
@ COL_TARGET
@ COL_PAGE
illustration this widget stands for, from the map. "" when unmapped
@ COL_CAPTURABLE
there is a target, so a check box is worth showing at all
@ COL_ROW
the tree view item this row captures. NULL when it captures a whole widget
@ COL_LABEL
displayed name, also the key into the widget-to-page map
@ COL_COUNT
@ COL_ENABLED
the target is currently displayed. Always TRUE on a section header
static int _write_surface(cairo_surface_t *surface, const char *filename)
void dt_gui_doc_screenshot_window_show(void)
static void _populate(GtkWidget *widget, gpointer user_data)
#define DOC_SCREENSHOT_SELECTION_CONF_KEY
GtkWidget * geometry
its size, under the preview
GHashTable * names
GtkWidget* -> the name the application knows it by.
#define DOC_SCREENSHOT_FILENAME_ALLOWED
static gchar * _localized_path(const char *relative, const char *code)
GtkWidget * window
void dt_gui_doc_screenshot_register_source(const char *section, dt_gui_doc_screenshot_source_t source)
GtkTreeStore * store
its model, owned by the view
static gboolean _capture_row(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
void dt_gui_doc_screenshot_set_directory(const char *path)
static void _on_select(GtkButton *button, gpointer user_data)
static GList * _sources
static gchar * _widget_label(GtkWidget *widget, const int index)
void dt_gui_doc_screenshot_enable(void)
static gboolean _select_row(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
GtkWidget * preview
what the selected row actually captures
static gboolean _widget_has_text(GtkWidget *widget)
static void _collect_expanded(GtkTreeView *view, GtkTreePath *path, gpointer user_data)
static struct @30 _g
_doc_screenshot_select_t
@ DOC_SCREENSHOT_SELECT_MAPPED
check exactly the displayed rows the map names
@ DOC_SCREENSHOT_SELECT_NONE
clear everything, displayed or not
@ DOC_SCREENSHOT_SELECT_ALL
check every displayed row
static gboolean _tree_row_area(GtkTreeView *view, GtkTreeRowReference *row, GdkRectangle *area)
static void _load_selection(void)
static void _load_pages(void)
static gint _compare_window_titles(gconstpointer a, gconstpointer b)
#define DOC_SCREENSHOT_PREVIEW_SIZE
GHashTable * selected
set of checked row labels, mirrored to conf on every change
static void _append_tree_rows(GtkTreeIter *parent, GtkTreeView *view, GtkTreeRowReference *under)
GtkWidget * folder
destination folder chooser
static void _append_section(const char *title, GtkTreeIter *section)
static void _append_placeholder(GtkTreeIter *parent)
gboolean dt_gui_doc_screenshot_enabled(void)
void dt_gui_doc_screenshot_add_target(void *inventory, GtkWidget *widget, const char *name)
static void _save_selection(void)
static gchar * _label_chain(GtkTreePath *path)
static cairo_surface_t * _render_area(GtkWidget *widget, const GdkRectangle *area)
static void _append_tree_row(GtkTreeIter *parent, GtkTreeView *view, GtkTreeModel *model, GtkTreeIter *item, const int index)
static GtkWidget * _unwrap(GtkWidget *widget)
static void _on_destroy(GtkWidget *widget, gpointer user_data)
static const char * _first_text(GtkWidget *widget)
gboolean enabled
–doc was passed on the command line
GtkWidget * status
result of the last capture
static int _save_target_as_image(GtkWidget *target, GtkTreeRowReference *row, const char *filename)
static GList * _application_windows(void)
GHashTable * pages
row label -> path of the illustration, relative to the destination
static void _on_row_expanded(GtkTreeView *view, GtkTreeIter *iter, GtkTreePath *path, gpointer user_data)
static void _on_toggled(GtkCellRendererToggle *renderer, gchar *path_string, gpointer user_data)
static cairo_surface_t * _render_target(GtkWidget *target, GtkTreeRowReference *row)
static gchar * _tree_row_text(GtkTreeView *view, GtkTreeModel *model, GtkTreeIter *item)
GtkWidget * view
the tree view
#define DOC_SCREENSHOT_SEPARATOR
static void _on_capture(GtkButton *button, gpointer user_data)
static void _append_row(GtkTreeIter *parent, GtkWidget *target, const char *label_override, const int index)
static void _on_row_selected(GtkTreeSelection *selection, gpointer user_data)
void(* dt_gui_doc_screenshot_source_t)(void *inventory)
const dt_collection_sort_t items[]
Definition filter.c:102
const char * tooltip
Definition image.h:310
const char * model
const char * dt_l10n_get_current_code(dt_l10n_t *l10n)
Definition l10n.c:463
struct dt_l10n_t * dt_l10n_get_global(void)
Definition darktable.c:636
_lib_location_type_t type
Definition location.c:1
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:96
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
uint32_t width
Definition mipmap_cache.c:0
uint32_t height
Definition mipmap_cache.c:1
size_t size
Definition mipmap_cache.c:3
const char * name
Definition pdf.h:90
static const dt_aligned_pixel_simd_t value
Definition simd.h:144
const char * code
language code inserted before the extension
GHashTable * used
base file name -> how many rows already claimed it
int skipped
checked, but its widget is not on screen right now
dt_gui_doc_screenshot_source_t fill
GtkWidget * top_panel
GtkWidget * panels[DT_UI_PANEL_SIZE]
#define MIN(a, b)
Definition thinplate.c:32
#define MAX(a, b)
Definition thinplate.c:29
GtkWidget * dt_ui_center_base(dt_ui_t *ui)
@ DT_UI_PANEL_TOP
@ DT_UI_PANEL_BOTTOM
@ DT_UI_PANEL_LEFT
@ DT_UI_PANEL_RIGHT