1 /*
2     This file is part of darktable,
3     Copyright (C) 2010-2021 darktable developers.
4 
5     darktable 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     darktable 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 darktable.  If not, see <http://www.gnu.org/licenses/>.
17 */
18 #include "common/collection.h"
19 #include "common/darktable.h"
20 #include "common/debug.h"
21 #include "common/selection.h"
22 #include "control/conf.h"
23 #include "control/control.h"
24 #include "dtgtk/button.h"
25 #include "gui/accelerators.h"
26 #include "gui/gtk.h"
27 #include "libs/lib.h"
28 #include <gdk/gdkkeysyms.h>
29 #ifdef USE_LUA
30 #include "lua/call.h"
31 #include "lua/image.h"
32 #endif
33 #include "libs/lib_api.h"
34 
35 DT_MODULE(1)
36 
name(dt_lib_module_t * self)37 const char *name(dt_lib_module_t *self)
38 {
39   return _("select");
40 }
41 
views(dt_lib_module_t * self)42 const char **views(dt_lib_module_t *self)
43 {
44   static const char *v[] = {"lighttable", NULL};
45   return v;
46 }
47 
container(dt_lib_module_t * self)48 uint32_t container(dt_lib_module_t *self)
49 {
50   return DT_UI_CONTAINER_PANEL_RIGHT_CENTER;
51 }
52 
53 typedef struct dt_lib_select_t
54 {
55   GtkWidget *select_all_button, *select_none_button, *select_invert_button, *select_film_roll_button,
56       *select_untouched_button;
57 } dt_lib_select_t;
58 
_update(dt_lib_module_t * self)59 static void _update(dt_lib_module_t *self)
60 {
61   dt_lib_select_t *d = (dt_lib_select_t *)self->data;
62 
63   const uint32_t collection_cnt =  dt_collection_get_count_no_group(darktable.collection);
64   const uint32_t selected_cnt = dt_collection_get_selected_count(darktable.collection);
65 
66   gtk_widget_set_sensitive(GTK_WIDGET(d->select_all_button), selected_cnt < collection_cnt);
67   gtk_widget_set_sensitive(GTK_WIDGET(d->select_none_button), selected_cnt > 0);
68 
69   gtk_widget_set_sensitive(GTK_WIDGET(d->select_invert_button), collection_cnt > 0);
70 
71   //theoretically can count if there are unaltered in collection but no need to waste CPU cycles on that.
72   gtk_widget_set_sensitive(GTK_WIDGET(d->select_untouched_button), collection_cnt > 0);
73 
74   gtk_widget_set_sensitive(GTK_WIDGET(d->select_film_roll_button), selected_cnt > 0);
75 }
76 
_image_selection_changed_callback(gpointer instance,dt_lib_module_t * self)77 static void _image_selection_changed_callback(gpointer instance, dt_lib_module_t *self)
78 {
79   _update(self);
80 #ifdef USE_LUA
81   dt_lua_async_call_alien(dt_lua_event_trigger_wrapper,
82     0, NULL,NULL,
83     LUA_ASYNC_TYPENAME,"char*","selection-changed",
84     LUA_ASYNC_DONE);
85 #endif
86 }
87 
_collection_updated_callback(gpointer instance,dt_collection_change_t query_change,dt_collection_properties_t changed_property,gpointer imgs,int next,dt_lib_module_t * self)88 static void _collection_updated_callback(gpointer instance, dt_collection_change_t query_change,
89                                          dt_collection_properties_t changed_property, gpointer imgs, int next,
90                                          dt_lib_module_t *self)
91 {
92   _update(self);
93 }
94 
button_clicked(GtkWidget * widget,gpointer user_data)95 static void button_clicked(GtkWidget *widget, gpointer user_data)
96 {
97   switch(GPOINTER_TO_INT(user_data))
98   {
99     case 0: // all
100       dt_selection_select_all(darktable.selection);
101       break;
102     case 1: // none
103       dt_selection_clear(darktable.selection);
104       break;
105     case 2: // invert
106       dt_selection_invert(darktable.selection);
107       break;
108     case 4: // untouched
109       dt_selection_select_unaltered(darktable.selection);
110       break;
111     default: // case 3: same film roll
112       dt_selection_select_filmroll(darktable.selection);
113   }
114 
115   dt_control_queue_redraw_center();
116 }
117 
position()118 int position()
119 {
120   return 800;
121 }
122 
gui_init(dt_lib_module_t * self)123 void gui_init(dt_lib_module_t *self)
124 {
125   dt_lib_select_t *d = (dt_lib_select_t *)malloc(sizeof(dt_lib_select_t));
126   self->data = d;
127   self->widget = gtk_grid_new();
128   dt_gui_add_help_link(self->widget, dt_get_help_url("select"));
129 
130   GtkGrid *grid = GTK_GRID(self->widget);
131   gtk_grid_set_column_homogeneous(grid, TRUE);
132   int line = 0;
133 
134   d->select_all_button = dt_ui_button_new(_("select all"), _("select all images in current collection"), NULL);
135   gtk_grid_attach(grid, d->select_all_button, 0, line, 1, 1);
136   g_signal_connect(G_OBJECT(d->select_all_button), "clicked", G_CALLBACK(button_clicked), GINT_TO_POINTER(0));
137 
138   d->select_none_button = dt_ui_button_new(_("select none"), _("clear selection"), NULL);
139   gtk_grid_attach(grid, d->select_none_button, 1, line++, 1, 1);
140   g_signal_connect(G_OBJECT(d->select_none_button), "clicked", G_CALLBACK(button_clicked), GINT_TO_POINTER(1));
141 
142   d->select_invert_button = dt_ui_button_new(_("invert selection"), _("select unselected images\nin current collection"), NULL);
143   gtk_grid_attach(grid, d->select_invert_button, 0, line, 1, 1);
144   g_signal_connect(G_OBJECT(d->select_invert_button), "clicked", G_CALLBACK(button_clicked), GINT_TO_POINTER(2));
145 
146   d->select_film_roll_button = dt_ui_button_new(_("select film roll"), _("select all images which are in the same\nfilm roll as the selected images"), NULL);
147   gtk_grid_attach(grid, d->select_film_roll_button, 1, line++, 1, 1);
148   g_signal_connect(G_OBJECT(d->select_film_roll_button), "clicked", G_CALLBACK(button_clicked), GINT_TO_POINTER(3));
149 
150   d->select_untouched_button = dt_ui_button_new(_("select untouched"), _("select untouched images in\ncurrent collection"), NULL);
151   gtk_grid_attach(grid, d->select_untouched_button, 0, line, 2, 1);
152   g_signal_connect(G_OBJECT(d->select_untouched_button), "clicked", G_CALLBACK(button_clicked), GINT_TO_POINTER(4));
153 
154   DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_SELECTION_CHANGED,
155                             G_CALLBACK(_image_selection_changed_callback), self);
156   DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_COLLECTION_CHANGED,
157                             G_CALLBACK(_collection_updated_callback), self);
158 
159   _update(self);
160 }
161 
gui_cleanup(dt_lib_module_t * self)162 void gui_cleanup(dt_lib_module_t *self)
163 {
164   DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_image_selection_changed_callback), self);
165   DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_collection_updated_callback), self);
166   free(self->data);
167   self->data = NULL;
168 }
169 
170 #ifdef USE_LUA
171 typedef struct
172 {
173   const char* key;
174   dt_lib_module_t * self;
175 } lua_callback_data;
176 
177 
lua_button_clicked_cb(lua_State * L)178 static int lua_button_clicked_cb(lua_State* L)
179 {
180   lua_callback_data * data = lua_touserdata(L, 1);
181   dt_lua_module_entry_push(L, "lib", data->self->plugin_name);
182   lua_getiuservalue(L, -1, 1);
183   lua_getfield(L, -1, "callbacks");
184   lua_getfield(L, -1, data->key);
185   lua_pushstring(L, data->key);
186 
187   GList *image = dt_collection_get_all(darktable.collection, -1);
188   lua_newtable(L);
189   int table_index = 1;
190   while(image)
191   {
192     luaA_push(L, dt_lua_image_t, &image->data);
193     lua_seti(L, -2, table_index);
194     table_index++;
195     image = g_list_delete_link(image, image);
196   }
197 
198   lua_call(L ,2, 1);
199 
200   GList *new_selection = NULL;
201   luaL_checktype(L, -1, LUA_TTABLE);
202   lua_pushnil(L);
203   while(lua_next(L, -2) != 0)
204   {
205     /* uses 'key' (at index -2) and 'value' (at index -1) */
206     int imgid;
207     luaA_to(L, dt_lua_image_t, &imgid, -1);
208     new_selection = g_list_prepend(new_selection, GINT_TO_POINTER(imgid));
209     lua_pop(L, 1);
210   }
211   new_selection = g_list_reverse(new_selection);
212   dt_selection_clear(darktable.selection);
213   dt_selection_select_list(darktable.selection, new_selection);
214   g_list_free(new_selection);
215   return 0;
216 
217 }
218 
lua_button_clicked(GtkWidget * widget,gpointer user_data)219 static void lua_button_clicked(GtkWidget *widget, gpointer user_data)
220 {
221   dt_lua_async_call_alien(lua_button_clicked_cb,
222       0, NULL, NULL,
223       LUA_ASYNC_TYPENAME, "void*", user_data,
224       LUA_ASYNC_DONE);
225 }
226 
lua_register_selection(lua_State * L)227 static int lua_register_selection(lua_State *L)
228 {
229   lua_settop(L, 3);
230   dt_lib_module_t *self = lua_touserdata(L, lua_upvalueindex(1));
231   dt_lua_module_entry_push(L, "lib", self->plugin_name);
232   lua_getiuservalue(L, -1, 1);
233   const char* name;
234   const char* key;
235   name = luaL_checkstring(L, 1);
236   key = luaL_checkstring(L, 2);
237   luaL_checktype(L, 3, LUA_TFUNCTION);
238 
239   lua_getfield(L, -1, "callbacks");
240   lua_pushstring(L, name);
241   lua_pushvalue(L, 3);
242   lua_settable(L, -3);
243 
244   GtkWidget* button = gtk_button_new_with_label(key);
245   const char * tooltip = lua_tostring(L, 3);
246   if(tooltip)
247     gtk_widget_set_tooltip_text(button, tooltip);
248 
249   gtk_widget_set_name(button, name);
250   gtk_grid_attach_next_to(GTK_GRID(self->widget), button, NULL, GTK_POS_BOTTOM, 2, 1);
251 
252 
253   lua_callback_data * data = malloc(sizeof(lua_callback_data));
254   data->key = strdup(name);
255   data->self = self;
256   gulong s = g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(lua_button_clicked), data);
257 
258   dt_lua_module_entry_push(L, "lib", self->plugin_name);
259   lua_getiuservalue(L, -1, 1);
260   lua_getfield(L, -1, "signal_handlers");
261   lua_pushstring(L, name);
262   lua_pushinteger(L, s);
263   lua_settable(L, -3);
264 
265   gtk_widget_show_all(self->widget);
266 
267   return 0;
268 }
269 
lua_destroy_selection(lua_State * L)270 static int lua_destroy_selection(lua_State *L)
271 {
272   lua_settop(L, 3);
273   dt_lib_module_t *self = lua_touserdata(L, lua_upvalueindex(1));
274   const char* name = luaL_checkstring(L, 1);
275 
276   // find the button named name
277 
278   GtkWidget* widget = NULL;
279   int row;
280 
281   for(row = 2; (widget = gtk_grid_get_child_at(GTK_GRID(self->widget), 0, row)) != NULL; row++)
282   {
283     if(GTK_IS_BUTTON(widget) && strcmp(gtk_widget_get_name(widget), name) == 0)
284     {
285       // set the callback to nil
286 
287       dt_lua_module_entry_push(L, "lib", self->plugin_name);
288       lua_getiuservalue(L, -1, 1);
289       lua_getfield(L, -1, "callbacks");
290       lua_pushstring(L, name);
291       lua_pushnil(L);
292       lua_settable(L, -3);
293 
294       // disconnect the signal
295 
296       dt_lua_module_entry_push(L, "lib", self->plugin_name);
297       lua_getiuservalue(L, -1, 1);
298       lua_getfield(L, -1, "signal_handlers");
299       lua_pushstring(L, name);
300       lua_gettable(L, -2);
301       gulong handler_id = 0;
302       handler_id = luaL_checkinteger(L, -1);
303       g_signal_handler_disconnect(G_OBJECT(widget), handler_id);
304 
305       // remove the widget
306 
307       gtk_grid_remove_row(GTK_GRID(self->widget), row);
308       break;
309     }
310   }
311 
312   return 0;
313 }
314 
lua_set_selection_sensitive(lua_State * L)315 static int lua_set_selection_sensitive(lua_State *L)
316 {
317   lua_settop(L, 3);
318   dt_lib_module_t *self = lua_touserdata(L, lua_upvalueindex(1));
319   const char* name = luaL_checkstring(L, 1);
320   gboolean sensitive = lua_toboolean(L, 2);
321 
322   // find the button named name
323 
324   GtkWidget* widget = NULL;
325   int row;
326 
327   for(row = 2; (widget = gtk_grid_get_child_at(GTK_GRID(self->widget), 0, row)) != NULL; row++)
328   {
329     if(GTK_IS_BUTTON(widget) && strcmp(gtk_widget_get_name(widget), name) == 0)
330     {
331       gtk_widget_set_sensitive(widget, sensitive);
332       break;
333     }
334   }
335   return 0;
336 }
337 
init(struct dt_lib_module_t * self)338 void init(struct dt_lib_module_t *self)
339 {
340 
341   lua_State *L = darktable.lua_state.state;
342   int my_type = dt_lua_module_entry_get_type(L, "lib", self->plugin_name);
343   lua_pushlightuserdata(L, self);
344   lua_pushcclosure(L, lua_register_selection , 1);
345   dt_lua_gtk_wrap(L);
346   lua_pushcclosure(L, dt_lua_type_member_common, 1);
347   dt_lua_type_register_const_type(L, my_type, "register_selection");
348 
349   lua_pushlightuserdata(L, self);
350   lua_pushcclosure(L, lua_destroy_selection, 1);
351   dt_lua_gtk_wrap(L);
352   lua_pushcclosure(L, dt_lua_type_member_common, 1);
353   dt_lua_type_register_const_type(L, my_type, "destroy_selection");
354 
355   lua_pushlightuserdata(L, self);
356   lua_pushcclosure(L, lua_set_selection_sensitive, 1);
357   dt_lua_gtk_wrap(L);
358   lua_pushcclosure(L, dt_lua_type_member_common, 1);
359   dt_lua_type_register_const_type(L, my_type, "set_sensitive");
360 
361   dt_lua_module_entry_push(L, "lib", self->plugin_name);
362   lua_getiuservalue(L, -1, 1);
363   lua_newtable(L);
364   lua_setfield(L, -2, "callbacks");
365   lua_pop(L, 2);
366 
367   dt_lua_module_entry_push(L, "lib", self->plugin_name);
368   lua_getiuservalue(L, -1, 1);
369   lua_newtable(L);
370   lua_setfield(L, -2, "signal_handlers");
371   lua_pop(L, 2);
372 }
373 #endif
374 
init_key_accels(dt_lib_module_t * self)375 void init_key_accels(dt_lib_module_t *self)
376 {
377   dt_accel_register_lib(self, NC_("accel", "select all"), GDK_KEY_a, GDK_CONTROL_MASK);
378   dt_accel_register_lib(self, NC_("accel", "select none"), GDK_KEY_a, GDK_CONTROL_MASK | GDK_SHIFT_MASK);
379   dt_accel_register_lib(self, NC_("accel", "invert selection"), GDK_KEY_i, GDK_CONTROL_MASK);
380   dt_accel_register_lib(self, NC_("accel", "select film roll"), 0, 0);
381   dt_accel_register_lib(self, NC_("accel", "select untouched"), 0, 0);
382 }
383 
connect_key_accels(dt_lib_module_t * self)384 void connect_key_accels(dt_lib_module_t *self)
385 {
386   dt_lib_select_t *d = (dt_lib_select_t *)self->data;
387 
388   dt_accel_connect_button_lib(self, "select all", GTK_WIDGET(d->select_all_button));
389   dt_accel_connect_button_lib(self, "select none", GTK_WIDGET(d->select_none_button));
390   dt_accel_connect_button_lib(self, "invert selection", GTK_WIDGET(d->select_invert_button));
391   dt_accel_connect_button_lib(self, "select film roll", GTK_WIDGET(d->select_film_roll_button));
392   dt_accel_connect_button_lib(self, "select untouched", GTK_WIDGET(d->select_untouched_button));
393 }
394 
395 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
396 // vim: shiftwidth=2 expandtab tabstop=2 cindent
397 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
398