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 auto found = name in _cache; 489 if (found) { 490 return *found; 491 } 492 string iconPath; 493 try { 494 if (context.length) { 495 iconPath = findClosestIcon!(subdir => subdir.context == context)(name, 32, _iconThemes, _baseIconDirs); 496 } else { 497 iconPath = findClosestIcon(name, 32, _iconThemes, _baseIconDirs); 498 } 499 } catch(Exception e) { 500 Log.e("Error while searching for icon", name); 501 Log.e(e); 502 } 503 504 if (iconPath.length) { 505 auto image = DrawBufRef(loadImage(iconPath)); 506 _cache[name] = image; 507 return image; 508 } else { 509 _cache[name] = DrawBufRef(null); 510 } 511 } 512 return DrawBufRef(null); 513 } 514 515 override DrawBufRef getStandardIcon(StandardIcon icon) 516 { 517 auto t = standardIconToNameAndContext(icon); 518 return getIconFromTheme(t[0], t[1]); 519 } 520 521 private: 522 auto standardIconToNameAndContext(StandardIcon icon) nothrow pure 523 { 524 final switch(icon) with(StandardIcon) 525 { 526 case document: 527 return tuple("x-office-document", "MimeTypes"); 528 case application: 529 return tuple("application-x-executable", "MimeTypes"); 530 case folder: 531 return tuple("folder", "Places"); 532 case folderOpen: 533 return tuple("folder-open", "Status"); 534 case driveFloppy: 535 return tuple("media-floppy", "Devices"); 536 case driveRemovable: 537 return tuple("drive-removable-media", "Devices"); 538 case driveFixed: 539 return tuple("drive-harddisk", "Devices"); 540 case driveCD: 541 return tuple("drive-optical", "Devices"); 542 case driveDVD: 543 return tuple("drive-optical", "Devices"); 544 case server: 545 return tuple("network-server", "Places"); 546 case printer: 547 return tuple("printer", "Devices"); 548 case find: 549 return tuple("edit-find", "Actions"); 550 case help: 551 return tuple("help-contents", "Actions"); 552 case sharedItem: 553 return tuple("emblem-shared", "Emblems"); 554 case link: 555 return tuple("emblem-symbolic-link", "Emblems"); 556 case trashcanEmpty: 557 return tuple("user-trash", "Places"); 558 case trashcanFull: 559 return tuple("user-trash-full", "Status"); 560 case mediaCDAudio: 561 return tuple("media-optical-audio", "Devices"); 562 case mediaDVDAudio: 563 return tuple("media-optical-audio", "Devices"); 564 case mediaDVD: 565 return tuple("media-optical", "Devices"); 566 case mediaCD: 567 return tuple("media-optical", "Devices"); 568 case fileAudio: 569 return tuple("audio-x-generic", "MimeTypes"); 570 case fileImage: 571 return tuple("image-x-generic", "MimeTypes"); 572 case fileVideo: 573 return tuple("video-x-generic", "MimeTypes"); 574 case fileZip: 575 return tuple("application-zip", "MimeTypes"); 576 case fileUnknown: 577 return tuple("unknown", "MimeTypes"); 578 case warning: 579 return tuple("dialog-warning", "Status"); 580 case information: 581 return tuple("dialog-information", "Status"); 582 case error: 583 return tuple("dialog-error", "Status"); 584 case password: 585 return tuple("dialog-password", "Status"); 586 case rename: 587 return tuple("edit-rename", "Actions"); 588 case deleteItem: 589 return tuple("edit-delete", "Actions"); 590 case computer: 591 return tuple("computer", "Devices"); 592 case laptop: 593 return tuple("computer-laptop", "Devices"); 594 case users: 595 return tuple("system-users", "Applications"); 596 case deviceCellphone: 597 return tuple("phone", "Devices"); 598 case deviceCamera: 599 return tuple("camera-photo", "Devices"); 600 case deviceCameraVideo: 601 return tuple("camera-video", "Devices"); 602 } 603 } 604 605 DrawBufRef[string] _cache; 606 string[] _baseIconDirs; 607 IconThemeFile[] _iconThemes; 608 } 609 alias FreedesktopIconProvider NativeIconProvider; 610 } else { 611 alias DummyIconProvider NativeIconProvider; 612 } 613 } else { // !BACKEND_CONSOLE 614 alias DummyIconProvider NativeIconProvider; 615 }