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 }