1 /*
2     This file is part of darktable,
3     Copyright (C) 2019-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 
19 #include "common/collection.h"
20 #include "common/darktable.h"
21 #include "common/debug.h"
22 #include "control/control.h"
23 #include "develop/develop.h"
24 #include "gui/accelerators.h"
25 #include "gui/gtk.h"
26 #include "libs/lib.h"
27 #include "libs/lib_api.h"
28 #include "views/view.h"
29 
30 DT_MODULE(1)
31 
32 typedef enum dt_lib_timeline_zooms_t {
33   DT_LIB_TIMELINE_ZOOM_YEAR = 0,
34   DT_LIB_TIMELINE_ZOOM_4MONTH,
35   DT_LIB_TIMELINE_ZOOM_MONTH,
36   DT_LIB_TIMELINE_ZOOM_10DAY,
37   DT_LIB_TIMELINE_ZOOM_DAY,
38   DT_LIB_TIMELINE_ZOOM_6HOUR,
39   DT_LIB_TIMELINE_ZOOM_HOUR,
40   DT_LIB_TIMELINE_ZOOM_10MINUTE,
41   DT_LIB_TIMELINE_ZOOM_MINUTE
42 } dt_lib_timeline_zooms_t;
43 
44 typedef enum dt_lib_timeline_mode_t {
45   DT_LIB_TIMELINE_MODE_AND = 0,
46   DT_LIB_TIMELINE_MODE_RESET
47 } dt_lib_timeline_mode_t;
48 
49 typedef struct dt_lib_timeline_time_t
50 {
51   int year;
52   int month;
53   int day;
54   int hour;
55   int minute;
56 
57 } dt_lib_timeline_time_t;
58 
59 typedef struct dt_lib_timeline_block_t
60 {
61   gchar *name;
62   int *values;
63   int *collect_values;
64   int values_count;
65   dt_lib_timeline_time_t init;
66   int width;
67 
68 } dt_lib_timeline_block_t;
69 
70 
71 
72 typedef struct dt_lib_timeline_t
73 {
74   dt_lib_timeline_time_t time_mini;
75   dt_lib_timeline_time_t time_maxi;
76   dt_lib_timeline_time_t time_pos;
77 
78   GtkWidget *timeline;
79   cairo_surface_t *surface;
80   int surface_width;
81   int surface_height;
82   int32_t panel_width;
83   int32_t panel_height;
84 
85   GList *blocks;
86   dt_lib_timeline_zooms_t zoom;
87   dt_lib_timeline_zooms_t precision;
88 
89   int start_x;
90   int stop_x;
91   int current_x;
92   dt_lib_timeline_time_t start_t;
93   dt_lib_timeline_time_t stop_t;
94   gboolean has_selection;
95   gboolean selecting;
96   gboolean move_edge;
97 
98   gboolean autoscroll;
99   gboolean in;
100 
101   gboolean size_handle_is_dragging;
102   gint size_handle_x, size_handle_y;
103   int32_t size_handle_height;
104 
105 } dt_lib_timeline_t;
106 
107 
108 
name(dt_lib_module_t * self)109 const char *name(dt_lib_module_t *self)
110 {
111   return _("timeline");
112 }
113 
views(dt_lib_module_t * self)114 const char **views(dt_lib_module_t *self)
115 {
116   static const char *v[] = { "lighttable", NULL };
117   return v;
118 }
119 
container(dt_lib_module_t * self)120 uint32_t container(dt_lib_module_t *self)
121 {
122   return DT_UI_CONTAINER_PANEL_BOTTOM;
123 }
124 
expandable(dt_lib_module_t * self)125 int expandable(dt_lib_module_t *self)
126 {
127   return 0;
128 }
129 
position()130 int position()
131 {
132   return 1002;
133 }
134 
135 // get the number of days in a given month
_time_days_in_month(int year,int month)136 static int _time_days_in_month(int year, int month)
137 {
138   switch(month)
139   {
140     case 2:
141       if((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
142         return 29;
143       else
144         return 28;
145     case 1:
146     case 3:
147     case 5:
148     case 7:
149     case 8:
150     case 10:
151     case 12:
152       return 31;
153     default:
154       return 30;
155   }
156 }
157 
158 // free blocks
_block_free(gpointer data)159 static void _block_free(gpointer data)
160 {
161   dt_lib_timeline_block_t *bloc = (dt_lib_timeline_block_t *)data;
162   if(bloc)
163   {
164     g_free(bloc->name);
165     free(bloc->values);
166     free(bloc->collect_values);
167     free(bloc);
168   }
169 }
170 
171 // get the width of each bar in the graph, depending of the zoom level
_block_get_bar_width(dt_lib_timeline_zooms_t zoom)172 static int _block_get_bar_width(dt_lib_timeline_zooms_t zoom)
173 {
174   if(zoom == DT_LIB_TIMELINE_ZOOM_YEAR)
175     return 10;
176   else if(zoom == DT_LIB_TIMELINE_ZOOM_4MONTH)
177     return 1;
178   else if(zoom == DT_LIB_TIMELINE_ZOOM_MONTH)
179     return 4;
180   else if(zoom == DT_LIB_TIMELINE_ZOOM_10DAY)
181     return 1;
182   else if(zoom == DT_LIB_TIMELINE_ZOOM_DAY)
183     return 5;
184   else if(zoom == DT_LIB_TIMELINE_ZOOM_6HOUR)
185     return 1;
186   else if(zoom == DT_LIB_TIMELINE_ZOOM_HOUR)
187     return 2;
188   return 1; /* dummy value */
189 }
190 // get the number of bar in a block
_block_get_bar_count(dt_lib_timeline_time_t t,dt_lib_timeline_zooms_t zoom)191 static int _block_get_bar_count(dt_lib_timeline_time_t t, dt_lib_timeline_zooms_t zoom)
192 {
193   if(zoom == DT_LIB_TIMELINE_ZOOM_YEAR)
194     return 12;
195   else if(zoom == DT_LIB_TIMELINE_ZOOM_4MONTH)
196   {
197     int ti = (t.month - 1) / 4 * 4 + 1;
198     return _time_days_in_month(t.year, ti) + _time_days_in_month(t.year, ti + 1)
199            + _time_days_in_month(t.year, ti + 2) + _time_days_in_month(t.year, ti + 3);
200   }
201   else if(zoom == DT_LIB_TIMELINE_ZOOM_MONTH)
202     return _time_days_in_month(t.year, t.month);
203   else if(zoom == DT_LIB_TIMELINE_ZOOM_10DAY)
204     return 120;
205   else if(zoom == DT_LIB_TIMELINE_ZOOM_DAY)
206     return 24;
207   else if(zoom == DT_LIB_TIMELINE_ZOOM_6HOUR)
208     return 120;
209   else if(zoom == DT_LIB_TIMELINE_ZOOM_HOUR)
210     return 60;
211   return 1; /* dummy value */
212 }
213 
_block_get_bar_height(int nb,int max_height)214 static int _block_get_bar_height(int nb, int max_height)
215 {
216   // we want height to be between 0 and max_height
217   // small value should have visible height
218   return max_height * (1.0 - 2.0 / sqrtf(nb + 4.0));
219 }
220 
221 // init time
_time_init()222 static dt_lib_timeline_time_t _time_init()
223 {
224   dt_lib_timeline_time_t tt = { 0 };
225   // we don't want 0 values for month and day
226   tt.month = tt.day = 1;
227   return tt;
228 }
229 
230 // compare times
_time_compare_at_zoom(dt_lib_timeline_time_t t1,dt_lib_timeline_time_t t2,dt_lib_timeline_zooms_t zoom)231 static int _time_compare_at_zoom(dt_lib_timeline_time_t t1, dt_lib_timeline_time_t t2, dt_lib_timeline_zooms_t zoom)
232 {
233   if(t1.year != t2.year) return (t1.year - t2.year);
234   if(zoom >= DT_LIB_TIMELINE_ZOOM_YEAR)
235   {
236     if(t1.month != t2.month) return (t1.month - t2.month);
237     if(zoom >= DT_LIB_TIMELINE_ZOOM_4MONTH)
238     {
239       if(t1.day != t2.day) return (t1.day - t2.day);
240       if(zoom >= DT_LIB_TIMELINE_ZOOM_10DAY)
241       {
242         if(t1.hour / 2 != t2.hour / 2) return (t1.hour / 2 - t2.hour / 2);
243         if(zoom >= DT_LIB_TIMELINE_ZOOM_DAY)
244         {
245           if(t1.hour != t2.hour) return (t1.hour - t2.hour);
246           if(zoom >= DT_LIB_TIMELINE_ZOOM_6HOUR)
247           {
248             if(t1.minute / 3 != t2.minute / 3) return (t1.minute / 3 - t2.minute / 3);
249             if(zoom >= DT_LIB_TIMELINE_ZOOM_HOUR)
250             {
251               if(t1.minute != t2.minute) return (t1.minute - t2.minute);
252             }
253           }
254         }
255       }
256     }
257   }
258 
259   return 0;
260 }
_time_compare(dt_lib_timeline_time_t t1,dt_lib_timeline_time_t t2)261 static int _time_compare(dt_lib_timeline_time_t t1, dt_lib_timeline_time_t t2)
262 {
263   if(t1.year != t2.year) return (t1.year - t2.year);
264   if(t1.month != t2.month) return (t1.month - t2.month);
265   if(t1.day != t2.day) return (t1.day - t2.day);
266   if(t1.hour != t2.hour) return (t1.hour - t2.hour);
267   if(t1.minute != t2.minute) return (t1.minute - t2.minute);
268 
269   return 0;
270 }
271 
272 // add/subtract value to a time at certain level
_time_add(dt_lib_timeline_time_t * t,int val,dt_lib_timeline_zooms_t level)273 static void _time_add(dt_lib_timeline_time_t *t, int val, dt_lib_timeline_zooms_t level)
274 {
275   if(level == DT_LIB_TIMELINE_ZOOM_YEAR)
276   {
277     t->year += val;
278   }
279   else if(level == DT_LIB_TIMELINE_ZOOM_4MONTH)
280   {
281     t->month += val * 4;
282     while(t->month > 12)
283     {
284       t->year++;
285       t->month -= 12;
286     }
287     while(t->month < 1)
288     {
289       t->year--;
290       t->month += 12;
291     }
292   }
293   else if(level == DT_LIB_TIMELINE_ZOOM_MONTH)
294   {
295     t->month += val;
296     while(t->month > 12)
297     {
298       t->year++;
299       t->month -= 12;
300     }
301     while(t->month < 1)
302     {
303       t->year--;
304       t->month += 12;
305     }
306   }
307   else if(level == DT_LIB_TIMELINE_ZOOM_10DAY)
308   {
309     t->day += val * 10;
310     while(t->day > _time_days_in_month(t->year, t->month))
311     {
312       t->day -= _time_days_in_month(t->year, t->month);
313       _time_add(t, 1, DT_LIB_TIMELINE_ZOOM_MONTH);
314     }
315     while(t->day < 1)
316     {
317       _time_add(t, -1, DT_LIB_TIMELINE_ZOOM_MONTH);
318       t->day += _time_days_in_month(t->year, t->month);
319     }
320   }
321   else if(level == DT_LIB_TIMELINE_ZOOM_DAY)
322   {
323     t->day += val;
324     while(t->day > _time_days_in_month(t->year, t->month))
325     {
326       t->day -= _time_days_in_month(t->year, t->month);
327       _time_add(t, 1, DT_LIB_TIMELINE_ZOOM_MONTH);
328     }
329     while(t->day < 1)
330     {
331       _time_add(t, -1, DT_LIB_TIMELINE_ZOOM_MONTH);
332       t->day += _time_days_in_month(t->year, t->month);
333     }
334   }
335   else if(level == DT_LIB_TIMELINE_ZOOM_6HOUR)
336   {
337     t->hour += val * 6;
338     while(t->hour > 23)
339     {
340       t->hour -= 24;
341       _time_add(t, 1, DT_LIB_TIMELINE_ZOOM_DAY);
342     }
343     while(t->hour < 0)
344     {
345       t->hour += 24;
346       _time_add(t, -1, DT_LIB_TIMELINE_ZOOM_DAY);
347     }
348   }
349   else if(level == DT_LIB_TIMELINE_ZOOM_HOUR)
350   {
351     t->hour += val;
352     while(t->hour > 23)
353     {
354       t->hour -= 24;
355       _time_add(t, 1, DT_LIB_TIMELINE_ZOOM_DAY);
356     }
357     while(t->hour < 0)
358     {
359       t->hour += 24;
360       _time_add(t, -1, DT_LIB_TIMELINE_ZOOM_DAY);
361     }
362   }
363   else if(level == DT_LIB_TIMELINE_ZOOM_MINUTE)
364   {
365     t->minute += val;
366     while(t->minute > 59)
367     {
368       t->minute -= 60;
369       _time_add(t, 1, DT_LIB_TIMELINE_ZOOM_HOUR);
370     }
371     while(t->minute < 0)
372     {
373       t->minute += 60;
374       _time_add(t, -1, DT_LIB_TIMELINE_ZOOM_HOUR);
375     }
376   }
377 
378   // fix for date with year set to 0 (bug ?)
379   if(t->year < 0) t->year = 0;
380 }
381 
382 // retrieve the date from the position at given zoom level
_time_get_from_pos(int pos,dt_lib_timeline_t * strip)383 static dt_lib_timeline_time_t _time_get_from_pos(int pos, dt_lib_timeline_t *strip)
384 {
385   dt_lib_timeline_time_t tt = _time_init();
386 
387   int x = 0;
388   for(const GList *bl = strip->blocks; bl; bl = g_list_next(bl))
389   {
390     dt_lib_timeline_block_t *blo = bl->data;
391     if(pos < x + blo->width)
392     {
393       tt.year = blo->init.year;
394       if(strip->zoom >= DT_LIB_TIMELINE_ZOOM_4MONTH) tt.month = blo->init.month;
395       if(strip->zoom >= DT_LIB_TIMELINE_ZOOM_10DAY) tt.day = blo->init.day;
396       if(strip->zoom >= DT_LIB_TIMELINE_ZOOM_6HOUR) tt.hour = blo->init.hour;
397 
398       if(strip->zoom == DT_LIB_TIMELINE_ZOOM_YEAR)
399       {
400         tt.month = (pos - x) / _block_get_bar_width(strip->zoom) + 1;
401         if(tt.month < 1) tt.month = 1;
402       }
403       else if(strip->zoom == DT_LIB_TIMELINE_ZOOM_4MONTH)
404       {
405         int nb = (pos - x) / _block_get_bar_width(strip->zoom) + 1;
406         _time_add(&tt, nb, DT_LIB_TIMELINE_ZOOM_DAY);
407         if(tt.day < 1) tt.day = 1;
408       }
409       else if(strip->zoom == DT_LIB_TIMELINE_ZOOM_MONTH)
410       {
411         tt.day = (pos - x) / _block_get_bar_width(strip->zoom) + 1;
412         if(tt.day < 1) tt.day = 1;
413       }
414       else if(strip->zoom == DT_LIB_TIMELINE_ZOOM_10DAY)
415       {
416         int nb = (pos - x) / _block_get_bar_width(strip->zoom) + 1;
417         _time_add(&tt, nb * 2, DT_LIB_TIMELINE_ZOOM_HOUR);
418         if(tt.hour < 0) tt.hour = 0;
419       }
420       else if(strip->zoom == DT_LIB_TIMELINE_ZOOM_DAY)
421       {
422         tt.hour = (pos - x) / _block_get_bar_width(strip->zoom) + 1;
423         if(tt.hour < 0) tt.hour = 0;
424       }
425       else if(strip->zoom == DT_LIB_TIMELINE_ZOOM_6HOUR)
426       {
427         int nb = (pos - x) / _block_get_bar_width(strip->zoom) + 1;
428         _time_add(&tt, nb * 3, DT_LIB_TIMELINE_ZOOM_MINUTE);
429         if(tt.minute < 0) tt.minute = 0;
430       }
431       else if(strip->zoom == DT_LIB_TIMELINE_ZOOM_HOUR)
432       {
433         int nb = (pos - x) / _block_get_bar_width(strip->zoom) + 1;
434         _time_add(&tt, nb, DT_LIB_TIMELINE_ZOOM_MINUTE);
435         if(tt.minute < 0) tt.minute = 0;
436       }
437 
438       return tt;
439     }
440     x += blo->width + 2;
441   }
442 
443   return tt;
444 }
445 
_time_compute_offset_for_zoom(int pos,dt_lib_timeline_t * strip,dt_lib_timeline_zooms_t new_zoom)446 static dt_lib_timeline_time_t _time_compute_offset_for_zoom(int pos, dt_lib_timeline_t *strip,
447                                                             dt_lib_timeline_zooms_t new_zoom)
448 {
449   if(new_zoom == strip->zoom) return strip->time_pos;
450 
451   dt_lib_timeline_time_t tt = _time_get_from_pos(pos, strip);
452 
453   // we search the number of the bloc under pos
454   int bloc_nb = 0;
455   int x = 0;
456   GList *bl;
457   for(bl = strip->blocks; bl; bl = g_list_next(bl))
458   {
459     dt_lib_timeline_block_t *blo = bl->data;
460     if(pos < x + blo->width) break;
461     x += blo->width + 2;
462     bloc_nb++;
463   }
464   if(!bl)
465   {
466     // we are outside the timeline
467   }
468 
469   // we translate to the new date_pos at new_zoom level
470   _time_add(&tt, -bloc_nb, new_zoom);
471 
472   // we need to verify that we are not out of the bounds
473   if(_time_compare(tt, strip->time_mini) < 0) tt = strip->time_mini;
474   return tt;
475 }
476 
_time_format_for_ui(dt_lib_timeline_time_t t,dt_lib_timeline_zooms_t zoom)477 static gchar *_time_format_for_ui(dt_lib_timeline_time_t t, dt_lib_timeline_zooms_t zoom)
478 {
479   if(zoom == DT_LIB_TIMELINE_ZOOM_YEAR)
480   {
481     return g_strdup_printf("%04d", t.year);
482   }
483   else if(zoom == DT_LIB_TIMELINE_ZOOM_4MONTH)
484   {
485     int x = (t.month - 1) / 4 * 4 + 1; // This is NOT a no-op (rounding)
486     return g_strdup_printf("(%02d-%02d)/%04d", x, x + 3, t.year);
487   }
488   else if(zoom == DT_LIB_TIMELINE_ZOOM_MONTH)
489   {
490     return g_strdup_printf("%02d/%04d", t.month, t.year);
491   }
492   else if(zoom == DT_LIB_TIMELINE_ZOOM_10DAY)
493   {
494     int x = (t.day - 1) / 10 * 10 + 1; // This is NOT a no-op (rounding)
495     int x2 = x + 9;
496     if(x2 == 30) x2 = _time_days_in_month(t.year, t.month);
497     return g_strdup_printf("(%02d-%02d)/%02d/%02d", x, x2, t.month, t.year % 100);
498   }
499   else if(zoom == DT_LIB_TIMELINE_ZOOM_DAY)
500   {
501     return g_strdup_printf("%02d/%02d/%02d", t.day, t.month, t.year % 100);
502   }
503   else if(zoom == DT_LIB_TIMELINE_ZOOM_6HOUR)
504   {
505     return g_strdup_printf("%02d/%02d/%02d (h%02d-%02d)", t.day, t.month, t.year % 100, t.hour / 6 * 6,
506                            t.hour / 6 * 6 + 5);
507   }
508   else if(zoom == DT_LIB_TIMELINE_ZOOM_HOUR)
509   {
510     return g_strdup_printf("%02d/%02d/%02d h%02d", t.day, t.month, t.year % 100, t.hour);
511   }
512   else if(zoom == DT_LIB_TIMELINE_ZOOM_10MINUTE)
513   {
514     return g_strdup_printf("%02d/%02d/%02d %02dh(%02d-%02d)", t.day, t.month, t.year % 100, t.hour,
515                            t.minute / 10 * 10, t.minute / 10 * 10 + 9);
516   }
517   else if(zoom == DT_LIB_TIMELINE_ZOOM_MINUTE)
518   {
519     return g_strdup_printf("%02d/%02d/%02d %02d:%02d", t.day, t.month, t.year % 100, t.hour, t.minute);
520   }
521 
522   return NULL;
523 }
_time_format_for_db(dt_lib_timeline_time_t t,dt_lib_timeline_zooms_t zoom,gboolean full)524 static gchar *_time_format_for_db(dt_lib_timeline_time_t t, dt_lib_timeline_zooms_t zoom, gboolean full)
525 {
526   if(zoom == DT_LIB_TIMELINE_ZOOM_YEAR)
527   {
528     if(full)
529       return g_strdup_printf("%04d:01:01 00:00:00", t.year);
530     else
531       return g_strdup_printf("%04d", t.year);
532   }
533   else if(zoom == DT_LIB_TIMELINE_ZOOM_4MONTH || zoom == DT_LIB_TIMELINE_ZOOM_MONTH)
534   {
535     if(full)
536       return g_strdup_printf("%04d:%02d:01 00:00:00", t.year, t.month);
537     else
538       return g_strdup_printf("%04d:%02d", t.year, t.month);
539   }
540   else if(zoom == DT_LIB_TIMELINE_ZOOM_10DAY || zoom == DT_LIB_TIMELINE_ZOOM_DAY)
541   {
542     if(full)
543       return g_strdup_printf("%04d:%02d:%02d 00:00:00", t.year, t.month, t.day);
544     else
545       return g_strdup_printf("%04d:%02d:%02d", t.year, t.month, t.day);
546   }
547   else if(zoom == DT_LIB_TIMELINE_ZOOM_6HOUR || zoom == DT_LIB_TIMELINE_ZOOM_HOUR)
548   {
549     if(full)
550       return g_strdup_printf("%04d:%02d:%02d %02d:00:00", t.year, t.month, t.day, t.hour);
551     else
552       return g_strdup_printf("%04d:%02d:%02d %02d", t.year, t.month, t.day, t.hour);
553   }
554   else if(zoom == DT_LIB_TIMELINE_ZOOM_10MINUTE || zoom == DT_LIB_TIMELINE_ZOOM_MINUTE)
555   {
556     if(full)
557       return g_strdup_printf("%04d:%02d:%02d %02d:%02d:00", t.year, t.month, t.day, t.hour, t.minute);
558     else
559       return g_strdup_printf("%04d:%02d:%02d %02d:%02d", t.year, t.month, t.day, t.hour, t.minute);
560   }
561 
562   return NULL;
563 }
564 
565 // get all the datetimes from the database
_time_read_bounds_from_db(dt_lib_module_t * self)566 static gboolean _time_read_bounds_from_db(dt_lib_module_t *self)
567 {
568   dt_lib_timeline_t *strip = (dt_lib_timeline_t *)self->data;
569 
570   sqlite3_stmt *stmt;
571   const char *query = "SELECT datetime_taken FROM main.images WHERE LENGTH(datetime_taken) = 19 AND "
572                       "datetime_taken > '0001:01:01 00:00:00' COLLATE NOCASE ORDER BY "
573                       "datetime_taken ASC LIMIT 1";
574   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
575 
576   if(sqlite3_step(stmt) == SQLITE_ROW)
577   {
578     const char *tx = (const char *)sqlite3_column_text(stmt, 0);
579     strip->time_mini.year = MAX(strtol(tx, NULL, 10), 0);
580     strip->time_mini.month = CLAMP(strtol(tx + 5, NULL, 10), 1, 12);
581     strip->time_mini.day
582         = CLAMP(strtol(tx + 8, NULL, 10), 1, _time_days_in_month(strip->time_mini.year, strip->time_mini.month));
583     strip->time_mini.hour = CLAMP(strtol(tx + 11, NULL, 10), 0, 23);
584     strip->time_mini.minute = CLAMP(strtol(tx + 14, NULL, 10), 0, 59);
585     strip->has_selection = TRUE;
586   }
587   else
588     strip->has_selection = FALSE;
589   sqlite3_finalize(stmt);
590 
591   const char *query2 = "SELECT datetime_taken FROM main.images WHERE LENGTH(datetime_taken) = 19 AND "
592                        "datetime_taken > '0001:01:01 00:00:00' COLLATE NOCASE ORDER BY "
593                        "datetime_taken DESC LIMIT 1";
594   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query2, -1, &stmt, NULL);
595 
596   if(sqlite3_step(stmt) == SQLITE_ROW)
597   {
598     const char *tx = (const char *)sqlite3_column_text(stmt, 0);
599     strip->time_maxi.year = MAX(strtol(tx, NULL, 10), 0);
600     strip->time_maxi.month = CLAMP(strtol(tx + 5, NULL, 10), 1, 12);
601     strip->time_maxi.day
602         = CLAMP(strtol(tx + 8, NULL, 10), 1, _time_days_in_month(strip->time_mini.year, strip->time_mini.month));
603     strip->time_maxi.hour = CLAMP(strtol(tx + 11, NULL, 10), 0, 23);
604     strip->time_maxi.minute = CLAMP(strtol(tx + 14, NULL, 10), 0, 59);
605   }
606   sqlite3_finalize(stmt);
607 
608   return TRUE;
609 }
610 
611 // get all the datetimes from the actual collection
_time_read_bounds_from_collection(dt_lib_module_t * self)612 static gboolean _time_read_bounds_from_collection(dt_lib_module_t *self)
613 {
614   dt_lib_timeline_t *strip = (dt_lib_timeline_t *)self->data;
615 
616   sqlite3_stmt *stmt;
617   const char *query = "SELECT db.datetime_taken FROM main.images AS db, memory.collected_images AS col WHERE "
618                       "db.id=col.imgid AND LENGTH(db.datetime_taken) = 19 AND db.datetime_taken > '0001:01:01 "
619                       "00:00:00' COLLATE NOCASE ORDER BY "
620                       "db.datetime_taken ASC LIMIT 1";
621   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
622 
623   if(sqlite3_step(stmt) == SQLITE_ROW)
624   {
625     const char *tx = (const char *)sqlite3_column_text(stmt, 0);
626     strip->start_t.year = MAX(strtol(tx, NULL, 10), 0);
627     strip->start_t.month = CLAMP(strtol(tx + 5, NULL, 10), 1, 12);
628     strip->start_t.day
629         = CLAMP(strtol(tx + 8, NULL, 10), 1, _time_days_in_month(strip->time_mini.year, strip->time_mini.month));
630     strip->start_t.hour = CLAMP(strtol(tx + 11, NULL, 10), 0, 23);
631     strip->start_t.minute = CLAMP(strtol(tx + 14, NULL, 10), 0, 59);
632     strip->has_selection = TRUE;
633   }
634   else
635     strip->has_selection = FALSE;
636   sqlite3_finalize(stmt);
637 
638   const char *query2 = "SELECT db.datetime_taken FROM main.images AS db, memory.collected_images AS col WHERE "
639                        "db.id=col.imgid AND LENGTH(db.datetime_taken) = 19 AND db.datetime_taken > '0001:01:01 "
640                        "00:00:00' COLLATE NOCASE ORDER BY "
641                        "db.datetime_taken DESC LIMIT 1";
642   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query2, -1, &stmt, NULL);
643 
644   if(sqlite3_step(stmt) == SQLITE_ROW)
645   {
646     const char *tx = (const char *)sqlite3_column_text(stmt, 0);
647     strip->stop_t.year = MAX(strtol(tx, NULL, 10), 0);
648     strip->stop_t.month = CLAMP(strtol(tx + 5, NULL, 10), 1, 12);
649     strip->stop_t.day
650         = CLAMP(strtol(tx + 8, NULL, 10), 1, _time_days_in_month(strip->time_mini.year, strip->time_mini.month));
651     strip->stop_t.hour = CLAMP(strtol(tx + 11, NULL, 10), 0, 23);
652     strip->stop_t.minute = CLAMP(strtol(tx + 14, NULL, 10), 0, 59);
653   }
654   sqlite3_finalize(stmt);
655 
656   return TRUE;
657 }
658 
659 
_time_get_from_db(gchar * tx,gboolean last)660 static dt_lib_timeline_time_t _time_get_from_db(gchar *tx, gboolean last)
661 {
662   dt_lib_timeline_time_t tt = _time_init();
663   if(strlen(tx) > 3) tt.year = CLAMP(strtol(tx, NULL, 10), 0, 4000);
664   if(strlen(tx) > 6) tt.month = CLAMP(strtol(tx + 5, NULL, 10), 1, 12);
665   if(strlen(tx) > 9) tt.day = CLAMP(strtol(tx + 8, NULL, 10), 1, _time_days_in_month(tt.year, tt.month));
666   if(strlen(tx) > 12) tt.hour = CLAMP(strtol(tx + 11, NULL, 10), 0, 23);
667   if(strlen(tx) > 15) tt.minute = CLAMP(strtol(tx + 14, NULL, 10), 0, 59);
668 
669   // if we need to complete a non full date to get the last one ("2012" > "2012:12:31 23:59")
670   if(last)
671   {
672     if(strlen(tx) < 16)
673     {
674       tt.minute = 59;
675       if(strlen(tx) < 13)
676       {
677         tt.hour = 23;
678         if(strlen(tx) < 7)
679         {
680           tt.month = 12;
681         }
682         if(strlen(tx) < 10)
683         {
684           tt.day = _time_days_in_month(tt.year, tt.month);
685         }
686       }
687     }
688   }
689   return tt;
690 }
691 
692 // get the time of the first block of the strip in order to show the selection
_selection_scroll_to(dt_lib_timeline_time_t t,dt_lib_timeline_t * strip)693 static dt_lib_timeline_time_t _selection_scroll_to(dt_lib_timeline_time_t t, dt_lib_timeline_t *strip)
694 {
695   dt_lib_timeline_time_t tt = t;
696   int nb = strip->panel_width / 122;
697 
698   for(int i = 0; i < nb; i++)
699   {
700     // we ensure that we are not before the strip bound
701     if(_time_compare(tt, strip->time_mini) <= 0) return strip->time_mini;
702 
703     // and we don't want to display blocks after the bounds too
704     dt_lib_timeline_time_t ttt = tt;
705     _time_add(&ttt, nb - 1, strip->zoom);
706     if(_time_compare(ttt, strip->time_maxi) <= 0) return tt;
707 
708     // we test the previous date
709     _time_add(&tt, -1, strip->zoom);
710   }
711   // if we are here that means we fail to scroll... why ?
712   return t;
713 }
714 
715 // computes blocks at the current zoom level
_block_get_at_zoom(dt_lib_module_t * self,int width)716 static int _block_get_at_zoom(dt_lib_module_t *self, int width)
717 {
718   dt_lib_timeline_t *strip = (dt_lib_timeline_t *)self->data;
719 
720   // we erase previous blocks if any
721   if(strip->blocks)
722   {
723     g_list_free_full(strip->blocks, _block_free);
724     strip->blocks = NULL;
725   }
726 
727   int w = 0;
728 
729   // if selection start/stop if lower than the begiining of the strip
730   if(_time_compare_at_zoom(strip->start_t, strip->time_pos, strip->zoom) < 0) strip->start_x = -2;
731   if(_time_compare_at_zoom(strip->stop_t, strip->time_pos, strip->zoom) < 0) strip->stop_x = -1;
732 
733   sqlite3_stmt *stmt;
734   gchar *query = g_strdup_printf("SELECT db.datetime_taken, col.imgid FROM main.images AS db LEFT JOIN "
735                                  "memory.collected_images AS col ON db.id=col.imgid WHERE "
736                                  "LENGTH(db.datetime_taken) = 19 AND "
737                                  "db.datetime_taken > '%s' COLLATE NOCASE ORDER BY db.datetime_taken ASC",
738                                  _time_format_for_db(strip->time_pos, strip->zoom, TRUE));
739   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL);
740 
741   char *tx = "";
742   int id = 0;
743   int stat = sqlite3_step(stmt);
744   if(stat == SQLITE_ROW)
745   {
746     tx = (char *)sqlite3_column_text(stmt, 0);
747     id = sqlite3_column_int(stmt, 1);
748   }
749   else
750     return 0;
751 
752   dt_lib_timeline_time_t tt = strip->time_pos;
753   // we round correctly this date
754   if(strip->zoom <= DT_LIB_TIMELINE_ZOOM_HOUR)
755   {
756     tt.minute = 0;
757     if(strip->zoom <= DT_LIB_TIMELINE_ZOOM_6HOUR)
758     {
759       tt.hour = tt.hour / 6 * 6;
760       if(strip->zoom <= DT_LIB_TIMELINE_ZOOM_DAY)
761       {
762         tt.hour = 0;
763         if(strip->zoom <= DT_LIB_TIMELINE_ZOOM_10DAY)
764         {
765           tt.day = (tt.day - 1) / 10 * 10 + 1;
766           if(strip->zoom <= DT_LIB_TIMELINE_ZOOM_MONTH)
767           {
768             tt.day = 1;
769             if(strip->zoom <= DT_LIB_TIMELINE_ZOOM_4MONTH)
770             {
771               tt.month = (tt.month - 1) / 4 * 4 + 1;
772               if(strip->zoom <= DT_LIB_TIMELINE_ZOOM_YEAR)
773               {
774                 tt.month = 1;
775               }
776             }
777           }
778         }
779       }
780     }
781   }
782 
783   while(TRUE)
784   {
785     dt_lib_timeline_block_t *bloc = (dt_lib_timeline_block_t *)calloc(1, sizeof(dt_lib_timeline_block_t));
786     bloc->name = _time_format_for_ui(tt, strip->zoom);
787     bloc->init = tt;
788     bloc->values_count = _block_get_bar_count(tt, strip->zoom);
789     bloc->values = (int *)calloc(bloc->values_count, sizeof(int));
790     bloc->collect_values = (int *)calloc(bloc->values_count, sizeof(int));
791     bloc->width = bloc->values_count * _block_get_bar_width(strip->zoom);
792 
793     if(strip->zoom == DT_LIB_TIMELINE_ZOOM_YEAR)
794       tt.month = 1;
795     else if(strip->zoom == DT_LIB_TIMELINE_ZOOM_4MONTH || strip->zoom == DT_LIB_TIMELINE_ZOOM_MONTH)
796       tt.day = 1;
797     else if(strip->zoom == DT_LIB_TIMELINE_ZOOM_10DAY || strip->zoom == DT_LIB_TIMELINE_ZOOM_DAY)
798       tt.hour = 0;
799     else if(strip->zoom == DT_LIB_TIMELINE_ZOOM_6HOUR || strip->zoom == DT_LIB_TIMELINE_ZOOM_HOUR)
800       tt.minute = 0;
801     // we count the number of photos per month
802     for(int i = 0; i < bloc->values_count; i++)
803     {
804 
805       // if it's the selection start/stop time, we set the x value accordindgly
806       if(_time_compare_at_zoom(strip->start_t, tt, strip->zoom) == 0)
807         strip->start_x = w + i * _block_get_bar_width(strip->zoom);
808       if(_time_compare_at_zoom(strip->stop_t, tt, strip->zoom) == 0)
809         strip->stop_x = w + (i + 1) * _block_get_bar_width(strip->zoom);
810       // and we count how many photos we have for this time
811       while(stat == SQLITE_ROW && _time_compare_at_zoom(tt, _time_get_from_db(tx, FALSE), strip->zoom) == 0)
812       {
813         bloc->values[i]++;
814         if(id > 0) bloc->collect_values[i]++;
815         stat = sqlite3_step(stmt);
816         tx = (char *)sqlite3_column_text(stmt, 0);
817         id = sqlite3_column_int(stmt, 1);
818       }
819 
820       // and we jump to next date
821       // if (i+1 >= bloc->values_count) break;
822       if(strip->zoom == DT_LIB_TIMELINE_ZOOM_YEAR)
823         _time_add(&tt, 1, DT_LIB_TIMELINE_ZOOM_MONTH);
824       else if(strip->zoom == DT_LIB_TIMELINE_ZOOM_4MONTH || strip->zoom == DT_LIB_TIMELINE_ZOOM_MONTH)
825         _time_add(&tt, 1, DT_LIB_TIMELINE_ZOOM_DAY);
826       else if(strip->zoom == DT_LIB_TIMELINE_ZOOM_10DAY)
827         _time_add(&tt, 2, DT_LIB_TIMELINE_ZOOM_HOUR);
828       else if(strip->zoom == DT_LIB_TIMELINE_ZOOM_DAY)
829         _time_add(&tt, 1, DT_LIB_TIMELINE_ZOOM_HOUR);
830       else if(strip->zoom == DT_LIB_TIMELINE_ZOOM_6HOUR)
831         _time_add(&tt, 3, DT_LIB_TIMELINE_ZOOM_MINUTE);
832       else if(strip->zoom == DT_LIB_TIMELINE_ZOOM_HOUR)
833         _time_add(&tt, 1, DT_LIB_TIMELINE_ZOOM_MINUTE);
834     }
835     strip->blocks = g_list_append(strip->blocks, bloc);
836 
837     w += bloc->width + 2;
838     if(w > width || stat != SQLITE_ROW)
839     {
840       // if selection start/stop times are greater than the last time
841       if(_time_compare_at_zoom(strip->start_t, tt, strip->zoom) >= 0) strip->start_x = strip->panel_width + 1;
842       if(_time_compare_at_zoom(strip->stop_t, tt, strip->zoom) >= 0) strip->stop_x = strip->panel_width + 2;
843       break;
844     }
845   }
846   sqlite3_finalize(stmt);
847   g_free(query);
848 
849   // and we return the width of the strip
850   return w;
851 }
852 
_time_is_visible(dt_lib_timeline_time_t t,dt_lib_timeline_t * strip)853 static gboolean _time_is_visible(dt_lib_timeline_time_t t, dt_lib_timeline_t *strip)
854 {
855   // first case, the date is before the strip
856   if(_time_compare_at_zoom(t, strip->time_pos, strip->zoom) < 0) return FALSE;
857 
858   // now the end of the visible strip
859   // if the date is in the last block, we consider it's outside, because the last block can be partially hidden
860   GList *bl = g_list_last(strip->blocks);
861   if(bl)
862   {
863     dt_lib_timeline_block_t *blo = bl->data;
864     if(_time_compare_at_zoom(t, blo->init, strip->zoom) > 0) return FALSE;
865   }
866 
867   return TRUE;
868 }
869 
_lib_timeline_collection_changed(gpointer instance,dt_collection_change_t query_change,dt_collection_properties_t changed_property,gpointer imgs,int next,gpointer user_data)870 static void _lib_timeline_collection_changed(gpointer instance, dt_collection_change_t query_change,
871                                              dt_collection_properties_t changed_property, gpointer imgs, int next,
872                                              gpointer user_data)
873 {
874   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
875   dt_lib_timeline_t *strip = (dt_lib_timeline_t *)self->data;
876 
877   // we read the collect bounds
878   _time_read_bounds_from_collection(self);
879 
880   // if the start in not visible, we recompute the start of the strip
881   if(!_time_is_visible(strip->start_t, strip))
882   {
883     strip->time_pos = _selection_scroll_to(strip->start_t, strip);
884   }
885 
886   // in any case we redraw the strip (to reflect any selected image change)
887   cairo_surface_destroy(strip->surface);
888   strip->surface = NULL;
889   gtk_widget_queue_draw(strip->timeline);
890 }
891 
892 
_timespec_has_date_only(const char * const spec)893 static gboolean _timespec_has_date_only(const char *const spec)
894 {
895   // spec could be "YYYY:MM", "YYYY:MM:DD", "YYYY:MM:DD HH", etc.
896   return strlen(spec) <= 10; // is string YYYY:MM:DD or shorter?
897 }
898 
899 // add the selected portions to the collect
_selection_collect(dt_lib_timeline_t * strip,dt_lib_timeline_mode_t mode)900 static void _selection_collect(dt_lib_timeline_t *strip, dt_lib_timeline_mode_t mode)
901 {
902   // if the last rule is date-time type or is empty, we modify it
903   // else we add a new rule date-time rule
904 
905   int new_rule = 0;
906   const int nb_rules = dt_conf_get_int("plugins/lighttable/collect/num_rules");
907   if(nb_rules > 0 && mode != DT_LIB_TIMELINE_MODE_RESET)
908   {
909     char confname[200] = { 0 };
910     snprintf(confname, sizeof(confname), "plugins/lighttable/collect/item%1d", nb_rules - 1);
911     dt_collection_properties_t prop = dt_conf_get_int(confname);
912     snprintf(confname, sizeof(confname), "plugins/lighttable/collect/mode%1d", nb_rules - 1);
913     int rmode = dt_conf_get_int(confname);
914     snprintf(confname, sizeof(confname), "plugins/lighttable/collect/string%1d", nb_rules - 1);
915     gchar *string = dt_conf_get_string(confname);
916     string = g_strstrip(string);
917     if(((prop == DT_COLLECTION_PROP_TIME || prop == DT_COLLECTION_PROP_DAY) && rmode == 0)
918        || !string || strlen(string) == 0 || g_strcmp0(string, "%") == 0)
919       new_rule = nb_rules - 1;
920     else
921       new_rule = nb_rules;
922     g_free(string);
923   }
924 
925   // we construct the rule
926   gchar *coll = NULL;
927   gboolean date_only = FALSE;
928   if(strip->start_x == strip->stop_x)
929   {
930     coll = _time_format_for_db(strip->start_t, (strip->zoom + 1) / 2 * 2 + 2, FALSE);
931     date_only = _timespec_has_date_only(coll);
932   }
933   else
934   {
935     dt_lib_timeline_time_t start = strip->start_t;
936     dt_lib_timeline_time_t stop = strip->stop_t;
937     if(strip->start_x > strip->stop_x)
938     {
939       // we exchange the values
940       start = strip->stop_t;
941       stop = strip->start_t;
942     }
943     gchar *d1 = _time_format_for_db(start, (strip->zoom + 1) / 2 * 2 + 2, FALSE);
944     gchar *d2 = _time_format_for_db(stop, (strip->zoom + 1) / 2 * 2 + 2, FALSE);
945     if(d1 && d2)
946     {
947       coll = g_strdup_printf("[%s;%s]", d1, d2);
948       date_only = _timespec_has_date_only(d1) && _timespec_has_date_only(d2);
949     }
950     g_free(d1);
951     g_free(d2);
952   }
953 
954   if(coll)
955   {
956     dt_conf_set_int("plugins/lighttable/collect/num_rules", new_rule + 1);
957     char confname[200] = { 0 };
958     snprintf(confname, sizeof(confname), "plugins/lighttable/collect/item%1d", new_rule);
959     dt_conf_set_int(confname, date_only ? DT_COLLECTION_PROP_DAY : DT_COLLECTION_PROP_TIME);
960     snprintf(confname, sizeof(confname), "plugins/lighttable/collect/mode%1d", new_rule);
961     dt_conf_set_int(confname, 0);
962     snprintf(confname, sizeof(confname), "plugins/lighttable/collect/string%1d", new_rule);
963     dt_conf_set_string(confname, coll);
964     g_free(coll);
965 
966     dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_NEW_QUERY, DT_COLLECTION_PROP_UNDEF,
967                                NULL);
968   }
969 }
970 
_lib_timeline_draw_callback(GtkWidget * widget,cairo_t * wcr,gpointer user_data)971 static gboolean _lib_timeline_draw_callback(GtkWidget *widget, cairo_t *wcr, gpointer user_data)
972 {
973   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
974   dt_lib_timeline_t *strip = (dt_lib_timeline_t *)self->data;
975 
976   GtkAllocation allocation;
977   gtk_widget_get_allocation(widget, &allocation);
978   const int32_t width = allocation.width;
979   const int32_t height = allocation.height;
980 
981   // windows could have been expanded for example, we need to create a new surface of the good size and redraw
982   if(width != strip->panel_width || height != strip->panel_height)
983   {
984     // if it's the first show, we need to recompute the scroll too
985     if(strip->panel_width == 0 || strip->panel_height == 0)
986     {
987       strip->panel_width = width;
988       strip->panel_height = height;
989       strip->time_pos = _selection_scroll_to(strip->start_t, strip);
990     }
991     if(strip->surface)
992     {
993       cairo_surface_destroy(strip->surface);
994       strip->surface = NULL;
995     }
996   }
997 
998   // create the persistent surface if it does not exists.
999   if(!strip->surface)
1000   {
1001     strip->surface_width = _block_get_at_zoom(self, width);
1002     strip->panel_width = width;
1003     strip->panel_height = height;
1004     strip->surface_height = allocation.height;
1005 
1006     // we set the width of a unit (bar) in the drawing (depending of the zoom level)
1007     int wu = _block_get_bar_width(strip->zoom);
1008 
1009     strip->surface = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, allocation.width, allocation.height);
1010 
1011     // get cairo drawing handle
1012     cairo_t *cr = cairo_create(strip->surface);
1013 
1014     /* fill background */
1015     dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_FILMSTRIP_BG);
1016     cairo_paint(cr);
1017 
1018     // draw content depending of zoom level
1019     int posx = 0;
1020     for(const GList *bl = strip->blocks; bl; bl = g_list_next(bl))
1021     {
1022       dt_lib_timeline_block_t *blo = bl->data;
1023 
1024       // width of this block
1025       int wb = blo->values_count * wu;
1026 
1027       cairo_text_extents_t te;
1028       dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_TIMELINE_TEXT_FG);
1029       cairo_set_font_size(cr, 10 * (1 + (darktable.gui->dpi_factor - 1) / 2));
1030       cairo_text_extents(cr, blo->name, &te);
1031       int bh = allocation.height - te.height - 4;
1032       cairo_move_to(cr, posx + (wb - te.width) / 2 - te.x_bearing, allocation.height - 2);
1033       cairo_show_text(cr, blo->name);
1034 
1035       dt_gui_gtk_set_source_rgb(cr, DT_GUI_COLOR_TIMELINE_BG);
1036       cairo_rectangle(cr, posx, 0, wb, bh);
1037       cairo_fill(cr);
1038 
1039       for(int i = 0; i < blo->values_count; i++)
1040       {
1041         dt_gui_gtk_set_source_rgba(cr, DT_GUI_COLOR_TIMELINE_FG, 0.5);
1042         int h = _block_get_bar_height(blo->values[i], bh);
1043         cairo_rectangle(cr, posx + (i * wu), bh - h, wu, h);
1044         cairo_fill(cr);
1045         dt_gui_gtk_set_source_rgba(cr, DT_GUI_COLOR_TIMELINE_FG, 1.0);
1046         h = _block_get_bar_height(blo->collect_values[i], bh);
1047         cairo_rectangle(cr, posx + (i * wu), bh - h, wu, h);
1048         cairo_fill(cr);
1049       }
1050 
1051       posx += wb + 2;
1052       if(posx >= allocation.width) break;
1053     }
1054 
1055     // copy back the new content into the cairo handle of the draw callback
1056     cairo_destroy(cr);
1057   }
1058   cairo_set_source_surface(wcr, strip->surface, 0, 0);
1059   cairo_paint(wcr);
1060 
1061   // we draw the selection
1062   if(strip->has_selection)
1063   {
1064     int stop = 0;
1065     int start = 0;
1066     if(strip->selecting)
1067       stop = strip->current_x;
1068     else
1069       stop = strip->stop_x;
1070     if(stop > strip->start_x)
1071       start = strip->start_x;
1072     else
1073     {
1074       start = stop;
1075       stop = strip->start_x;
1076     }
1077     // we verify that the selection is not in a hidden zone
1078     if(!(start < 0 && stop < 0) && !(start > strip->panel_width && stop > strip->panel_width))
1079     {
1080       // we draw the selection
1081       if(start >= 0)
1082       {
1083         // dt_gui_gtk_set_source_rgb(wcr, DT_GUI_COLOR_THUMBNAIL_HOVER_BG);
1084         dt_gui_gtk_set_source_rgba(wcr, DT_GUI_COLOR_TIMELINE_FG, 0.8);
1085         cairo_move_to(wcr, start, 0);
1086         cairo_line_to(wcr, start, allocation.height);
1087         cairo_stroke(wcr);
1088         dt_gui_gtk_set_source_rgba(wcr, DT_GUI_COLOR_FILMSTRIP_BG, 0.3);
1089         cairo_move_to(wcr, start, 0);
1090         cairo_line_to(wcr, start, allocation.height);
1091         cairo_stroke(wcr);
1092       }
1093       dt_gui_gtk_set_source_rgba(wcr, DT_GUI_COLOR_TIMELINE_FG, 0.5);
1094       cairo_rectangle(wcr, start, 0, stop - start, allocation.height);
1095       cairo_fill(wcr);
1096       if(stop <= strip->panel_width)
1097       {
1098         dt_gui_gtk_set_source_rgba(wcr, DT_GUI_COLOR_TIMELINE_FG, 0.8);
1099         cairo_move_to(wcr, stop, 0);
1100         cairo_line_to(wcr, stop, allocation.height);
1101         cairo_stroke(wcr);
1102         dt_gui_gtk_set_source_rgba(wcr, DT_GUI_COLOR_FILMSTRIP_BG, 0.3);
1103         cairo_move_to(wcr, stop, 0);
1104         cairo_line_to(wcr, stop, allocation.height);
1105         cairo_stroke(wcr);
1106       }
1107     }
1108   }
1109 
1110   // we draw the line under cursor and the date-time
1111   if(strip->in && strip->current_x > 0)
1112   {
1113     dt_lib_timeline_time_t tt;
1114     if(strip->selecting)
1115       tt = strip->stop_t;
1116     else
1117       tt = _time_get_from_pos(strip->current_x, strip);
1118 
1119     // we don't display NULL date (if it's outside bounds)
1120     if(_time_compare(tt, _time_init()) != 0)
1121     {
1122       dt_gui_gtk_set_source_rgb(wcr, DT_GUI_COLOR_TIMELINE_TEXT_BG);
1123       cairo_move_to(wcr, strip->current_x, 0);
1124       cairo_line_to(wcr, strip->current_x, allocation.height);
1125       cairo_stroke(wcr);
1126       gchar *dte = _time_format_for_ui(tt, strip->precision);
1127       cairo_text_extents_t te2;
1128       cairo_set_font_size(wcr, 10 * darktable.gui->dpi_factor);
1129       cairo_text_extents(wcr, dte, &te2);
1130       cairo_rectangle(wcr, strip->current_x, 8, te2.width + 4, te2.height + 4);
1131       dt_gui_gtk_set_source_rgb(wcr, DT_GUI_COLOR_TIMELINE_TEXT_BG);
1132       cairo_fill(wcr);
1133       cairo_move_to(wcr, strip->current_x + 2, 10 + te2.height);
1134       dt_gui_gtk_set_source_rgb(wcr, DT_GUI_COLOR_TIMELINE_TEXT_FG);
1135       cairo_show_text(wcr, dte);
1136       g_free(dte);
1137     }
1138   }
1139 
1140   return TRUE;
1141 }
1142 
_lib_timeline_button_press_callback(GtkWidget * w,GdkEventButton * e,gpointer user_data)1143 static gboolean _lib_timeline_button_press_callback(GtkWidget *w, GdkEventButton *e, gpointer user_data)
1144 {
1145   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
1146   dt_lib_timeline_t *strip = (dt_lib_timeline_t *)self->data;
1147 
1148   if(e->button == 1)
1149   {
1150     if(e->type == GDK_BUTTON_PRESS)
1151     {
1152       if(e->x - strip->start_x < 2 && e->x - strip->start_x > -2)
1153       {
1154         strip->start_x = strip->stop_x;
1155         strip->start_t = strip->stop_t;
1156         strip->stop_x = e->x;
1157         strip->stop_t = _time_get_from_pos(e->x, strip);
1158         strip->move_edge = TRUE;
1159       }
1160       else if(e->x - strip->stop_x < 2 && e->x - strip->stop_x > -2)
1161       {
1162         strip->stop_x = e->x;
1163         strip->stop_t = _time_get_from_pos(e->x, strip);
1164         strip->move_edge = TRUE;
1165       }
1166       else
1167       {
1168         strip->start_x = strip->stop_x = e->x;
1169         dt_lib_timeline_time_t tt = _time_get_from_pos(e->x, strip);
1170         if(_time_compare(tt, _time_init()) == 0)
1171           strip->start_t = strip->stop_t = strip->time_maxi; //we are past the end so selection extends until the end
1172         else
1173           strip->start_t = strip->stop_t = tt;
1174         strip->move_edge = FALSE;
1175       }
1176       strip->selecting = TRUE;
1177       strip->has_selection = TRUE;
1178       gtk_widget_queue_draw(strip->timeline);
1179     }
1180   }
1181   else if(e->button == 3)
1182   {
1183     // we remove the last rule if it's a datetime one
1184     const int nb_rules = dt_conf_get_int("plugins/lighttable/collect/num_rules");
1185     if(nb_rules > 0)
1186     {
1187       char confname[200] = { 0 };
1188       snprintf(confname, sizeof(confname), "plugins/lighttable/collect/item%1d", nb_rules - 1);
1189       if(dt_conf_get_int(confname) == DT_COLLECTION_PROP_TIME)
1190       {
1191         dt_conf_set_int("plugins/lighttable/collect/num_rules", nb_rules - 1);
1192         dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_UNDEF,
1193                                    NULL);
1194 
1195         strip->selecting = FALSE;
1196       }
1197     }
1198   }
1199 
1200   return FALSE;
1201 }
1202 
_lib_timeline_button_release_callback(GtkWidget * w,GdkEventButton * e,gpointer user_data)1203 static gboolean _lib_timeline_button_release_callback(GtkWidget *w, GdkEventButton *e, gpointer user_data)
1204 {
1205   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
1206   dt_lib_timeline_t *strip = (dt_lib_timeline_t *)self->data;
1207 
1208   if(strip->selecting)
1209   {
1210     strip->stop_x = e->x;
1211     dt_lib_timeline_time_t tt = _time_get_from_pos(e->x, strip);
1212     if(_time_compare(tt, _time_init()) == 0)
1213       strip->stop_t = strip->time_maxi; //we are past the end so selection extends until the end
1214     else
1215     {
1216       strip->stop_t = tt;
1217       // we want to be at the "end" of this date
1218       if(strip->zoom <= DT_LIB_TIMELINE_ZOOM_DAY)
1219       {
1220 	strip->stop_t.minute = 59;
1221 	if(strip->zoom <= DT_LIB_TIMELINE_ZOOM_MONTH)
1222 	{
1223 	  strip->stop_t.hour = 23;
1224 	  if(strip->zoom <= DT_LIB_TIMELINE_ZOOM_YEAR)
1225 	  {
1226 	    strip->stop_t.day = _time_days_in_month(strip->stop_t.year, strip->stop_t.month);
1227 	  }
1228 	}
1229       }
1230     }
1231     strip->selecting = FALSE;
1232 
1233     if(!strip->move_edge && dt_modifier_is(e->state, GDK_SHIFT_MASK))
1234       _selection_collect(strip, DT_LIB_TIMELINE_MODE_RESET);
1235     else
1236       _selection_collect(strip, DT_LIB_TIMELINE_MODE_AND);
1237     gtk_widget_queue_draw(strip->timeline);
1238   }
1239 
1240   return TRUE;
1241 }
1242 
_selection_start(GtkAccelGroup * accel_group,GObject * aceeleratable,guint keyval,GdkModifierType modifier,gpointer data)1243 static gboolean _selection_start(GtkAccelGroup *accel_group, GObject *aceeleratable, guint keyval,
1244                                  GdkModifierType modifier, gpointer data)
1245 {
1246   dt_lib_timeline_t *strip = (dt_lib_timeline_t *)data;
1247 
1248   strip->start_x = strip->current_x;
1249   dt_lib_timeline_time_t tt = _time_get_from_pos(strip->current_x, strip);
1250   if(_time_compare(tt, _time_init()) == 0)
1251     strip->start_t = strip->time_maxi; //we are past the end so selection extends until the end
1252   else
1253     strip->start_t = _time_get_from_pos(strip->current_x, strip);
1254   strip->stop_x = strip->start_x;
1255   strip->stop_t = strip->start_t;
1256   strip->selecting = TRUE;
1257   strip->has_selection = TRUE;
1258 
1259   gtk_widget_queue_draw(strip->timeline);
1260   return TRUE;
1261 }
_selection_stop(GtkAccelGroup * accel_group,GObject * aceeleratable,guint keyval,GdkModifierType modifier,gpointer data)1262 static gboolean _selection_stop(GtkAccelGroup *accel_group, GObject *aceeleratable, guint keyval,
1263                                 GdkModifierType modifier, gpointer data)
1264 {
1265   dt_lib_timeline_t *strip = (dt_lib_timeline_t *)data;
1266   dt_lib_timeline_time_t tt = _time_get_from_pos(strip->current_x, strip);
1267 
1268   strip->stop_x = strip->current_x;
1269   if(_time_compare(tt, _time_init()) == 0)
1270     strip->stop_t = strip->time_maxi; //we are past the end so selection extends until the end
1271   else
1272   {
1273     strip->stop_t = tt;
1274     // we want to be at the "end" of this date
1275     if(strip->zoom < DT_LIB_TIMELINE_ZOOM_HOUR)
1276     {
1277       strip->stop_t.minute = 59;
1278       if(strip->zoom < DT_LIB_TIMELINE_ZOOM_DAY)
1279       {
1280 	strip->stop_t.hour = 23;
1281 	if(strip->zoom < DT_LIB_TIMELINE_ZOOM_MONTH)
1282 	{
1283 	  strip->stop_t.day = _time_days_in_month(strip->stop_t.year, strip->stop_t.month);
1284 	}
1285       }
1286     }
1287   }
1288 
1289   strip->selecting = FALSE;
1290   _selection_collect(strip, DT_LIB_TIMELINE_MODE_AND);
1291   gtk_widget_queue_draw(strip->timeline);
1292   return TRUE;
1293 }
1294 
_block_autoscroll(gpointer user_data)1295 static gboolean _block_autoscroll(gpointer user_data)
1296 {
1297   // this function is called repetidly until the pointer is not more in the autoscoll zone
1298   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
1299   dt_lib_timeline_t *strip = (dt_lib_timeline_t *)self->data;
1300 
1301   if(!strip->in)
1302   {
1303     strip->autoscroll = FALSE;
1304     return FALSE;
1305   }
1306 
1307   int move = 0;
1308   if(strip->current_x < 10)
1309     move = -1;
1310   else if(strip->current_x > strip->panel_width - 10)
1311     move = 1;
1312 
1313   if(move == 0)
1314   {
1315     strip->autoscroll = FALSE;
1316     return FALSE;
1317   }
1318 
1319   dt_lib_timeline_time_t old_pos = strip->time_pos;
1320   _time_add(&(strip->time_pos), move, strip->zoom);
1321   // we ensure that the fimlstrip stay in the bounds
1322   dt_lib_timeline_time_t tt = _selection_scroll_to(strip->time_pos, strip);
1323   if(_time_compare(tt, strip->time_pos) != 0)
1324   {
1325     strip->time_pos = old_pos; //no scroll, so we restore the previous position
1326     strip->autoscroll = FALSE;
1327     return FALSE;
1328   }
1329 
1330   cairo_surface_destroy(strip->surface);
1331   strip->surface = NULL;
1332   gtk_widget_queue_draw(strip->timeline);
1333   return TRUE;
1334 }
1335 
_lib_timeline_motion_notify_callback(GtkWidget * w,GdkEventMotion * e,gpointer user_data)1336 static gboolean _lib_timeline_motion_notify_callback(GtkWidget *w, GdkEventMotion *e, gpointer user_data)
1337 {
1338   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
1339   dt_lib_timeline_t *strip = (dt_lib_timeline_t *)self->data;
1340 
1341   strip->in = TRUE;
1342 
1343   // auto-scroll if cursor is at one end of the panel
1344   if((e->x < 10 || e->x > strip->panel_width - 10) && !strip->autoscroll)
1345   {
1346     // first scroll immediately and then every 400ms until cursor quit the "auto-zone"
1347     if(_block_autoscroll(user_data))
1348     {
1349       strip->autoscroll = TRUE;
1350       g_timeout_add(400, _block_autoscroll, user_data);
1351     }
1352   }
1353 
1354   strip->current_x = e->x;
1355 
1356   if(strip->selecting)
1357   {
1358     strip->stop_x = e->x;
1359     strip->stop_t = _time_get_from_pos(e->x, strip);
1360     dt_control_change_cursor(GDK_LEFT_PTR);
1361   }
1362   else
1363   {
1364     // we change the cursor if we are close enough of a selection limit
1365     if(e->x - strip->start_x < 2 && e->x - strip->start_x > -2)
1366     {
1367       dt_control_change_cursor(GDK_LEFT_SIDE);
1368     }
1369     else if(e->x - strip->stop_x < 2 && e->x - strip->stop_x > -2)
1370     {
1371       dt_control_change_cursor(GDK_RIGHT_SIDE);
1372     }
1373     else
1374     {
1375       dt_control_change_cursor(GDK_LEFT_PTR);
1376     }
1377   }
1378   gtk_widget_queue_draw(strip->timeline);
1379   return TRUE;
1380 }
1381 
_lib_timeline_scroll_callback(GtkWidget * w,GdkEventScroll * e,gpointer user_data)1382 static gboolean _lib_timeline_scroll_callback(GtkWidget *w, GdkEventScroll *e, gpointer user_data)
1383 {
1384   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
1385   dt_lib_timeline_t *strip = (dt_lib_timeline_t *)self->data;
1386 
1387   // zoom change (with Ctrl key)
1388   if(dt_modifier_is(e->state, GDK_CONTROL_MASK))
1389   {
1390     int z = strip->zoom;
1391     int delta_y = 0;
1392     if(dt_gui_get_scroll_unit_deltas(e, NULL, &delta_y))
1393     {
1394       if(delta_y < 0)
1395       {
1396         if(z != DT_LIB_TIMELINE_ZOOM_HOUR) z++;
1397       }
1398       else if(delta_y > 0)
1399       {
1400         if(z != DT_LIB_TIMELINE_ZOOM_YEAR) z--;
1401       }
1402     }
1403 
1404     // if the zoom as changed, we need to recompute blocks and redraw
1405     if(z != strip->zoom)
1406     {
1407       dt_conf_set_int("plugins/lighttable/timeline/last_zoom", z);
1408       strip->time_pos = _time_compute_offset_for_zoom(strip->current_x, strip, z);
1409       strip->zoom = z;
1410       if(z % 2 == 0)
1411         strip->precision = z + 2;
1412       else
1413         strip->precision = z + 1;
1414       cairo_surface_destroy(strip->surface);
1415       strip->surface = NULL;
1416       gtk_widget_queue_draw(strip->timeline);
1417     }
1418     return TRUE;
1419   }
1420   else
1421   {
1422     int delta;
1423     if(dt_gui_get_scroll_unit_delta(e, &delta))
1424     {
1425       int move = delta;
1426       if(dt_modifier_is(e->state, GDK_SHIFT_MASK)) move *= 2;
1427 
1428       _time_add(&(strip->time_pos), move, strip->zoom);
1429       // we ensure that the fimlstrip stay in the bounds
1430       strip->time_pos = _selection_scroll_to(strip->time_pos, strip);
1431 
1432       cairo_surface_destroy(strip->surface);
1433       strip->surface = NULL;
1434       gtk_widget_queue_draw(strip->timeline);
1435     }
1436   }
1437   return FALSE;
1438 }
1439 
_lib_timeline_mouse_leave_callback(GtkWidget * w,GdkEventCrossing * e,gpointer user_data)1440 static gboolean _lib_timeline_mouse_leave_callback(GtkWidget *w, GdkEventCrossing *e, gpointer user_data)
1441 {
1442   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
1443   dt_lib_timeline_t *strip = (dt_lib_timeline_t *)self->data;
1444 
1445   strip->in = FALSE;
1446 
1447   gtk_widget_queue_draw(strip->timeline);
1448   return TRUE;
1449 }
1450 
init_key_accels(dt_lib_module_t * self)1451 void init_key_accels(dt_lib_module_t *self)
1452 {
1453   dt_accel_register_lib(self, NC_("accel", "start selection"), GDK_KEY_bracketleft, 0);
1454   dt_accel_register_lib(self, NC_("accel", "stop selection"), GDK_KEY_bracketright, 0);
1455 }
1456 
connect_key_accels(dt_lib_module_t * self)1457 void connect_key_accels(dt_lib_module_t *self)
1458 {
1459   GClosure *closure = g_cclosure_new(G_CALLBACK(_selection_start), (gpointer)self->data, NULL);
1460   dt_accel_connect_lib(self, "start selection", closure);
1461   closure = g_cclosure_new(G_CALLBACK(_selection_stop), (gpointer)self->data, NULL);
1462   dt_accel_connect_lib(self, "stop selection", closure);
1463 }
1464 
gui_init(dt_lib_module_t * self)1465 void gui_init(dt_lib_module_t *self)
1466 {
1467   /* initialize ui widgets */
1468   dt_lib_timeline_t *d = (dt_lib_timeline_t *)calloc(1, sizeof(dt_lib_timeline_t));
1469   self->data = (void *)d;
1470 
1471   d->zoom = CLAMP(dt_conf_get_int("plugins/lighttable/timeline/last_zoom"), 0, 8);
1472   if(d->zoom % 2 == 0)
1473     d->precision = d->zoom + 2;
1474   else
1475     d->precision = d->zoom + 1;
1476 
1477   d->time_mini = _time_init();
1478   d->time_maxi = _time_init();
1479   d->start_t = _time_init();
1480   d->stop_t = _time_init();
1481 
1482   _time_read_bounds_from_db(self);
1483   d->time_pos = d->time_mini;
1484   /* creating drawing area */
1485   self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
1486   dt_gui_add_help_link(self->widget, dt_get_help_url(self->plugin_name));
1487 
1488   /* creating timeline box*/
1489   d->timeline = gtk_event_box_new();
1490 
1491   gtk_widget_add_events(d->timeline, GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK
1492                                          | GDK_BUTTON_RELEASE_MASK | darktable.gui->scroll_mask
1493                                          | GDK_LEAVE_NOTIFY_MASK);
1494 
1495   g_signal_connect(G_OBJECT(d->timeline), "draw", G_CALLBACK(_lib_timeline_draw_callback), self);
1496   g_signal_connect(G_OBJECT(d->timeline), "button-press-event", G_CALLBACK(_lib_timeline_button_press_callback),
1497                    self);
1498   g_signal_connect(G_OBJECT(d->timeline), "button-release-event",
1499                    G_CALLBACK(_lib_timeline_button_release_callback), self);
1500   g_signal_connect(G_OBJECT(d->timeline), "scroll-event", G_CALLBACK(_lib_timeline_scroll_callback), self);
1501   g_signal_connect(G_OBJECT(d->timeline), "motion-notify-event", G_CALLBACK(_lib_timeline_motion_notify_callback),
1502                    self);
1503   g_signal_connect(G_OBJECT(d->timeline), "leave-notify-event", G_CALLBACK(_lib_timeline_mouse_leave_callback),
1504                    self);
1505 
1506   gtk_box_pack_start(GTK_BOX(self->widget), d->timeline, TRUE, TRUE, 0);
1507 
1508   // we update the selection with actual collect rules
1509   _lib_timeline_collection_changed(NULL, DT_COLLECTION_CHANGE_NEW_QUERY, DT_COLLECTION_PROP_UNDEF, NULL, -1, self);
1510 
1511   /* initialize view manager proxy */
1512   darktable.view_manager->proxy.timeline.module = self;
1513 
1514   DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_COLLECTION_CHANGED,
1515                             G_CALLBACK(_lib_timeline_collection_changed), (gpointer)self);
1516 }
1517 
gui_cleanup(dt_lib_module_t * self)1518 void gui_cleanup(dt_lib_module_t *self)
1519 {
1520   /* cleanup */
1521   dt_lib_timeline_t *strip = (dt_lib_timeline_t *)self->data;
1522   if(strip->blocks) g_list_free_full(strip->blocks, _block_free);
1523   DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_lib_timeline_collection_changed), self);
1524   /* unset viewmanager proxy */
1525   darktable.view_manager->proxy.timeline.module = NULL;
1526   free(self->data);
1527   self->data = NULL;
1528 }
1529 
1530 
1531 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
1532 // vim: shiftwidth=2 expandtab tabstop=2 cindent
1533 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
1534