1 module dlangui.graphics.iconprovider;
2 
3 /**
4  * Getting images for standard system icons and file paths.
5  *
6  * Copyright: Roman Chistokhodov, 2017
7  * License:   Boost License 1.0
8  * Authors:   Roman Chistokhodov, freeslave93@gmail.com
9  *
10  */
11 
12 import dlangui.core.config;
13 import dlangui.graphics.drawbuf;
14 import dlangui.core.logger;
15 import isfreedesktop;
16 
17 /**
18  * Crossplatform names for some of system icons.
19  */
20 enum StandardIcon
21 {
22     document,
23     application,
24     folder,
25     folderOpen,
26     driveFloppy,
27     driveFixed,
28     driveRemovable,
29     driveCD,
30     driveDVD,
31     server,
32     printer,
33     find,
34     help,
35     sharedItem,
36     link,
37     trashcanEmpty,
38     trashcanFull,
39     mediaCDAudio,
40     mediaDVDAudio,
41     mediaDVD,
42     mediaCD,
43     fileAudio,
44     fileImage,
45     fileVideo,
46     fileZip,
47     fileUnknown,
48     warning,
49     information,
50     error,
51     password,
52     rename,
53     deleteItem,
54     computer,
55     laptop,
56     users,
57     deviceCellphone,
58     deviceCamera,
59     deviceCameraVideo,
60 }
61 
62 /**
63  * Base class for icon provider.
64  */
65 abstract class IconProviderBase
66 {
67     /**
68      * Get image of standard icon. If icon was not found use fallback.
69      */
70     final DrawBufRef getStandardIcon(StandardIcon icon, lazy DrawBufRef fallback)
71     {
72         auto image = getStandardIcon(icon);
73         return image.isNull() ? fallback() : image;
74     }
75     /**
76      * Get image of icon associated with file path. If icon was not found use fallback.
77      */
78     final DrawBufRef getIconForFilePath(string filePath, lazy DrawBufRef fallback)
79     {
80         auto image = getIconForFilePath(filePath);
81         return image.isNull() ? fallback() : image;
82     }
83 
84     /**
85      * Get image of standard icon. Return the null image if icon was not found in the system.
86      */
87     DrawBufRef getStandardIcon(StandardIcon icon);
88 
89     /**
90      * Get image of icon associated with file path. Return null image if icon was not found in the system.
91      * Default implementation detects icon for a directory and for a file using the list of hardcoded extensions.
92      *
93      */
94     DrawBufRef getIconForFilePath(string filePath)
95     {
96         // TODO: implement specifically for different platforms
97         import std.path : extension;
98         import std.uni : toLower;
99         import std.file : isDir, isFile;
100         import std.exception : collectException;
101         bool isdir;
102         collectException(isDir(filePath), isdir);
103         if (isdir) {
104             return getStandardIcon(StandardIcon.folder);
105         }
106         if (!filePath.extension.length) {
107             return getStandardIcon(StandardIcon.fileUnknown);
108         }
109         switch(filePath.extension.toLower) with(StandardIcon)
110         {
111             case ".jpeg": case ".jpg": case ".png": case ".bmp":
112                 return getStandardIcon(fileImage);
113             case ".wav": case ".mp3": case ".ogg":
114                 return getStandardIcon(fileAudio);
115             case ".avi": case ".mkv":
116                 return getStandardIcon(fileVideo);
117             case ".doc": case ".docx":
118                 return getStandardIcon(document);
119             case ".zip": case ".rar": case ".7z": case ".gz":
120                 return getStandardIcon(fileZip);
121             default:
122                 return DrawBufRef(null);
123         }
124     }
125 }
126 
127 /**
128  * Dummy icon provider. Always returns null images or fallbacks. Available on all platforms.
129  */
130 class DummyIconProvider : IconProviderBase
131 {
132     override DrawBufRef getStandardIcon(StandardIcon icon)
133     {
134         return DrawBufRef(null);
135     }
136     override DrawBufRef getIconForFilePath(string filePath)
137     {
138         return DrawBufRef(null);
139     }
140 }
141 
142 static if (!WIDGET_STYLE_CONSOLE) {
143     version(Windows)
144     {
145         import core.sys.windows.windows;
146         enum SHSTOCKICONID {
147             SIID_DOCNOASSOC         = 0,
148             SIID_DOCASSOC           = 1,
149             SIID_APPLICATION        = 2,
150             SIID_FOLDER             = 3,
151             SIID_FOLDEROPEN         = 4,
152             SIID_DRIVE525           = 5,
153             SIID_DRIVE35            = 6,
154             SIID_DRIVEREMOVE        = 7,
155             SIID_DRIVEFIXED         = 8,
156             SIID_DRIVENET           = 9,
157             SIID_DRIVENETDISABLED   = 10,
158             SIID_DRIVECD            = 11,
159             SIID_DRIVERAM           = 12,
160             SIID_WORLD              = 13,
161             SIID_SERVER             = 15,
162             SIID_PRINTER            = 16,
163             SIID_MYNETWORK          = 17,
164             SIID_FIND               = 22,
165             SIID_HELP               = 23,
166             SIID_SHARE              = 28,
167             SIID_LINK               = 29,
168             SIID_SLOWFILE           = 30,
169             SIID_RECYCLER           = 31,
170             SIID_RECYCLERFULL       = 32,
171             SIID_MEDIACDAUDIO       = 40,
172             SIID_LOCK               = 47,
173             SIID_AUTOLIST           = 49,
174             SIID_PRINTERNET         = 50,
175             SIID_SERVERSHARE        = 51,
176             SIID_PRINTERFAX         = 52,
177             SIID_PRINTERFAXNET      = 53,
178             SIID_PRINTERFILE        = 54,
179             SIID_STACK              = 55,
180             SIID_MEDIASVCD          = 56,
181             SIID_STUFFEDFOLDER      = 57,
182             SIID_DRIVEUNKNOWN       = 58,
183             SIID_DRIVEDVD           = 59,
184             SIID_MEDIADVD           = 60,
185             SIID_MEDIADVDRAM        = 61,
186             SIID_MEDIADVDRW         = 62,
187             SIID_MEDIADVDR          = 63,
188             SIID_MEDIADVDROM        = 64,
189             SIID_MEDIACDAUDIOPLUS   = 65,
190             SIID_MEDIACDRW          = 66,
191             SIID_MEDIACDR           = 67,
192             SIID_MEDIACDBURN        = 68,
193             SIID_MEDIABLANKCD       = 69,
194             SIID_MEDIACDROM         = 70,
195             SIID_AUDIOFILES         = 71,
196             SIID_IMAGEFILES         = 72,
197             SIID_VIDEOFILES         = 73,
198             SIID_MIXEDFILES         = 74,
199             SIID_FOLDERBACK         = 75,
200             SIID_FOLDERFRONT        = 76,
201             SIID_SHIELD             = 77,
202             SIID_WARNING            = 78,
203             SIID_INFO               = 79,
204             SIID_ERROR              = 80,
205             SIID_KEY                = 81,
206             SIID_SOFTWARE           = 82,
207             SIID_RENAME             = 83,
208             SIID_DELETE             = 84,
209             SIID_MEDIAAUDIODVD      = 85,
210             SIID_MEDIAMOVIEDVD      = 86,
211             SIID_MEDIAENHANCEDCD    = 87,
212             SIID_MEDIAENHANCEDDVD   = 88,
213             SIID_MEDIAHDDVD         = 89,
214             SIID_MEDIABLURAY        = 90,
215             SIID_MEDIAVCD           = 91,
216             SIID_MEDIADVDPLUSR      = 92,
217             SIID_MEDIADVDPLUSRW     = 93,
218             SIID_DESKTOPPC          = 94,
219             SIID_MOBILEPC           = 95,
220             SIID_USERS              = 96,
221             SIID_MEDIASMARTMEDIA    = 97,
222             SIID_MEDIACOMPACTFLASH  = 98,
223             SIID_DEVICECELLPHONE    = 99,
224             SIID_DEVICECAMERA       = 100,
225             SIID_DEVICEVIDEOCAMERA  = 101,
226             SIID_DEVICEAUDIOPLAYER  = 102,
227             SIID_NETWORKCONNECT     = 103,
228             SIID_INTERNET           = 104,
229             SIID_ZIPFILE            = 105,
230             SIID_SETTINGS           = 106,
231             SIID_DRIVEHDDVD         = 132,
232             SIID_DRIVEBD            = 133,
233             SIID_MEDIAHDDVDROM      = 134,
234             SIID_MEDIAHDDVDR        = 135,
235             SIID_MEDIAHDDVDRAM      = 136,
236             SIID_MEDIABDROM         = 137,
237             SIID_MEDIABDR           = 138,
238             SIID_MEDIABDRE          = 139,
239             SIID_CLUSTEREDDRIVE     = 140,
240             SIID_MAX_ICONS          = 175
241         };
242 
243         private struct SHSTOCKICONINFO {
244             DWORD cbSize;
245             HICON hIcon;
246             int   iSysImageIndex;
247             int   iIcon;
248             WCHAR[MAX_PATH] szPath;
249         };
250 
251         private extern(Windows) HRESULT _dummy_SHGetStockIconInfo(SHSTOCKICONID siid, UINT uFlags, SHSTOCKICONINFO *psii);
252 
253         class WindowsIconProvider : IconProviderBase
254         {
255             this()
256             {
257                 import std.windows.syserror;
258                 _shell = wenforce(LoadLibraryA("Shell32"), "Could not load Shell32 library");
259                 _SHGetStockIconInfo = cast(typeof(&_dummy_SHGetStockIconInfo))wenforce(GetProcAddress(_shell, "SHGetStockIconInfo"), "Could not load SHGetStockIconInfo");
260             }
261             ~this()
262             {
263                 if (_shell) {
264                     FreeLibrary(_shell);
265                 }
266                 foreach(ref buf; _cache)
267                 {
268                     buf.clear();
269                 }
270             }
271 
272             DrawBufRef getIconFromStock(SHSTOCKICONID id)
273             {
274                 if (_SHGetStockIconInfo) {
275                     auto found = id in _cache;
276                     if (found) {
277                         return *found;
278                     }
279                     HICON icon = getStockIcon(id);
280                     if (icon) {
281                         scope(exit) DestroyIcon(icon);
282                         auto image = DrawBufRef(iconToImage(icon));
283                         _cache[id] = image;
284                         return image;
285                     } else {
286                         _cache[id] = DrawBufRef(null); // save the fact that the icon was not found
287                     }
288                 }
289                 return DrawBufRef(null);
290             }
291 
292             override DrawBufRef getStandardIcon(StandardIcon icon)
293             {
294                 if (_SHGetStockIconInfo) {
295                     return getIconFromStock(standardIconToStockId(icon));
296                 }
297                 return DrawBufRef(null);
298             }
299 
300         private:
301             SHSTOCKICONID standardIconToStockId(StandardIcon icon) nothrow pure
302             {
303                 with(SHSTOCKICONID)
304                 final switch(icon) with(StandardIcon)
305                 {
306                     case document:
307                         return SIID_DOCASSOC;
308                     case application:
309                         return SIID_APPLICATION;
310                     case folder:
311                         return SIID_FOLDER;
312                     case folderOpen:
313                         return SIID_FOLDEROPEN;
314                     case driveFloppy:
315                         return SIID_DRIVE35;
316                     case driveRemovable:
317                         return SIID_DRIVEREMOVE;
318                     case driveFixed:
319                         return SIID_DRIVEFIXED;
320                     case driveCD:
321                         return SIID_DRIVECD;
322                     case driveDVD:
323                         return SIID_DRIVEDVD;
324                     case server:
325                         return SIID_SERVER;
326                     case printer:
327                         return SIID_PRINTER;
328                     case find:
329                         return SIID_FIND;
330                     case help:
331                         return SIID_HELP;
332                     case sharedItem:
333                         return SIID_SHARE;
334                     case link:
335                         return SIID_LINK;
336                     case trashcanEmpty:
337                         return SIID_RECYCLER;
338                     case trashcanFull:
339                         return SIID_RECYCLERFULL;
340                     case mediaCDAudio:
341                         return SIID_MEDIACDAUDIO;
342                     case mediaDVDAudio:
343                         return SIID_MEDIAAUDIODVD;
344                     case mediaDVD:
345                         return SIID_MEDIADVD;
346                     case mediaCD:
347                         return SIID_MEDIABLANKCD;
348                     case fileAudio:
349                         return SIID_AUDIOFILES;
350                     case fileImage:
351                         return SIID_IMAGEFILES;
352                     case fileVideo:
353                         return SIID_VIDEOFILES;
354                     case fileZip:
355                         return SIID_ZIPFILE;
356                     case fileUnknown:
357                         return SIID_DOCNOASSOC;
358                     case warning:
359                         return SIID_WARNING;
360                     case information:
361                         return SIID_INFO;
362                     case error:
363                         return SIID_ERROR;
364                     case password:
365                         return SIID_KEY;
366                     case rename:
367                         return SIID_RENAME;
368                     case deleteItem:
369                         return SIID_DELETE;
370                     case computer:
371                         return SIID_DESKTOPPC;
372                     case laptop:
373                         return SIID_MOBILEPC;
374                     case users:
375                         return SIID_USERS;
376                     case deviceCellphone:
377                         return SIID_DEVICECELLPHONE;
378                     case deviceCamera:
379                         return SIID_DEVICECAMERA;
380                     case deviceCameraVideo:
381                         return SIID_DEVICEVIDEOCAMERA;
382                 }
383             }
384 
385             HICON getStockIcon(SHSTOCKICONID id)
386             {
387                 assert(_SHGetStockIconInfo !is null);
388                 enum SHGSI_ICON = 0x000000100;
389                 SHSTOCKICONINFO info;
390                 info.cbSize = SHSTOCKICONINFO.sizeof;
391                 if (_SHGetStockIconInfo(id, SHGSI_ICON, &info) == S_OK) {
392                     return info.hIcon;
393                 }
394                 Log.d("Could not get icon from stock. Id: ", id);
395                 return null;
396             }
397 
398             ColorDrawBuf iconToImage(HICON hIcon)
399             {
400                 BITMAP bm;
401                 ICONINFO iconInfo;
402                 GetIconInfo(hIcon, &iconInfo);
403                 GetObject(iconInfo.hbmColor, BITMAP.sizeof, &bm);
404                 const int width = bm.bmWidth;
405                 const int height = bm.bmHeight;
406                 const int bytesPerScanLine = (width * 3 + 3) & 0xFFFFFFFC;
407                 const int size = bytesPerScanLine * height;
408                 BITMAPINFO infoheader;
409                 infoheader.bmiHeader.biSize = BITMAPINFOHEADER.sizeof;
410                 infoheader.bmiHeader.biWidth = width;
411                 infoheader.bmiHeader.biHeight = height;
412                 infoheader.bmiHeader.biPlanes = 1;
413                 infoheader.bmiHeader.biBitCount = 24;
414                 infoheader.bmiHeader.biCompression = BI_RGB;
415                 infoheader.bmiHeader.biSizeImage = size;
416 
417                 ubyte[] pixelsIconRGB = new ubyte[size];
418                 ubyte[] alphaPixels	= new ubyte[size];
419                 HDC hDC = CreateCompatibleDC(null);
420                 scope(exit) DeleteDC(hDC);
421 
422                 HBITMAP hBmpOld = cast(HBITMAP)SelectObject(hDC, cast(HGDIOBJ)(iconInfo.hbmColor));
423                 if(!GetDIBits(hDC, iconInfo.hbmColor, 0, height, cast(LPVOID) pixelsIconRGB.ptr, &infoheader, DIB_RGB_COLORS))
424                     return null;
425                 SelectObject(hDC, hBmpOld);
426 
427                 if(!GetDIBits(hDC, iconInfo.hbmMask, 0,height,cast(LPVOID)alphaPixels.ptr, &infoheader, DIB_RGB_COLORS))
428                     return null;
429 
430                 const int lsSrc = width*3;
431                 auto colorDrawBuf = new ColorDrawBuf(width, height);
432                 for(int y=0; y<height; y++)
433                 {
434                     const int linePosSrc = (height-1-y)*lsSrc;
435                     auto pixelLine = colorDrawBuf.scanLine(y);
436                     for(int x=0; x<width; x++)
437                     {
438                         const int currentSrcPos  = linePosSrc+x*3;
439                         // BGR -> ARGB
440                         const uint red = pixelsIconRGB[currentSrcPos+2];
441                         const uint green = pixelsIconRGB[currentSrcPos+1];
442                         const uint blue = pixelsIconRGB[currentSrcPos];
443                         const uint alpha = alphaPixels[currentSrcPos];
444                         const uint color = (red << 16) | (green << 8) | blue | (alpha << 24);
445                         pixelLine[x] = color;
446                     }
447                 }
448                 return colorDrawBuf;
449             }
450 
451             DrawBufRef[SHSTOCKICONID] _cache;
452             HANDLE _shell;
453             typeof(&_dummy_SHGetStockIconInfo) _SHGetStockIconInfo;
454         }
455 
456         alias WindowsIconProvider NativeIconProvider;
457     } else static if (isFreedesktop) {
458         import icontheme;
459         import std.typecons : tuple;
460         import dlangui.graphics.images;
461         class FreedesktopIconProvider : IconProviderBase
462         {
463             this()
464             {
465                 _baseIconDirs = baseIconDirs();
466                 auto themeName = currentIconThemeName();
467                 IconThemeFile iconTheme = openIconTheme(themeName, _baseIconDirs);
468                 if (iconTheme) {
469                     _iconThemes ~= iconTheme;
470                     _iconThemes ~= openBaseThemes(iconTheme, _baseIconDirs);
471                 }
472                 foreach(theme; _iconThemes) {
473                     theme.tryLoadCache();
474                 }
475             }
476 
477             ~this()
478             {
479                 foreach(ref buf; _cache)
480                 {
481                     buf.clear();
482                 }
483             }
484 
485             DrawBufRef getIconFromTheme(string name, string context = null)
486             {
487                 static if (!WIDGET_STYLE_CONSOLE) {
488                     immutable extensions = [".svg",".png"];
489                     auto found = name in _cache;
490                     if (found) {
491                         return *found;
492                     }
493                     string iconPath;
494                     try {
495                         if (context.length) {
496                             // Take the context into account to reduce the number of searches.
497                             // In practice some icons that should be in Status context can be found in Places context for some icon themes.
498                             iconPath = findClosestIcon!(subdir => subdir.context == context || (context == "Status" && subdir.context == "Places"))(name, 32, _iconThemes, _baseIconDirs, extensions);
499                         } else {
500                             iconPath = findClosestIcon(name, 32, _iconThemes, _baseIconDirs, extensions);
501                         }
502                     } catch(Exception e) {
503                         Log.e("Error while searching for icon", name);
504                         Log.e(e);
505                     }
506 
507                     if (iconPath.length) {
508                         auto image = DrawBufRef(loadImage(iconPath));
509                         _cache[name] = image;
510                         return image;
511                     } else {
512                         _cache[name] = DrawBufRef(null);
513                     }
514                 }
515                 return DrawBufRef(null);
516             }
517 
518             override DrawBufRef getStandardIcon(StandardIcon icon)
519             {
520                 auto t = standardIconToNameAndContext(icon);
521                 return getIconFromTheme(t[0], t[1]);
522             }
523 
524         private:
525             auto standardIconToNameAndContext(StandardIcon icon) nothrow pure
526             {
527                 final switch(icon) with(StandardIcon)
528                 {
529                     case document:
530                         return tuple("x-office-document", "MimeTypes");
531                     case application:
532                         return tuple("application-x-executable", "MimeTypes");
533                     case folder:
534                         return tuple("folder", "Places");
535                     case folderOpen:
536                         return tuple("folder-open", "Status");
537                     case driveFloppy:
538                         return tuple("media-floppy", "Devices");
539                     case driveRemovable:
540                         return tuple("drive-removable-media", "Devices");
541                     case driveFixed:
542                         return tuple("drive-harddisk", "Devices");
543                     case driveCD:
544                         return tuple("drive-optical", "Devices");
545                     case driveDVD:
546                         return tuple("drive-optical", "Devices");
547                     case server:
548                         return tuple("network-server", "Places");
549                     case printer:
550                         return tuple("printer", "Devices");
551                     case find:
552                         return tuple("edit-find", "Actions");
553                     case help:
554                         return tuple("help-contents", "Actions");
555                     case sharedItem:
556                         return tuple("emblem-shared", "Emblems");
557                     case link:
558                         return tuple("emblem-symbolic-link", "Emblems");
559                     case trashcanEmpty:
560                         return tuple("user-trash", "Places");
561                     case trashcanFull:
562                         return tuple("user-trash-full", "Status");
563                     case mediaCDAudio:
564                         return tuple("media-optical-audio", "Devices");
565                     case mediaDVDAudio:
566                         return tuple("media-optical-audio", "Devices");
567                     case mediaDVD:
568                         return tuple("media-optical", "Devices");
569                     case mediaCD:
570                         return tuple("media-optical", "Devices");
571                     case fileAudio:
572                         return tuple("audio-x-generic", "MimeTypes");
573                     case fileImage:
574                         return tuple("image-x-generic", "MimeTypes");
575                     case fileVideo:
576                         return tuple("video-x-generic", "MimeTypes");
577                     case fileZip:
578                         return tuple("application-zip", "MimeTypes");
579                     case fileUnknown:
580                         return tuple("unknown", "MimeTypes");
581                     case warning:
582                         return tuple("dialog-warning", "Status");
583                     case information:
584                         return tuple("dialog-information", "Status");
585                     case error:
586                         return tuple("dialog-error", "Status");
587                     case password:
588                         return tuple("dialog-password", "Status");
589                     case rename:
590                         return tuple("edit-rename", "Actions");
591                     case deleteItem:
592                         return tuple("edit-delete", "Actions");
593                     case computer:
594                         return tuple("computer", "Devices");
595                     case laptop:
596                         return tuple("computer-laptop", "Devices");
597                     case users:
598                         return tuple("system-users", "Applications");
599                     case deviceCellphone:
600                         return tuple("phone", "Devices");
601                     case deviceCamera:
602                         return tuple("camera-photo", "Devices");
603                     case deviceCameraVideo:
604                         return tuple("camera-video", "Devices");
605                 }
606             }
607 
608             DrawBufRef[string] _cache;
609             string[] _baseIconDirs;
610             IconThemeFile[] _iconThemes;
611         }
612         alias FreedesktopIconProvider NativeIconProvider;
613     } else {
614         alias DummyIconProvider NativeIconProvider;
615     }
616 } else { // !BACKEND_CONSOLE
617     alias DummyIconProvider NativeIconProvider;
618 }