1 module unde.viewers.image_viewer.lib;
2 
3 import unde.global_state;
4 import unde.path_mnt;
5 import unde.lib;
6 import unde.slash;
7 
8 import berkeleydb.all;
9 
10 import derelict.sdl2.sdl;
11 import derelict.sdl2.image;
12 
13 import std..string;
14 import std.stdio;
15 import std.algorithm.sorting;
16 import std.math;
17 import core.memory;
18 
19 import std.file;
20 
21 void
22 image_viewer(GlobalState gs, PathMnt p)
23 {
24     with (gs.image_viewer)
25     {
26         auto old_state = gs.state;
27         gs.state = State.ImageViewer;
28         path = p;
29 
30         rect = SDL_Rect(0, 0, gs.screen.w, gs.screen.h);
31 
32         auto st = get_image_from_cache(gs, path);
33         texture_tick = st;
34         get_rectsize(gs);
35         if (st != null)
36         {
37             if (st.w > gs.screen.w || st.h > gs.screen.h)
38             {
39                 setup_0_scale(gs);
40             }
41             else
42             {
43                 setup_1_scale(gs);
44             }
45         }
46 
47         if (old_state != State.ImageViewer)
48         {
49             calculate_positions_in_directories(gs);
50         }
51 
52         gs.dirty = true;
53     }
54 }
55 
56 package void
57 setup_0_scale(GlobalState gs)
58 {
59     with (gs.image_viewer)
60     {
61         with (gs.image_viewer.texture_tick)
62         {
63             if (texture)
64             {
65                 int w = w;
66                 int h = h;
67 
68                 if (rectsize.angle == 90 || rectsize.angle == 270)
69                 {
70                     if (cast(double)(w)/h > cast(double)(gs.screen.w)/gs.screen.h)
71                     {
72                         /* EN: Center and scale down picture to take all width
73                             of screen
74                            RU: Отцентрировать и уменьшить картинку так, чтобы
75                             она заняла всю ширину экрана */
76                         rect.x = (gs.screen.w - gs.screen.h)/2;
77                         rect.y = (gs.screen.w*h/w - gs.screen.h*h/w)/2 + (gs.screen.h-gs.screen.w*h/w)/2;
78                         rect.w = gs.screen.h;
79                         rect.h = gs.screen.h*h/w;
80                     }
81                     else
82                     {
83                         /* EN: Center and scale down picture to take all height
84                             of screen
85                            RU: Отцентрировать и уменьшить картинку так, чтобы
86                             она заняла всю высоту экрана */
87                         rect.x = (gs.screen.h*w/h - gs.screen.h)/2 + (gs.screen.w-gs.screen.h*w/h)/2;
88                         rect.y = (gs.screen.h - gs.screen.h*h/w)/2;
89                         rect.w = gs.screen.h;
90                         rect.h = gs.screen.h*h/w;
91                     }
92                 }
93                 else
94                 {
95                     if (cast(double)(w)/h > cast(double)(gs.screen.w)/gs.screen.h)
96                     {
97                         /* EN: Center and scale down picture to take all width
98                             of screen
99                            RU: Отцентрировать и уменьшить картинку так, чтобы
100                             она заняла всю ширину экрана */
101                         rect.x = 0;
102                         rect.y = (gs.screen.h-gs.screen.w*h/w)/2;
103                         rect.w = gs.screen.w;
104                         rect.h = gs.screen.w*h/w;
105                     }
106                     else
107                     {
108                         /* EN: Center and scale down picture to take all height
109                             of screen
110                            RU: Отцентрировать и уменьшить картинку так, чтобы
111                             она заняла всю высоту экрана */
112                         rect.x = (gs.screen.w-gs.screen.h*w/h)/2;
113                         rect.y = 0;
114                         rect.w = gs.screen.h*w/h;
115                         rect.h = gs.screen.h;
116                     }
117                 }
118             }
119         }
120     }
121 }
122 
123 package void
124 setup_1_scale(GlobalState gs)
125 {
126     with (gs.image_viewer)
127     {
128         with (gs.image_viewer.texture_tick)
129         {
130             if (texture)
131             {
132                 rect = SDL_Rect(0, 0, w, h);
133                 if (rectsize.angle == 90 || rectsize.angle == 270)
134                 {
135                     rect.x = -(w-h)/2;
136                     rect.y = (w-h)/2;
137                 }
138             }
139         }
140     }
141 }
142 
143 private void
144 get_rectsize(GlobalState gs)
145 {
146     with (gs.image_viewer)
147     {
148         Dbt key, data;
149         string path0 = path.get_key(gs.lsblk);
150         key = path0;
151         //writefln("GET %s", path0.replace("\0", SL));
152         auto res = gs.db_map.get(null, &key, &data);
153 
154         if (res == 0)
155         {
156             rectsize = data.to!(RectSize);
157         }
158     }
159 }
160 
161 package void
162 put_rectsize(GlobalState gs)
163 {
164     with (gs.image_viewer)
165     {
166         Dbt key, data;
167         string path0 = path.get_key(gs.lsblk);
168         key = path0;
169         data = rectsize;
170         auto res = gs.db_map.put(null, &key, &data);
171         if (res != 0)
172             throw new Exception("Path info to map-db not written");
173     }
174 }
175 
176 auto
177 get_image_from_cache(GlobalState gs,
178         in string p)
179 {
180     /* EN: Get picture from cache or load it from file
181        RU: Получить картинку из кеша или загрузить из файла */
182     with(gs.image_viewer)
183     {
184         auto st = p in image_cache;
185         if (st)
186         {
187             st.tick = SDL_GetTicks();
188             last_image_cache_use = SDL_GetTicks();
189         }
190         else
191         {
192             auto image = IMG_Load(p.toStringz);
193 
194             if (image)
195             {
196                 auto image_texture =
197                     SDL_CreateTextureFromSurface(gs.renderer, image);
198                 image_cache[p] = Texture_Tick(image.w, image.h, [], image_texture, SDL_GetTicks());
199                 SDL_FreeSurface(image);
200                 last_image_cache_use = SDL_GetTicks();
201                 st = path in image_cache;
202             }
203         }
204 
205         return st;
206     }
207 }
208 
209 /* EN: clear cache from old entries
210    RU: очистить кеш от старых элементов */
211 void
212 clear_image_cache(GlobalState gs)
213 {
214     int cleared;
215     with(gs.image_viewer)
216     {
217         foreach(k, v; image_cache)
218         {
219             if (v.tick < last_image_cache_use - 30_000)
220             {
221                 cleared++;
222                 if (v.texture) SDL_DestroyTexture(v.texture);
223                 if (!image_cache.remove(k))
224                 {
225                     writefln("Can't remove image cache key %s", k);
226                 }
227                 writefln("v.tick = %s < %s. Remove key %s",
228                         v.tick, last_image_cache_use - 30_000, k);
229             }
230         }
231     }
232     if (cleared) 
233     {
234         //writefln("Cleared %d objects from lines cache", cleared);
235         GC.collect();
236     }
237 }
238 
239 void
240 draw_image(GlobalState gs)
241 {
242     with(gs.image_viewer)
243     {
244         texture_tick = get_image_from_cache(gs, path);
245         clear_image_cache(gs);
246         if (texture_tick && texture_tick.texture)
247         {
248             if (path in gs.selection_hash)
249             {
250                 SDL_Rect dst = SDL_Rect(0, 0, gs.screen.w, gs.screen.h);
251                 int r = SDL_RenderCopy(gs.renderer, gs.texture_blue, null, &dst);
252                 if (r < 0)
253                 {
254                     writefln( "draw_image(): Error while render copy: %s", fromStringz(SDL_GetError()) );
255                 }
256             }
257 
258             int r = SDL_RenderCopyEx(gs.renderer, texture_tick.texture, null, &gs.image_viewer.rect, rectsize.angle,
259                         null, SDL_FLIP_NONE);
260             if (r < 0)
261             {
262                 writefln( "draw_image(): Error while render copy: %s", fromStringz(SDL_GetError()) );
263             }
264 
265             if (path in gs.selection_hash &&
266                     abs(gs.image_viewer.rect.w - gs.screen.w) < 20 && (gs.image_viewer.rect.h - gs.screen.h) < 20)
267             {
268                 auto rect = SDL_Rect(0, 0, gs.screen.w, 32);
269                 r = SDL_RenderCopy(gs.renderer, gs.texture_blue, null, &rect);
270                 if (r < 0)
271                 {
272                     writefln( "draw_image(): Error while render copy: %s", fromStringz(SDL_GetError()) );
273                 }
274             }
275         }
276     }
277 }
278 
279 private void
280 positions_in_directories_recursive(GlobalState gs, string p, ssize_t lev = 0)
281 {
282     with (gs.image_viewer)
283     {
284         if (lev == 0)
285         {
286             files = [];
287             positions = [];
288         }
289 
290         string[] paths;
291 
292         try
293         {
294             foreach (string name; dirEntries(p, SpanMode.shallow))
295             {
296                 paths ~= name;
297             }
298         }
299         catch (FileException e)
300         {
301             return;
302         }
303 
304         sort!("a < b")(paths);
305 
306         bool found = false;
307         foreach (i, name; paths)
308         {
309             if (path.startsWith(name) && (name.length+1 > path.length || path[name.length] == SL[0]))
310             {
311                 found = true;
312                 positions ~= i;
313                 files ~= paths;
314 
315                 if (name == path)
316                 {
317                     level = lev;
318                 }
319                 else
320                 {
321                     positions_in_directories_recursive(gs, name, level+1);
322                 }
323                 break;
324             }
325         }
326         assert(found);
327     }
328 }
329 
330 private void
331 calculate_positions_in_directories(GlobalState gs)
332 {
333     with (gs.image_viewer)
334     {
335         selections = [];
336 
337         foreach (selection; gs.selection_hash.byKey())
338         {
339             if ( mime(selection) != "inode/directory" ) continue;
340 
341             bool found_subdirs = false;
342             //writefln("%s", selection);
343             rescan_selections:
344             foreach (i, ref s; selections)
345             {
346                 if ( selection.startsWith(s) )
347                 {
348                     //writefln("1. %s starts with %s", selection, s);
349                     // Do nothing, the super-directory in the selections
350                     found_subdirs = true;
351                 }
352                 else if ( s.startsWith(selection) )
353                 {
354                     //writefln("2. %s starts with %s", s, selection);
355                     // selections consists subdirectory
356                     if (found_subdirs)
357                     {
358                         //writefln("before selections=%s", selections);
359                         if (i < selection.length-1)
360                             selections = selections[0..i] ~ selections[i+1..$];
361                         else
362                             selections = selections[0..i];
363                         //writefln("after selections=%s", selections);
364                         goto rescan_selections;
365                     }
366                     else
367                     {
368                         s = selection;
369                         found_subdirs = true;
370                     }
371                 }
372             }
373 
374             if (!found_subdirs)
375             {
376                 selections ~= selection;
377             }
378         }
379 
380         if (selections.length == 0)
381         {
382             string dir = getParent(path);
383             selections ~= dir;
384         }
385 
386         /*writefln("selections = %s", selections);*/
387 
388         sel = -1;
389         foreach (i, s; selections)
390         {
391             if (path.startsWith(s))
392             {
393                 sel = i;
394             }
395         }
396 
397         if (sel >= 0)
398         {
399             positions_in_directories_recursive(gs, selections[sel]);
400         }
401 
402         /*writefln("positions = %s", positions);
403         writefln("files = %s", files);
404         writefln("level = %s", level);*/
405     }
406 }
407 
408 package void
409 image_next(GlobalState gs)
410 {
411     with (gs.image_viewer)
412     {
413         int sel0_count = 0;
414         bool found_image = false;
415         do
416         {
417             if (sel >= 0 && level >= 0)
418             {
419                 bool level_dec;
420                 do
421                 {
422                     level_dec = false;
423                     positions[level]++;
424                     //writefln("positions[%s] = %s", level, positions[level]);
425                     //writefln("files = %s", files);
426                     if ( positions[level] >= files[level].length )
427                     {
428                         //writefln("files[%s] exceeded", level);
429                         files = files[0..$-1];
430                         positions = positions[0..$-1];
431                         level--;
432                         level_dec = true;
433                         if (level == -1)
434                         {
435                             //writefln("level == -1");
436                             sel++;
437                             if (sel >= selections.length)
438                             {
439                                 //writefln("sel = 0");
440                                 sel = 0;
441                                 sel0_count++;
442                             }
443                         }
444                     }
445                 } while (level_dec && level >= 0);
446             }
447             else if (sel == -1)
448             {
449                 sel = 0;
450                 sel0_count++;
451             }
452 
453             //writefln("level = %s", level);
454             if (level == -1)
455             {
456                 string[] paths;
457                 try
458                 {
459                     foreach (string name; dirEntries(selections[sel], SpanMode.shallow))
460                     {
461                         paths ~= name;
462                     }
463                 }
464                 catch (FileException e)
465                 {
466                     return;
467                 }
468 
469                 sort!("a < b")(paths);
470 
471                 positions ~= 0;
472                 files ~= paths;
473                 level++;
474                 //writefln("0 paths added");
475             }
476 
477             do
478             {
479                 if (files[level].length == 0) break;
480                 string p = files[level][positions[level]]; 
481                     //writefln("p=%s", p);
482                 string mime_type = mime(p);
483                 if (mime_type == "inode/directory")
484                 {
485                     //writefln("directory");
486                     string[] paths;
487 
488                     try
489                     {
490                         foreach (string name; dirEntries(p, SpanMode.shallow))
491                         {
492                             paths ~= name;
493                         }
494                     }
495                     catch (FileException e)
496                     {
497                         return;
498                     }
499 
500                     if (paths.length == 0)
501                     {
502                         break;
503                     }
504 
505                     sort!("a < b")(paths);
506 
507                     positions ~= 0;
508                     files ~= paths;
509                     level++;
510                 }
511                 else if (mime_type.startsWith("image/"))
512                 {
513                     //writefln("image!");
514                     auto pathmnt = PathMnt(gs.lsblk, p);
515                     image_viewer(gs, pathmnt);
516                     found_image = true;
517                     break;
518                 }
519                 else
520                 {
521                     break;
522                 }
523             } while (true);
524         } while(sel0_count < 2 && !found_image);
525     }
526 }
527 
528 package void
529 image_prev(GlobalState gs)
530 {
531     with (gs.image_viewer)
532     {
533         int seln_count = 0;
534         bool found_image = false;
535         do
536         {
537             if (sel >= 0 && level >= 0)
538             {
539                 bool level_dec;
540                 do
541                 {
542                     level_dec = false;
543                     positions[level]--;
544                     //writefln("positions[%s] = %s", level, positions[level]);
545                     //writefln("files = %s", files);
546                     if ( positions[level] < 0 )
547                     {
548                         //writefln("files[%s] exceeded", level);
549                         files = files[0..$-1];
550                         positions = positions[0..$-1];
551                         level--;
552                         level_dec = true;
553                         if (level == -1)
554                         {
555                             //writefln("level == -1");
556                             sel--;
557                             if (sel < 0)
558                             {
559                                 //writefln("sel = 0");
560                                 sel = selections.length-1;
561                                 seln_count++;
562                             }
563                         }
564                     }
565                 } while (level_dec && level >= 0);
566             }
567             else if (sel == -1)
568             {
569                 sel = selections.length-1;
570                 seln_count++;
571             }
572 
573             //writefln("level = %s", level);
574             if (level == -1)
575             {
576                 string[] paths;
577                 try
578                 {
579                     foreach (string name; dirEntries(selections[sel], SpanMode.shallow))
580                     {
581                         paths ~= name;
582                     }
583                 }
584                 catch (FileException e)
585                 {
586                     return;
587                 }
588 
589                 sort!("a < b")(paths);
590 
591                 positions ~= paths.length-1;
592                 files ~= paths;
593                 level++;
594                 //writefln("0 paths added");
595             }
596 
597             do
598             {
599                 if (files[level].length == 0) break;
600                 string p = files[level][positions[level]]; 
601                     //writefln("p=%s", p);
602                 string mime_type = mime(p);
603                 if (mime_type == "inode/directory")
604                 {
605                     //writefln("directory");
606                     string[] paths;
607 
608                     try
609                     {
610                         foreach (string name; dirEntries(p, SpanMode.shallow))
611                         {
612                             paths ~= name;
613                         }
614                     }
615                     catch (FileException e)
616                     {
617                         return;
618                     }
619 
620                     if (paths.length == 0)
621                     {
622                         break;
623                     }
624 
625                     sort!("a < b")(paths);
626 
627                     positions ~= paths.length-1;
628                     files ~= paths;
629                     level++;
630                 }
631                 else if (mime_type.startsWith("image/"))
632                 {
633                     //writefln("image!");
634                     auto pathmnt = PathMnt(gs.lsblk, p);
635                     image_viewer(gs, pathmnt);
636                     found_image = true;
637                     break;
638                 }
639                 else
640                 {
641                     break;
642                 }
643             } while (true);
644         } while(seln_count < 2 && !found_image);
645     }
646 }