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