1 /** 2 * Lookup of icon themes and icons. 3 * 4 * Note: All found icons are just paths. They are not verified to be valid images. 5 * 6 * Authors: 7 * $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov) 8 * Copyright: 9 * Roman Chistokhodov, 2015-2016 10 * License: 11 * $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 12 * See_Also: 13 * $(LINK2 http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html, Icon Theme Specification) 14 */ 15 16 module icontheme.lookup; 17 18 import icontheme.file; 19 20 package { 21 import std.file; 22 import std.path; 23 import std.range; 24 import std.traits; 25 import std.typecons; 26 } 27 28 @trusted bool isDirNothrow(string dir) nothrow 29 { 30 bool ok; 31 collectException(dir.isDir(), ok); 32 return ok; 33 } 34 35 @trusted bool isFileNothrow(string file) nothrow 36 { 37 bool ok; 38 collectException(file.isFile(), ok); 39 return ok; 40 } 41 42 @trusted InputRange!DirEntry dirEntriesNothrow(string path, SpanMode mode) nothrow 43 { 44 try { 45 return inputRangeObject(dirEntries(path, mode)); 46 } catch(Exception e) { 47 return inputRangeObject(DirEntry[].init); 48 } 49 } 50 51 /** 52 * Default icon extensions. This array includes .png and .xpm. 53 * PNG is recommended format. 54 * XPM is kept for backward compatibility. 55 * 56 * Note: Icon Theme Specificiation also lists .svg as possible format, 57 * but it's less common to have SVG support for applications, 58 * hence this format is defined as optional by specificiation. 59 * If your application has proper support for SVG images, 60 * array should include it in the first place as the most preferred format 61 * because SVG images are scalable. 62 */ 63 enum defaultIconExtensions = [".png", ".xpm"]; 64 65 /** 66 * Convenient constant for the default icon theme name. 67 */ 68 enum defaultGenericIconTheme = "hicolor"; 69 70 /// 71 deprecated("use defaultGenericIconTheme") alias defaultFallbackIconTheme = defaultGenericIconTheme; 72 73 /** 74 * Find all icon themes in searchIconDirs. 75 * Note: 76 * You may want to skip icon themes duplicates if there're different versions of the index.theme file for the same theme. 77 * Returns: 78 * Range of paths to index.theme files represented icon themes. 79 * Params: 80 * searchIconDirs = base icon directories to search icon themes. 81 * See_Also: $(D icontheme.paths.baseIconDirs) 82 */ 83 auto iconThemePaths(Range)(Range searchIconDirs) 84 if(is(ElementType!Range : string)) 85 { 86 return searchIconDirs.map!(function(iconDir) { 87 return iconDir.dirEntriesNothrow(SpanMode.shallow) 88 .map!(p => buildPath(p, "index.theme")).cache() 89 .filter!(isFileNothrow); 90 }).joiner; 91 } 92 93 /// 94 version(iconthemeFileTest) unittest 95 { 96 auto paths = iconThemePaths(["test"]).array; 97 assert(paths.length == 3); 98 assert(paths.canFind(buildPath("test", "NewTango", "index.theme"))); 99 assert(paths.canFind(buildPath("test", "Tango", "index.theme"))); 100 assert(paths.canFind(buildPath("test", "hicolor", "index.theme"))); 101 } 102 103 /** 104 * Lookup index.theme files by theme name. 105 * Params: 106 * themeName = Theme name (as the base name of theme subdirectory). 107 * searchIconDirs = Base icon directories to search icon themes. 108 * Returns: 109 * Range of paths to index.theme file corresponding to the given theme. 110 * Note: 111 * Usually you want to use the only first found file. 112 * See_Also: $(D icontheme.paths.baseIconDirs), $(D findIconTheme) 113 */ 114 auto lookupIconTheme(Range)(string themeName, Range searchIconDirs) 115 if(is(ElementType!Range : string)) 116 { 117 return searchIconDirs 118 .map!(dir => buildPath(dir, themeName, "index.theme")).cache() 119 .filter!(isFileNothrow); 120 } 121 122 /** 123 * Find index.theme file by theme name. 124 * Returns: 125 * Path to the first found index.theme file or null string if not found. 126 * Params: 127 * themeName = Theme name (as the base name of theme subdirectory). 128 * searchIconDirs = Base icon directories to search icon themes. 129 * Returns: 130 * Path to the first found index.theme file corresponding to the given theme. 131 * See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIconTheme) 132 */ 133 auto findIconTheme(Range)(string themeName, Range searchIconDirs) 134 { 135 auto paths = lookupIconTheme(themeName, searchIconDirs); 136 if (paths.empty) { 137 return null; 138 } else { 139 return paths.front; 140 } 141 } 142 143 /** 144 * Find index.theme file for given theme and create instance of $(D icontheme.file.IconThemeFile). The first found file will be used. 145 * Returns: $(D icontheme.file.IconThemeFile) object read from the first found index.theme file corresponding to given theme or null if none were found. 146 * Params: 147 * themeName = Theme name (as the base name of theme subdirectory). 148 * searchIconDirs = Base icon directories to search icon themes. 149 * options = options for $(D icontheme.file.IconThemeFile) reading. 150 * Throws: 151 * $(B ErrnoException) if file could not be opened. 152 * $(B IniLikeException) if error occured while reading the file. 153 * See_Also: $(D findIconTheme), $(D icontheme.paths.baseIconDirs) 154 */ 155 IconThemeFile openIconTheme(Range)(string themeName, 156 Range searchIconDirs, 157 IconThemeFile.IconThemeReadOptions options = IconThemeFile.IconThemeReadOptions.init) 158 { 159 auto path = findIconTheme(themeName, searchIconDirs); 160 return path.empty ? null : new IconThemeFile(to!string(path), options); 161 } 162 163 /// 164 version(iconthemeFileTest) unittest 165 { 166 auto tango = openIconTheme("Tango", ["test"]); 167 assert(tango); 168 assert(tango.displayName() == "Tango"); 169 170 auto hicolor = openIconTheme("hicolor", ["test"]); 171 assert(hicolor); 172 assert(hicolor.displayName() == "Hicolor"); 173 174 assert(openIconTheme("Nonexistent", ["test"]) is null); 175 } 176 177 /** 178 * Result of icon lookup. 179 */ 180 struct IconSearchResult(IconTheme) if (is(IconTheme : const(IconThemeFile))) 181 { 182 /** 183 * File path of found icon. 184 */ 185 string filePath; 186 /** 187 * Subdirectory the found icon belongs to. 188 */ 189 IconSubDir subdir; 190 /** 191 * $(D icontheme.file.IconThemeFile) the found icon belongs to. 192 */ 193 Rebindable!IconTheme iconTheme; 194 } 195 196 /** 197 * Lookup icon alternatives in icon themes. It uses icon theme cache wherever it's loaded. If searched icon is found in some icon theme all subsequent themes are ignored. 198 * 199 * This function may require many $(B stat) calls, so beware. Use subdirFilter to filter icons by $(D icontheme.file.IconSubDir) properties (e.g. by size or context) to decrease the number of searchable items and allocations. Loading $(D icontheme.cache.IconThemeCache) may also descrease the number of stats. 200 * 201 * Params: 202 * iconName = Icon name. 203 * iconThemes = Icon themes to search icon in. 204 * searchIconDirs = Case icon directories. 205 * extensions = Possible file extensions of needed icon file, in order of preference. 206 * sink = Output range accepting $(D IconSearchResult)s. 207 * reverse = Iterate over icon theme sub-directories in reverse way. 208 * Usually directories with larger icon size are listed the last, 209 * so this parameter may speed up the search when looking for the largest icon. 210 * Note: Specification says that extension must be ".png", ".xpm" or ".svg", though SVG is not required to be supported. Some icon themes also contain .svgz images. 211 * Example: 212 ---------- 213 lookupIcon!(subdir => subdir.context == "Places" && subdir.size >= 32)( 214 "folder", iconThemes, baseIconDirs(), [".png", ".xpm"], 215 delegate void (IconSearchResult!IconThemeFile item) { 216 writefln("Icon file: %s. Context: %s. Size: %s. Theme: %s", item.filePath, item.subdir.context, item.subdir.size, item.iconTheme.displayName); 217 }); 218 ---------- 219 * See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupNonThemedIcon) 220 */ 221 void lookupIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts, OutputRange)(string iconName, IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions, OutputRange sink, Flag!"reverse" reverse = No.reverse) 222 if (isInputRange!(IconThemes) && isForwardRange!(BaseDirs) && isForwardRange!(Exts) && 223 is(ElementType!IconThemes : const(IconThemeFile)) && is(ElementType!BaseDirs : string) && 224 is(ElementType!Exts : string) && isOutputRange!(OutputRange, IconSearchResult!(ElementType!IconThemes))) 225 { 226 bool onExtensions(string themeBaseDir, IconSubDir subdir, ElementType!IconThemes iconTheme) 227 { 228 string subdirPath = buildPath(themeBaseDir, subdir.name); 229 if (!subdirPath.isDirNothrow) { 230 return false; 231 } 232 bool found; 233 foreach(extension; extensions) { 234 string path = buildPath(subdirPath, iconName ~ extension); 235 if (path.isFileNothrow) { 236 found = true; 237 put(sink, IconSearchResult!(ElementType!IconThemes)(path, subdir, iconTheme)); 238 } 239 } 240 return found; 241 } 242 243 foreach(iconTheme; iconThemes) { 244 if (iconTheme is null || iconTheme.internalName().length == 0) { 245 continue; 246 } 247 248 string[] themeBaseDirs = searchIconDirs.map!(dir => buildPath(dir, iconTheme.internalName())).filter!(isDirNothrow).array; 249 250 bool found; 251 252 auto bySubdir = choose(reverse, iconTheme.bySubdir().retro(), iconTheme.bySubdir()); 253 foreach(subdir; bySubdir) { 254 if (!subdirFilter(subdir)) { 255 continue; 256 } 257 foreach(themeBaseDir; themeBaseDirs) { 258 if (iconTheme.cache !is null && themeBaseDir == iconTheme.cache.fileName.dirName) { 259 if (iconTheme.cache.containsIcon(iconName, subdir.name)) { 260 found = onExtensions(themeBaseDir, subdir, iconTheme) || found; 261 } 262 } else { 263 found = onExtensions(themeBaseDir, subdir, iconTheme) || found; 264 } 265 } 266 } 267 if (found) { 268 return; 269 } 270 } 271 } 272 273 /** 274 * Iterate over all icons in icon themes. 275 * iconThemes is usually the range of the main theme and themes it inherits from. 276 * Note: Usually if some icon was found in icon theme, it should be ignored in all subsequent themes, including sizes not presented in former theme. 277 * Use subdirFilter to filter icons by $(D icontheme.file.IconSubDir) thus decreasing the number of searchable items and allocations. 278 * Returns: Range of $(D IconSearchResult). 279 * Params: 280 * iconThemes = Icon themes to search icon in. 281 * searchIconDirs = Base icon directories. 282 * extensions = possible file extensions for icon files. 283 * Example: 284 ------------- 285 foreach(item; lookupThemeIcons!(subdir => subdir.context == "MimeTypes" && subdir.size >= 32)(iconThemes, baseIconDirs(), [".png", ".xpm"])) 286 { 287 writefln("Icon file: %s. Context: %s. Size: %s", item.filePath, item.subdir.context, item.subdir.size); 288 } 289 ------------- 290 * See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D openBaseThemes) 291 */ 292 293 auto lookupThemeIcons(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts)(IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions) 294 if (is(ElementType!IconThemes : const(IconThemeFile)) && is(ElementType!BaseDirs : string) && is (ElementType!Exts : string)) 295 { 296 return iconThemes.filter!(iconTheme => iconTheme !is null).map!( 297 iconTheme => iconTheme.bySubdir().filter!(subdirFilter).map!( 298 subdir => searchIconDirs.map!( 299 basePath => buildPath(basePath, iconTheme.internalName(), subdir.name) 300 ).map!( 301 subdirPath => subdirPath.dirEntriesNothrow(SpanMode.shallow).filter!( 302 filePath => filePath.isFileNothrow && extensions.canFind(filePath.extension) 303 ).map!(filePath => IconSearchResult!(ElementType!IconThemes)(filePath, subdir, iconTheme)) 304 ).joiner 305 ).joiner 306 ).joiner; 307 } 308 309 /** 310 * Iterate over all icons out of icon themes. 311 * Returns: Range of found icon file paths. 312 * Params: 313 * searchIconDirs = Base icon directories. 314 * extensions = Possible file extensions for icon files. 315 * See_Also: 316 * $(D lookupNonThemedIcon), $(D icontheme.paths.baseIconDirs) 317 */ 318 auto lookupNonThemedIcons(BaseDirs, Exts)(BaseDirs searchIconDirs, Exts extensions) 319 if (isInputRange!(BaseDirs) && isForwardRange!(Exts) && 320 is(ElementType!BaseDirs : string) && is(ElementType!Exts : string)) 321 { 322 return searchIconDirs.map!(basePath => basePath.dirEntriesNothrow(SpanMode.shallow).filter!( 323 filePath => filePath.isFileNothrow && extensions.canFind(filePath.extension) 324 )).joiner; 325 } 326 327 deprecated alias lookupFallbackIcons = lookupNonThemedIcons; 328 329 /** 330 * Lookup icon alternatives beyond the icon themes. May be used as fallback lookup, if $(D lookupIcon) returned empty range. 331 * Returns: The range of found icon file paths. 332 * Example: 333 ---------- 334 auto result = lookupNonThemedIcon("folder", baseIconDirs(), [".png", ".xpm"]); 335 ---------- 336 * See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D lookupNonThemedIcons) 337 */ 338 auto lookupNonThemedIcon(BaseDirs, Exts)(string iconName, BaseDirs searchIconDirs, Exts extensions) 339 if (isInputRange!(BaseDirs) && isForwardRange!(Exts) && 340 is(ElementType!BaseDirs : string) && is(ElementType!Exts : string)) 341 { 342 return searchIconDirs.map!(basePath => 343 extensions 344 .map!(extension => buildPath(basePath, iconName ~ extension)).cache() 345 .filter!(isFileNothrow) 346 ).joiner; 347 } 348 349 deprecated alias lookupFallbackIcon = lookupNonThemedIcon; 350 351 /** 352 * Find icon outside of icon themes. The first found is returned. 353 * See_Also: $(D lookupNonThemedIcon), $(D icontheme.paths.baseIconDirs) 354 */ 355 string findNonThemedIcon(BaseDirs, Exts)(string iconName, BaseDirs searchIconDirs, Exts extensions) 356 { 357 auto r = lookupNonThemedIcon(iconName, searchIconDirs, extensions); 358 if (r.empty) { 359 return null; 360 } else { 361 return r.front; 362 } 363 } 364 365 /// 366 version(iconthemeFileTest) unittest 367 { 368 assert(findNonThemedIcon("pidgin", ["test"], defaultIconExtensions) == buildPath("test", "pidgin.png")); 369 assert(findNonThemedIcon("nonexistent", ["test"], defaultIconExtensions).empty); 370 } 371 372 deprecated alias findFallbackIcon = findNonThemedIcon; 373 374 /** 375 * Find icon of the closest size. It uses icon theme cache wherever possible. The first perfect match is used. It searches only for icons in themes. 376 * Params: 377 * iconName = Name of icon to search as defined by Icon Theme Specification (i.e. without path and extension parts). 378 * desiredSize = Preferred icon size to get. 379 * iconThemes = Range of $(D icontheme.file.IconThemeFile) objects. 380 * searchIconDirs = Base icon directories. 381 * extensions = Allowed file extensions. 382 * Returns: $(D IconSearchResult). filePath will be empty if icon is not found. 383 * Note: If icon of some size was found in the icon theme, this algorithm does not check following themes, even if they contain icons with closer size. Therefore the icon found in the more preferred theme always has presedence over icons from other themes. 384 * See_Also: $(D findClosestIcon), $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D iconSizeDistance) 385 */ 386 auto findClosestThemedIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts)(string iconName, uint desiredSize, IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions) 387 { 388 uint minDistance = uint.max; 389 IconSearchResult!(ElementType!IconThemes) closest; 390 391 lookupIcon!(delegate bool(const(IconSubDir) subdir) { 392 return minDistance != 0 && subdirFilter(subdir) && iconSizeDistance(subdir, desiredSize) <= minDistance; 393 })(iconName, iconThemes, searchIconDirs, extensions, delegate void(IconSearchResult!(ElementType!IconThemes) t) { 394 uint distance = iconSizeDistance(t.subdir, desiredSize); 395 if (distance < minDistance) { 396 minDistance = distance; 397 closest = t; 398 } 399 }); 400 return closest; 401 } 402 403 /// 404 version(iconthemeFileTest) unittest 405 { 406 auto baseDirs = ["test"]; 407 auto iconThemes = [openIconTheme("Tango", baseDirs), openIconTheme("hicolor", baseDirs)]; 408 409 //exact match 410 auto found = findClosestThemedIcon("folder", 32, iconThemes, baseDirs); 411 assert(found.filePath == buildPath("test", "Tango", "32x32/places", "folder.png")); 412 assert(found.subdir.size == 32); 413 assert(found.subdir.context == "Places"); 414 assert(found.iconTheme.internalName == "Tango"); 415 416 found = findClosestThemedIcon("folder", 24, iconThemes, baseDirs); 417 assert(found.filePath == buildPath("test", "Tango", "24x24/devices", "folder.png")); 418 assert(found.subdir.size == 24); 419 assert(found.subdir.context == "Devices"); 420 421 // with subdir filter 422 found = findClosestThemedIcon!(subdir => subdir.context == "Places")("folder", 32, iconThemes, baseDirs); 423 assert(found.filePath == buildPath("test", "Tango", "32x32/places", "folder.png")); 424 assert(found.subdir.size == 32); 425 426 // non-exact match 427 found = findClosestThemedIcon!(subdir => subdir.context == "Places")("folder", 24, iconThemes, baseDirs); 428 assert(found.filePath == buildPath("test", "Tango", "32x32/places", "folder.png")); 429 assert(found.subdir.size == 32); 430 431 // no match, wrong subdir 432 found = findClosestThemedIcon!(subdir => subdir.context == "MimeTypes")("folder", 32, iconThemes, baseDirs); 433 assert(found.filePath.empty); 434 435 //hicolor has exact match, but Tango is more preferred. 436 found = findClosestThemedIcon("folder", 64, iconThemes, baseDirs); 437 assert(found.filePath == buildPath("test", "Tango", "32x32/places", "folder.png")); 438 assert(found.subdir.size == 32); 439 440 //find xpm 441 found = findClosestThemedIcon("folder", 32, iconThemes, baseDirs, [".xpm"]); 442 assert(found.filePath == buildPath("test", "Tango", "32x32/places", "folder.xpm")); 443 assert(found.subdir.size == 32); 444 445 //find big png, not exact match 446 found = findClosestThemedIcon("folder", 200, iconThemes, baseDirs); 447 assert(found.filePath == buildPath("test", "Tango", "128x128/places", "folder.png")); 448 assert(found.subdir.size == 128); 449 450 //svg is closer 451 found = findClosestThemedIcon("folder", 200, iconThemes, baseDirs, [".png", ".svg"]); 452 assert(found.filePath == buildPath("test", "Tango", "scalable/places", "folder.svg")); 453 assert(found.subdir.type == IconSubDir.Type.Scalable); 454 455 // exact match in hicolor 456 found = findClosestThemedIcon("text-plain", 48, iconThemes, baseDirs); 457 assert(found.filePath == buildPath("test", "hicolor", "48x48/mimetypes", "text-plain.png")); 458 assert(found.subdir.size == 48); 459 assert(found.subdir.context == "MimeTypes"); 460 assert(found.iconTheme.internalName == "hicolor"); 461 462 // with subdir filter 463 found = findClosestThemedIcon!(subdir => subdir.context == "MimeTypes")("text-plain", 48, iconThemes, baseDirs); 464 assert(found.filePath == buildPath("test", "hicolor", "48x48/mimetypes", "text-plain.png")); 465 assert(found.subdir.size == 48); 466 467 // no match 468 found = findClosestThemedIcon!(subdir => subdir.context == "Actions")("text-plain", 48, iconThemes, baseDirs); 469 assert(found.filePath.empty); 470 } 471 472 /** 473 * ditto, but with predefined extensions. 474 * See_Also: $(D defaultIconExtensions) 475 */ 476 auto findClosestThemedIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs)(string iconName, uint size, IconThemes iconThemes, BaseDirs searchIconDirs) 477 { 478 return findClosestThemedIcon!subdirFilter(iconName, size, iconThemes, searchIconDirs, defaultIconExtensions); 479 } 480 481 /** 482 * Find icon of the closest size. It uses icon theme cache wherever possible. The first perfect match is used. 483 * This is similar to $(D findClosestThemedIcon), but returns file path only and allows to search for non-themed icons. 484 * Params: 485 * iconName = Name of icon to search as defined by Icon Theme Specification (i.e. without path and extension parts). 486 * desiredSize = Preferred icon size to get. 487 * iconThemes = Range of $(D icontheme.file.IconThemeFile) objects. 488 * searchIconDirs = Base icon directories. 489 * extensions = Allowed file extensions. 490 * allowNonThemed = Allow searching for non-themed icon if could not find icon in themes (non-themed icon can be any size). 491 * Returns: Icon file path or empty string if not found. 492 * Note: If icon of some size was found in the icon theme, this algorithm does not check following themes, even if they contain icons with closer size. Therefore the icon found in the more preferred theme always has presedence over icons from other themes. 493 * See_Also: $(D findClosestThemedIcon), $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D findNonThemedIcon), $(D iconSizeDistance) 494 */ 495 string findClosestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts)(string iconName, uint desiredSize, IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions, Flag!"allowNonThemed" allowNonThemed = Yes.allowNonThemed) 496 { 497 string closest = findClosestThemedIcon!subdirFilter(iconName, desiredSize, iconThemes, searchIconDirs, extensions).filePath; 498 if (closest.empty && allowNonThemed) { 499 return findNonThemedIcon(iconName, searchIconDirs, extensions); 500 } else { 501 return closest; 502 } 503 } 504 505 /// 506 version(iconthemeFileTest) unittest 507 { 508 auto baseDirs = ["test"]; 509 auto iconThemes = [openIconTheme("Tango", baseDirs), openIconTheme("hicolor", baseDirs)]; 510 511 string found; 512 513 //exact match 514 found = findClosestIcon("folder", 32, iconThemes, baseDirs); 515 assert(found == buildPath("test", "Tango", "32x32/places", "folder.png")); 516 517 // with subdir filter 518 found = findClosestIcon!(subdir => subdir.context == "Places")("folder", 32, iconThemes, baseDirs); 519 assert(found == buildPath("test", "Tango", "32x32/places", "folder.png")); 520 521 // not exact match 522 found = findClosestIcon!(subdir => subdir.context == "Places")("folder", 24, iconThemes, baseDirs); 523 assert(found == buildPath("test", "Tango", "32x32/places", "folder.png")); 524 525 // no match, wrong subdir 526 found = findClosestIcon!(subdir => subdir.context == "MimeTypes")("folder", 32, iconThemes, baseDirs); 527 assert(found.empty); 528 529 //hicolor has exact match, but Tango is more preferred. 530 found = findClosestIcon("folder", 64, iconThemes, baseDirs); 531 assert(found == buildPath("test", "Tango", "32x32/places", "folder.png")); 532 533 //find xpm 534 found = findClosestIcon("folder", 32, iconThemes, baseDirs, [".xpm"]); 535 assert(found == buildPath("test", "Tango", "32x32/places", "folder.xpm")); 536 537 //lookup non-themed 538 found = findClosestIcon("pidgin", 96, iconThemes, baseDirs); 539 assert(found == buildPath("test", "pidgin.png")); 540 541 //don't lookup non-themed 542 found = findClosestIcon("pidgin", 96, iconThemes, baseDirs, defaultIconExtensions, No.allowNonThemed); 543 assert(found.empty); 544 } 545 546 deprecated string findClosestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts)(string iconName, uint desiredSize, IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions, Flag!"allowFallbackIcon" allowFallback) 547 { 548 return findClosestIcon!subdirFilter(iconName, desiredSize, iconThemes, searchIconDirs, extensions, cast(Flag!"allowNonThemed")allowFallback); 549 } 550 551 /** 552 * ditto, but with predefined extensions and non-themed icons allowed. 553 * See_Also: $(D defaultIconExtensions) 554 */ 555 string findClosestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs)(string iconName, uint size, IconThemes iconThemes, BaseDirs searchIconDirs) 556 { 557 return findClosestIcon!subdirFilter(iconName, size, iconThemes, searchIconDirs, defaultIconExtensions); 558 } 559 560 /** 561 * Find icon of the largest size. It uses icon theme cache wherever possible. 562 * Params: 563 * iconName = Name of icon to search as defined by Icon Theme Specification (i.e. without path and extension parts). 564 * iconThemes = Range of $(D icontheme.file.IconThemeFile) objects. 565 * searchIconDirs = Base icon directories. 566 * extensions = Allowed file extensions. 567 * Returns: $(D IconSearchResult). filePath will be empty if icon is not found. 568 * Note: If icon of some size was found in the icon theme, this algorithm does not check following themes, even if they contain icons with larger size. Therefore the icon found in the most preferred theme always has presedence over icons from other themes. 569 * See_Also: $(D findLargestIcon), $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D findNonThemedIcon) 570 */ 571 auto findLargestThemedIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts)(string iconName, IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions) 572 { 573 uint max = 0; 574 IconSearchResult!(ElementType!IconThemes) largest; 575 576 lookupIcon!(delegate bool(const(IconSubDir) subdir) { 577 return subdirFilter(subdir) && subdir.size() >= max; 578 })(iconName, iconThemes, searchIconDirs, extensions, delegate void(IconSearchResult!(ElementType!IconThemes) t) { 579 if (t.subdir.size() > max) { 580 max = t.subdir.size(); 581 largest = t; 582 } 583 }, Yes.reverse); 584 585 return largest; 586 } 587 588 /// 589 version(iconthemeFileTest) unittest 590 { 591 auto baseDirs = ["test"]; 592 auto iconThemes = [openIconTheme("Tango", baseDirs), openIconTheme("hicolor", baseDirs)]; 593 594 auto found = findLargestThemedIcon("folder", iconThemes, baseDirs); 595 assert(found.filePath == buildPath("test", "Tango", "128x128/places", "folder.png")); 596 assert(found.subdir.size == 128); 597 assert(found.subdir.context == "Places"); 598 assert(found.iconTheme.internalName == "Tango"); 599 600 found = findLargestThemedIcon!(subdir => subdir.context == "Places")("folder", iconThemes, baseDirs); 601 assert(found.filePath == buildPath("test", "Tango", "128x128/places", "folder.png")); 602 603 found = findLargestThemedIcon!(subdir => subdir.context == "Actions")("folder", iconThemes, baseDirs); 604 assert(found.filePath.empty); 605 606 found = findLargestThemedIcon("desktop", iconThemes, baseDirs); 607 assert(found.filePath == buildPath("test", "Tango", "32x32/places", "desktop.png")); 608 assert(found.subdir.size == 32); 609 610 found = findLargestThemedIcon("desktop", iconThemes, baseDirs, [".svg", ".png"]); 611 assert(found.filePath == buildPath("test", "Tango", "scalable/places", "desktop.svg")); 612 assert(found.subdir.type == IconSubDir.Type.Scalable); 613 } 614 615 /** 616 * ditto, but with predefined extensions. 617 * See_Also: $(D defaultIconExtensions) 618 */ 619 auto findLargestThemedIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs)(string iconName, IconThemes iconThemes, BaseDirs searchIconDirs) 620 { 621 return findLargestThemedIcon!subdirFilter(iconName, iconThemes, searchIconDirs, defaultIconExtensions); 622 } 623 624 /** 625 * Find icon of the largest size. It uses icon theme cache wherever possible. 626 * This is similar to $(D findLargestThemedIcon), but returns file path only and allows to search for non-themed icons. 627 * Params: 628 * iconName = Name of icon to search as defined by Icon Theme Specification (i.e. without path and extension parts). 629 * iconThemes = Range of $(D icontheme.file.IconThemeFile) objects. 630 * searchIconDirs = Base icon directories. 631 * extensions = Allowed file extensions. 632 * allowNonThemed = Allow searching for non-themed fallback if could not find icon in themes. 633 * Returns: Icon file path or empty string if not found. 634 * Note: If icon of some size was found in the icon theme, this algorithm does not check following themes, even if they contain icons with larger size. Therefore the icon found in the most preferred theme always has presedence over icons from other themes. 635 * See_Also: $(D findLargestThemedIcon), $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D findNonThemedIcon) 636 */ 637 string findLargestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts)(string iconName, IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions, Flag!"allowNonThemed" allowNonThemed = Yes.allowNonThemed) 638 { 639 string largest = findLargestThemedIcon!subdirFilter(iconName, iconThemes, searchIconDirs, extensions).filePath; 640 641 if (largest.empty && allowNonThemed) { 642 return findNonThemedIcon(iconName, searchIconDirs, extensions); 643 } else { 644 return largest; 645 } 646 } 647 648 /// 649 version(iconthemeFileTest) unittest 650 { 651 auto baseDirs = ["test"]; 652 auto iconThemes = [openIconTheme("Tango", baseDirs), openIconTheme("hicolor", baseDirs)]; 653 654 string found; 655 656 found = findLargestIcon("folder", iconThemes, baseDirs); 657 assert(found == buildPath("test", "Tango", "128x128/places", "folder.png")); 658 659 found = findLargestIcon!(subdir => subdir.context == "Places")("folder", iconThemes, baseDirs); 660 assert(found == buildPath("test", "Tango", "128x128/places", "folder.png")); 661 662 found = findLargestIcon!(subdir => subdir.context == "Actions")("folder", iconThemes, baseDirs); 663 assert(found.empty); 664 665 found = findLargestIcon("desktop", iconThemes, baseDirs); 666 assert(found == buildPath("test", "Tango", "32x32/places", "desktop.png")); 667 668 found = findLargestIcon("desktop", iconThemes, baseDirs, [".svg", ".png"]); 669 assert(found == buildPath("test", "Tango", "scalable/places", "desktop.svg")); 670 671 //lookup non-themed 672 found = findLargestIcon("pidgin", iconThemes, baseDirs); 673 assert(found == buildPath("test", "pidgin.png")); 674 675 //don't lookup non-themed 676 found = findLargestIcon("pidgin", iconThemes, baseDirs, defaultIconExtensions, No.allowNonThemed); 677 assert(found.empty); 678 } 679 680 deprecated string findLargestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts)(string iconName, IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions, Flag!"allowFallbackIcon" allowFallback) 681 { 682 return findLargestIcon!subdirFilter(iconName, iconThemes, searchIconDirs, extensions, cast(Flag!"allowNonThemed")allowFallback); 683 } 684 685 /** 686 * ditto, but with predefined extensions and non-themed icons allowed. 687 * See_Also: $(D defaultIconExtensions) 688 */ 689 string findLargestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs)(string iconName, IconThemes iconThemes, BaseDirs searchIconDirs) 690 { 691 return findLargestIcon!subdirFilter(iconName, iconThemes, searchIconDirs, defaultIconExtensions); 692 } 693 694 /** 695 * Distance between desired size and minimum or maximum size value supported by icon theme subdirectory. 696 */ 697 @nogc @safe uint iconSizeDistance(in IconSubDir subdir, uint matchSize) nothrow pure 698 { 699 const uint size = subdir.size(); 700 const uint minSize = subdir.minSize(); 701 const uint maxSize = subdir.maxSize(); 702 const uint threshold = subdir.threshold(); 703 704 final switch(subdir.type()) { 705 case IconSubDir.Type.Fixed: 706 { 707 if (size > matchSize) { 708 return size - matchSize; 709 } else if (size < matchSize) { 710 return matchSize - size; 711 } else { 712 return 0; 713 } 714 } 715 case IconSubDir.Type.Scalable: 716 { 717 if (matchSize < minSize) { 718 return minSize - matchSize; 719 } else if (matchSize > maxSize) { 720 return matchSize - maxSize; 721 } else { 722 return 0; 723 } 724 } 725 case IconSubDir.Type.Threshold: 726 { 727 if (matchSize < size - threshold) { 728 return (size - threshold) - matchSize; 729 } else if (matchSize > size + threshold) { 730 return matchSize - (size + threshold); 731 } else { 732 return 0; 733 } 734 } 735 } 736 } 737 738 /// 739 unittest 740 { 741 auto fixed = IconSubDir(32, IconSubDir.Type.Fixed); 742 assert(iconSizeDistance(fixed, fixed.size()) == 0); 743 assert(iconSizeDistance(fixed, 30) == 2); 744 assert(iconSizeDistance(fixed, 35) == 3); 745 746 auto threshold = IconSubDir(32, IconSubDir.Type.Threshold, "", 0, 0, 5); 747 assert(iconSizeDistance(threshold, threshold.size()) == 0); 748 assert(iconSizeDistance(threshold, threshold.size() - threshold.threshold()) == 0); 749 assert(iconSizeDistance(threshold, threshold.size() + threshold.threshold()) == 0); 750 assert(iconSizeDistance(threshold, 26) == 1); 751 assert(iconSizeDistance(threshold, 39) == 2); 752 753 auto scalable = IconSubDir(32, IconSubDir.Type.Scalable, "", 24, 48); 754 assert(iconSizeDistance(scalable, scalable.size()) == 0); 755 assert(iconSizeDistance(scalable, scalable.minSize()) == 0); 756 assert(iconSizeDistance(scalable, scalable.maxSize()) == 0); 757 assert(iconSizeDistance(scalable, 20) == 4); 758 assert(iconSizeDistance(scalable, 50) == 2); 759 } 760 761 /** 762 * Check if matchSize belongs to subdir's size range. 763 */ 764 @nogc @safe bool matchIconSize(in IconSubDir subdir, uint matchSize) nothrow pure 765 { 766 const uint size = subdir.size(); 767 const uint minSize = subdir.minSize(); 768 const uint maxSize = subdir.maxSize(); 769 const uint threshold = subdir.threshold(); 770 771 final switch(subdir.type()) { 772 case IconSubDir.Type.Fixed: 773 return size == matchSize; 774 case IconSubDir.Type.Threshold: 775 return matchSize <= (size + threshold) && matchSize >= (size - threshold); 776 case IconSubDir.Type.Scalable: 777 return matchSize >= minSize && matchSize <= maxSize; 778 } 779 } 780 781 /// 782 unittest 783 { 784 auto fixed = IconSubDir(32, IconSubDir.Type.Fixed); 785 assert(matchIconSize(fixed, fixed.size())); 786 assert(!matchIconSize(fixed, fixed.size() - 2)); 787 788 auto threshold = IconSubDir(32, IconSubDir.Type.Threshold, "", 0, 0, 5); 789 assert(matchIconSize(threshold, threshold.size() + threshold.threshold())); 790 assert(matchIconSize(threshold, threshold.size() - threshold.threshold())); 791 assert(!matchIconSize(threshold, threshold.size() + threshold.threshold() + 1)); 792 assert(!matchIconSize(threshold, threshold.size() - threshold.threshold() - 1)); 793 794 auto scalable = IconSubDir(32, IconSubDir.Type.Scalable, "", 24, 48); 795 assert(matchIconSize(scalable, scalable.minSize())); 796 assert(matchIconSize(scalable, scalable.maxSize())); 797 assert(!matchIconSize(scalable, scalable.minSize() - 1)); 798 assert(!matchIconSize(scalable, scalable.maxSize() + 1)); 799 } 800 801 /** 802 * Find icon closest to the given size among given alternatives. 803 * Params: 804 * alternatives = Range of $(D IconSearchResult)s, usually returned by $(D lookupIcon). 805 * matchSize = Desired size of icon. 806 */ 807 string matchBestIcon(Range)(Range alternatives, uint matchSize) 808 { 809 uint minDistance = uint.max; 810 string closest; 811 812 foreach(t; alternatives) { 813 auto path = t[0]; 814 auto subdir = t[1]; 815 uint distance = iconSizeDistance(subdir, matchSize); 816 if (distance < minDistance) { 817 minDistance = distance; 818 closest = path; 819 } 820 if (minDistance == 0) { 821 return closest; 822 } 823 } 824 825 return closest; 826 } 827 828 private void openBaseThemesHelper(Range)(ref IconThemeFile[] themes, IconThemeFile iconTheme, 829 Range searchIconDirs, 830 IconThemeFile.IconThemeReadOptions options) 831 { 832 foreach(name; iconTheme.inherits()) { 833 if (!themes.canFind!(function(theme, name) { 834 return theme.internalName == name; 835 })(name)) { 836 try { 837 IconThemeFile f = openIconTheme(name, searchIconDirs, options); 838 if (f) { 839 themes ~= f; 840 openBaseThemesHelper(themes, f, searchIconDirs, options); 841 } 842 } catch(Exception e) { 843 844 } 845 } 846 } 847 } 848 849 /** 850 * Recursively find all themes the given theme is inherited from. 851 * Params: 852 * iconTheme = Original icon theme to search for its base themes. It's NOT included in the resulting array. Must be not null. 853 * searchIconDirs = Base icon directories to search icon themes. 854 * genericThemeName = Name of icon theme which is loaded the last even if it's not specified in inheritance tree. 855 * Pass empty string to avoid it. It's NOT loaded twice if some theme in inheritance tree has it as base theme. 856 * Usually you don't need to change this parameter since $(D hicolor) is required to be used by specification. 857 * options = Options for $(D icontheme.file.IconThemeFile) reading. 858 * Returns: 859 * Array of unique $(D icontheme.file.IconThemeFile) objects represented base themes. 860 * See_Also: 861 * $(D openThemeFamily) 862 */ 863 IconThemeFile[] openBaseThemes(Range)(IconThemeFile iconTheme, 864 Range searchIconDirs, 865 string genericThemeName = defaultGenericIconTheme, 866 IconThemeFile.IconThemeReadOptions options = IconThemeFile.IconThemeReadOptions.init) 867 if(isForwardRange!Range && is(ElementType!Range : string)) 868 { 869 IconThemeFile[] themes; 870 openBaseThemesHelper(themes, iconTheme, searchIconDirs, options); 871 872 if (genericThemeName.length) { 873 auto genericFound = themes.filter!(theme => theme !is null).find!(theme => theme.internalName == genericThemeName); 874 if (genericFound.empty) { 875 IconThemeFile genericTheme; 876 collectException(openIconTheme(genericThemeName, searchIconDirs, options), genericTheme); 877 if (genericTheme) { 878 themes ~= genericTheme; 879 } 880 } 881 } 882 883 return themes; 884 } 885 886 /// 887 version(iconthemeFileTest) unittest 888 { 889 auto tango = openIconTheme("NewTango", ["test"]); 890 auto baseThemes = openBaseThemes(tango, ["test"]); 891 892 assert(baseThemes.length == 2); 893 assert(baseThemes[0].internalName() == "Tango"); 894 assert(baseThemes[1].internalName() == "hicolor"); 895 896 baseThemes = openBaseThemes(tango, ["test"], null); 897 assert(baseThemes.length == 1); 898 assert(baseThemes[0].internalName() == "Tango"); 899 } 900 901 /** 902 * Recursively find all themes the given theme is inherited from. 903 * Params: 904 * iconTheme = Original icon theme to search for its base themes. Included as first element in the resulting array. Must be not null. 905 * searchIconDirs = Base icon directories to search icon themes. 906 * genericThemeName = Name of icon theme which is loaded the last even if it's not specified in inheritance tree. 907 * Pass empty string to avoid it. It's NOT loaded twice if some theme in inheritance tree has it as base theme. 908 * Usually you don't need to change this parameter since $(D hicolor) is required to be used by specification. 909 * options = Options for $(D icontheme.file.IconThemeFile) reading. 910 * Returns: 911 * Array of unique $(D icontheme.file.IconThemeFile) objects that represent the provided icon theme and its base themes. 912 * See_Also: 913 * $(D openBaseThemes) 914 */ 915 IconThemeFile[] openThemeFamily(Range)(IconThemeFile iconTheme, 916 Range searchIconDirs, 917 string genericThemeName = defaultGenericIconTheme, 918 IconThemeFile.IconThemeReadOptions options = IconThemeFile.IconThemeReadOptions.init) 919 if(isForwardRange!Range && is(ElementType!Range : string)) 920 { 921 IconThemeFile[] toReturn; 922 toReturn ~= iconTheme; 923 toReturn ~= openBaseThemes(iconTheme, searchIconDirs, genericThemeName, options); 924 return toReturn; 925 } 926 927 /** 928 * ditto, but firstly loads the given icon theme by name. Returns an empty array if theme specified by $(D iconThemeName) could not be loaded. 929 */ 930 IconThemeFile[] openThemeFamily(Range)(string iconThemeName, 931 Range searchIconDirs, 932 string genericThemeName = defaultGenericIconTheme, 933 IconThemeFile.IconThemeReadOptions options = IconThemeFile.IconThemeReadOptions.init) 934 if(isForwardRange!Range && is(ElementType!Range : string)) 935 { 936 auto iconTheme = openIconTheme(iconThemeName, searchIconDirs, options); 937 if (iconTheme) { 938 return openThemeFamily(iconTheme, searchIconDirs, genericThemeName, options); 939 } 940 return typeof(return).init; 941 } 942 943 /// 944 version(iconthemeFileTest) unittest 945 { 946 auto iconThemes = openThemeFamily("NewTango", ["test"]); 947 948 assert(iconThemes.length == 3); 949 assert(iconThemes[0].internalName() == "NewTango"); 950 assert(iconThemes[1].internalName() == "Tango"); 951 assert(iconThemes[2].internalName() == "hicolor"); 952 953 iconThemes = openThemeFamily("NewTango", ["test"], null); 954 assert(iconThemes.length == 2); 955 assert(iconThemes[0].internalName() == "NewTango"); 956 assert(iconThemes[1].internalName() == "Tango"); 957 }