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 /** 43 * Default icon extensions. This array includes .png and .xpm. 44 * PNG is recommended format. 45 * XPM is kept for backward compatibility. 46 * 47 * Note: Icon Theme Specificiation also lists .svg as possible format, 48 * but it's less common to have SVG support for applications, 49 * hence this format is defined as optional by specificiation. 50 * If your application has proper support for SVG images, 51 * array should include it in the first place as the most preferred format 52 * because SVG images are scalable. 53 */ 54 enum defaultIconExtensions = [".png", ".xpm"]; 55 56 /** 57 * Find all icon themes in searchIconDirs. 58 * Note: 59 * You may want to skip icon themes duplicates if there're different versions of the index.theme file for the same theme. 60 * Returns: 61 * Range of paths to index.theme files represented icon themes. 62 * Params: 63 * searchIconDirs = base icon directories to search icon themes. 64 * See_Also: $(D icontheme.paths.baseIconDirs) 65 */ 66 auto iconThemePaths(Range)(Range searchIconDirs) 67 if(is(ElementType!Range : string)) 68 { 69 return searchIconDirs 70 .filter!(function(dir) { 71 bool ok; 72 collectException(dir.isDir, ok); 73 return ok; 74 }).map!(function(iconDir) { 75 return iconDir.dirEntries(SpanMode.shallow) 76 .map!(p => buildPath(p, "index.theme")).cache() 77 .filter!(isFileNothrow); 78 }).joiner; 79 } 80 81 /// 82 version(iconthemeFileTest) unittest 83 { 84 auto paths = iconThemePaths(["test"]).array; 85 assert(paths.length == 3); 86 assert(paths.canFind(buildPath("test", "NewTango", "index.theme"))); 87 assert(paths.canFind(buildPath("test", "Tango", "index.theme"))); 88 assert(paths.canFind(buildPath("test", "hicolor", "index.theme"))); 89 } 90 91 /** 92 * Lookup index.theme files by theme name. 93 * Params: 94 * themeName = theme name. 95 * searchIconDirs = base icon directories to search icon themes. 96 * Returns: 97 * Range of paths to index.theme file corresponding to the given theme. 98 * Note: 99 * Usually you want to use the only first found file. 100 * See_Also: $(D icontheme.paths.baseIconDirs), $(D findIconTheme) 101 */ 102 auto lookupIconTheme(Range)(string themeName, Range searchIconDirs) 103 if(is(ElementType!Range : string)) 104 { 105 return searchIconDirs 106 .map!(dir => buildPath(dir, themeName, "index.theme")).cache() 107 .filter!(isFileNothrow); 108 } 109 110 /** 111 * Find index.theme file by theme name. 112 * Returns: 113 * Path to the first found index.theme file or null string if not found. 114 * Params: 115 * themeName = Theme name. 116 * searchIconDirs = Base icon directories to search icon themes. 117 * Returns: 118 * Path to the first found index.theme file corresponding to the given theme. 119 * See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIconTheme) 120 */ 121 auto findIconTheme(Range)(string themeName, Range searchIconDirs) 122 { 123 auto paths = lookupIconTheme(themeName, searchIconDirs); 124 if (paths.empty) { 125 return null; 126 } else { 127 return paths.front; 128 } 129 } 130 131 /** 132 * Find index.theme file for given theme and create instance of $(D icontheme.file.IconThemeFile). The first found file will be used. 133 * Returns: $(D icontheme.file.IconThemeFile) object read from the first found index.theme file corresponding to given theme or null if none were found. 134 * Params: 135 * themeName = theme name. 136 * searchIconDirs = base icon directories to search icon themes. 137 * options = options for $(D icontheme.file.IconThemeFile) reading. 138 * Throws: 139 * $(B ErrnoException) if file could not be opened. 140 * $(B IniLikeException) if error occured while reading the file. 141 * See_Also: $(D findIconTheme), $(D icontheme.paths.baseIconDirs) 142 */ 143 IconThemeFile openIconTheme(Range)(string themeName, 144 Range searchIconDirs, 145 IconThemeFile.IconThemeReadOptions options = IconThemeFile.IconThemeReadOptions.init) 146 { 147 auto path = findIconTheme(themeName, searchIconDirs); 148 return path.empty ? null : new IconThemeFile(to!string(path), options); 149 } 150 151 /// 152 version(iconthemeFileTest) unittest 153 { 154 auto tango = openIconTheme("Tango", ["test"]); 155 assert(tango); 156 assert(tango.displayName() == "Tango"); 157 158 auto hicolor = openIconTheme("hicolor", ["test"]); 159 assert(hicolor); 160 assert(hicolor.displayName() == "Hicolor"); 161 162 assert(openIconTheme("Nonexistent", ["test"]) is null); 163 } 164 165 /** 166 * Result of icon lookup. 167 */ 168 struct IconSearchResult(IconTheme) if (is(IconTheme : const(IconThemeFile))) 169 { 170 /** 171 * File path of found icon. 172 */ 173 string filePath; 174 /** 175 * Subdirectory the found icon belongs to. 176 */ 177 IconSubDir subdir; 178 /** 179 * $(D icontheme.file.IconThemeFile) the found icon belongs to. 180 */ 181 IconTheme iconTheme; 182 } 183 184 /** 185 * 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. 186 * 187 * 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. 188 * 189 * Params: 190 * iconName = Icon name. 191 * iconThemes = Icon themes to search icon in. 192 * searchIconDirs = Case icon directories. 193 * extensions = Possible file extensions of needed icon file, in order of preference. 194 * sink = Output range accepting $(D IconSearchResult)s. 195 * reverse = Iterate over icon theme sub-directories in reverse way. 196 * Usually directories with larger icon size are listed the last, 197 * so this parameter may speed up the search when looking for the largest icon. 198 * 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. 199 * Example: 200 ---------- 201 lookupIcon!(subdir => subdir.context == "Places" && subdir.size >= 32)( 202 "folder", iconThemes, baseIconDirs(), [".png", ".xpm"], 203 delegate void (IconSearchResult!IconThemeFile item) { 204 writefln("Icon file: %s. Context: %s. Size: %s. Theme: %s", item.filePath, item.subdir.context, item.subdir.size, item.iconTheme.displayName); 205 }); 206 ---------- 207 * See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupFallbackIcon) 208 */ 209 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) 210 if (isInputRange!(IconThemes) && isForwardRange!(BaseDirs) && isForwardRange!(Exts) && 211 is(ElementType!IconThemes : const(IconThemeFile)) && is(ElementType!BaseDirs : string) && 212 is(ElementType!Exts : string) && isOutputRange!(OutputRange, IconSearchResult!(ElementType!IconThemes))) 213 { 214 bool onExtensions(string themeBaseDir, IconSubDir subdir, ElementType!IconThemes iconTheme) 215 { 216 string subdirPath = buildPath(themeBaseDir, subdir.name); 217 if (!subdirPath.isDirNothrow) { 218 return false; 219 } 220 bool found; 221 foreach(extension; extensions) { 222 string path = buildPath(subdirPath, iconName ~ extension); 223 if (path.isFileNothrow) { 224 found = true; 225 put(sink, IconSearchResult!(ElementType!IconThemes)(path, subdir, iconTheme)); 226 } 227 } 228 return found; 229 } 230 231 foreach(iconTheme; iconThemes) { 232 if (iconTheme is null || iconTheme.internalName().length == 0) { 233 continue; 234 } 235 236 string[] themeBaseDirs = searchIconDirs.map!(dir => buildPath(dir, iconTheme.internalName())).filter!(isDirNothrow).array; 237 238 bool found; 239 240 auto bySubdir = choose(reverse, iconTheme.bySubdir().retro(), iconTheme.bySubdir()); 241 foreach(subdir; bySubdir) { 242 if (!subdirFilter(subdir)) { 243 continue; 244 } 245 foreach(themeBaseDir; themeBaseDirs) { 246 if (iconTheme.cache !is null && themeBaseDir == iconTheme.cache.fileName.dirName) { 247 if (iconTheme.cache.containsIcon(iconName, subdir.name)) { 248 found = onExtensions(themeBaseDir, subdir, iconTheme) || found; 249 } 250 } else { 251 found = onExtensions(themeBaseDir, subdir, iconTheme) || found; 252 } 253 } 254 } 255 if (found) { 256 return; 257 } 258 } 259 } 260 261 /** 262 * Iterate over all icons in icon themes. 263 * iconThemes is usually the range of the main theme and themes it inherits from. 264 * 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. 265 * Use subdirFilter to filter icons by $(D icontheme.file.IconSubDir) thus decreasing the number of searchable items and allocations. 266 * Returns: Range of $(D IconSearchResult). 267 * Params: 268 * iconThemes = icon themes to search icon in. 269 * searchIconDirs = base icon directories. 270 * extensions = possible file extensions for icon files. 271 * Example: 272 ------------- 273 foreach(item; lookupThemeIcons!(subdir => subdir.context == "MimeTypes" && subdir.size >= 32)(iconThemes, baseIconDirs(), [".png", ".xpm"])) 274 { 275 writefln("Icon file: %s. Context: %s. Size: %s", item.filePath, item.subdir.context, item.subdir.size); 276 } 277 ------------- 278 * See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D openBaseThemes) 279 */ 280 281 auto lookupThemeIcons(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts)(IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions) 282 if (is(ElementType!IconThemes : const(IconThemeFile)) && is(ElementType!BaseDirs : string) && is (ElementType!Exts : string)) 283 { 284 return iconThemes.filter!(iconTheme => iconTheme !is null).map!( 285 iconTheme => iconTheme.bySubdir().filter!(subdirFilter).map!( 286 subdir => searchIconDirs.map!( 287 basePath => buildPath(basePath, iconTheme.internalName(), subdir.name) 288 ).filter!(isDirNothrow).map!( 289 subdirPath => subdirPath.dirEntries(SpanMode.shallow).filter!( 290 filePath => filePath.isFileNothrow && extensions.canFind(filePath.extension) 291 ).map!(filePath => IconSearchResult!(ElementType!IconThemes)(filePath, subdir, iconTheme)) 292 ).joiner 293 ).joiner 294 ).joiner; 295 } 296 297 /** 298 * Iterate over all icons out of icon themes. 299 * Returns: Range of found icon file paths. 300 * Params: 301 * searchIconDirs = base icon directories. 302 * extensions = possible file extensions for icon files. 303 * See_Also: 304 * $(D lookupFallbackIcon), $(D icontheme.paths.baseIconDirs) 305 */ 306 auto lookupFallbackIcons(BaseDirs, Exts)(BaseDirs searchIconDirs, Exts extensions) 307 if (isInputRange!(BaseDirs) && isForwardRange!(Exts) && 308 is(ElementType!BaseDirs : string) && is(ElementType!Exts : string)) 309 { 310 return searchIconDirs.filter!(isDirNothrow).map!(basePath => basePath.dirEntries(SpanMode.shallow).filter!( 311 filePath => filePath.isFileNothrow && extensions.canFind(filePath.extension) 312 )).joiner; 313 } 314 315 /** 316 * Lookup icon alternatives beyond the icon themes. May be used as fallback lookup, if lookupIcon returned empty range. 317 * Returns: The range of found icon file paths. 318 * Example: 319 ---------- 320 auto result = lookupFallbackIcon("folder", baseIconDirs(), [".png", ".xpm"]); 321 ---------- 322 * See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D lookupFallbackIcons) 323 */ 324 auto lookupFallbackIcon(BaseDirs, Exts)(string iconName, BaseDirs searchIconDirs, Exts extensions) 325 if (isInputRange!(BaseDirs) && isForwardRange!(Exts) && 326 is(ElementType!BaseDirs : string) && is(ElementType!Exts : string)) 327 { 328 return searchIconDirs.map!(basePath => 329 extensions 330 .map!(extension => buildPath(basePath, iconName ~ extension)).cache() 331 .filter!(isFileNothrow) 332 ).joiner; 333 } 334 335 /** 336 * Find fallback icon outside of icon themes. The first found is returned. 337 * See_Also: $(D lookupFallbackIcon), $(D icontheme.paths.baseIconDirs) 338 */ 339 string findFallbackIcon(BaseDirs, Exts)(string iconName, BaseDirs searchIconDirs, Exts extensions) 340 { 341 auto r = lookupFallbackIcon(iconName, searchIconDirs, extensions); 342 if (r.empty) { 343 return null; 344 } else { 345 return r.front; 346 } 347 } 348 349 /// 350 version(iconthemeFileTest) unittest 351 { 352 assert(findFallbackIcon("pidgin", ["test"], defaultIconExtensions) == buildPath("test", "pidgin.png")); 353 assert(findFallbackIcon("nonexistent", ["test"], defaultIconExtensions).empty); 354 } 355 356 /** 357 * Find icon closest of the size. It uses icon theme cache wherever possible. The first perfect match is used. 358 * Params: 359 * iconName = Name of icon to search as defined by Icon Theme Specification (i.e. without path and extension parts). 360 * size = Preferred icon size to get. 361 * iconThemes = Range of $(D icontheme.file.IconThemeFile) objects. 362 * searchIconDirs = Base icon directories. 363 * extensions = Allowed file extensions. 364 * allowFallback = Allow searching for non-themed fallback if could not find icon in themes (non-themed icon can be any size). 365 * Returns: Icon file path or empty string if not found. 366 * 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. 367 * See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D findFallbackIcon), $(D iconSizeDistance) 368 */ 369 string findClosestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts)(string iconName, uint size, IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions, Flag!"allowFallbackIcon" allowFallback = Yes.allowFallbackIcon) 370 { 371 uint minDistance = uint.max; 372 string closest; 373 374 lookupIcon!(delegate bool(const(IconSubDir) subdir) { 375 return minDistance != 0 && subdirFilter(subdir) && iconSizeDistance(subdir, size) <= minDistance; 376 })(iconName, iconThemes, searchIconDirs, extensions, delegate void(IconSearchResult!(ElementType!IconThemes) t) { 377 auto path = t.filePath; 378 auto subdir = t.subdir; 379 auto theme = t.iconTheme; 380 381 uint distance = iconSizeDistance(subdir, size); 382 if (distance < minDistance) { 383 minDistance = distance; 384 closest = path; 385 } 386 }); 387 388 if (closest.empty && allowFallback) { 389 return findFallbackIcon(iconName, searchIconDirs, extensions); 390 } else { 391 return closest; 392 } 393 } 394 395 /// 396 version(iconthemeFileTest) unittest 397 { 398 auto baseDirs = ["test"]; 399 auto iconThemes = [openIconTheme("Tango", baseDirs), openIconTheme("hicolor", baseDirs)]; 400 401 string found; 402 403 //exact match 404 found = findClosestIcon("folder", 32, iconThemes, baseDirs); 405 assert(found == buildPath("test", "Tango", "32x32/places", "folder.png")); 406 407 found = findClosestIcon("folder", 24, iconThemes, baseDirs); 408 assert(found == buildPath("test", "Tango", "24x24/devices", "folder.png")); 409 410 found = findClosestIcon!(subdir => subdir.context == "Places")("folder", 32, iconThemes, baseDirs); 411 assert(found == buildPath("test", "Tango", "32x32/places", "folder.png")); 412 413 found = findClosestIcon!(subdir => subdir.context == "Places")("folder", 24, iconThemes, baseDirs); 414 assert(found == buildPath("test", "Tango", "32x32/places", "folder.png")); 415 416 found = findClosestIcon!(subdir => subdir.context == "MimeTypes")("folder", 32, iconThemes, baseDirs); 417 assert(found.empty); 418 419 //hicolor has exact match, but Tango is more preferred. 420 found = findClosestIcon("folder", 64, iconThemes, baseDirs); 421 assert(found == buildPath("test", "Tango", "32x32/places", "folder.png")); 422 423 //find xpm 424 found = findClosestIcon("folder", 32, iconThemes, baseDirs, [".xpm"]); 425 assert(found == buildPath("test", "Tango", "32x32/places", "folder.xpm")); 426 427 //find big png, not exact match 428 found = findClosestIcon("folder", 200, iconThemes, baseDirs); 429 assert(found == buildPath("test", "Tango", "128x128/places", "folder.png")); 430 431 //svg is closer 432 found = findClosestIcon("folder", 200, iconThemes, baseDirs, [".png", ".svg"]); 433 assert(found == buildPath("test", "Tango", "scalable/places", "folder.svg")); 434 435 //lookup with fallback 436 found = findClosestIcon("pidgin", 96, iconThemes, baseDirs); 437 assert(found == buildPath("test", "pidgin.png")); 438 439 //lookup without fallback 440 found = findClosestIcon("pidgin", 96, iconThemes, baseDirs, defaultIconExtensions, No.allowFallbackIcon); 441 assert(found.empty); 442 443 found = findClosestIcon("text-plain", 48, iconThemes, baseDirs); 444 assert(found == buildPath("test", "hicolor", "48x48/mimetypes", "text-plain.png")); 445 446 found = findClosestIcon!(subdir => subdir.context == "MimeTypes")("text-plain", 48, iconThemes, baseDirs); 447 assert(found == buildPath("test", "hicolor", "48x48/mimetypes", "text-plain.png")); 448 449 found = findClosestIcon!(subdir => subdir.context == "Actions")("text-plain", 48, iconThemes, baseDirs); 450 assert(found.empty); 451 } 452 453 /** 454 * ditto, but with predefined extensions and fallback allowed. 455 * See_Also: $(D defaultIconExtensions) 456 */ 457 string findClosestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs)(string iconName, uint size, IconThemes iconThemes, BaseDirs searchIconDirs) 458 { 459 return findClosestIcon!subdirFilter(iconName, size, iconThemes, searchIconDirs, defaultIconExtensions); 460 } 461 462 /** 463 * Find icon of the largest size. It uses icon theme cache wherever possible. 464 * Params: 465 * iconName = Name of icon to search as defined by Icon Theme Specification (i.e. without path and extension parts). 466 * iconThemes = Range of $(D icontheme.file.IconThemeFile) objects. 467 * searchIconDirs = Base icon directories. 468 * extensions = Allowed file extensions. 469 * allowFallback = Allow searching for non-themed fallback if could not find icon in themes. 470 * Returns: Icon file path or empty string if not found. 471 * 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. 472 * See_Also: $(D icontheme.paths.baseIconDirs), $(D lookupIcon), $(D findFallbackIcon) 473 */ 474 string findLargestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs, Exts)(string iconName, IconThemes iconThemes, BaseDirs searchIconDirs, Exts extensions, Flag!"allowFallbackIcon" allowFallback = Yes.allowFallbackIcon) 475 { 476 uint max = 0; 477 string largest; 478 479 lookupIcon!(delegate bool(const(IconSubDir) subdir) { 480 return subdirFilter(subdir) && subdir.size() >= max; 481 })(iconName, iconThemes, searchIconDirs, extensions, delegate void(IconSearchResult!(ElementType!IconThemes) t) { 482 auto path = t.filePath; 483 auto subdir = t.subdir; 484 auto theme = t.iconTheme; 485 486 if (subdir.size() > max) { 487 max = subdir.size(); 488 largest = path; 489 } 490 }, Yes.reverse); 491 492 if (largest.empty && allowFallback) { 493 return findFallbackIcon(iconName, searchIconDirs, extensions); 494 } else { 495 return largest; 496 } 497 } 498 499 /// 500 version(iconthemeFileTest) unittest 501 { 502 auto baseDirs = ["test"]; 503 auto iconThemes = [openIconTheme("Tango", baseDirs), openIconTheme("hicolor", baseDirs)]; 504 505 string found; 506 507 found = findLargestIcon("folder", iconThemes, baseDirs); 508 assert(found == buildPath("test", "Tango", "128x128/places", "folder.png")); 509 510 found = findLargestIcon("desktop", iconThemes, baseDirs); 511 assert(found == buildPath("test", "Tango", "32x32/places", "desktop.png")); 512 513 found = findLargestIcon("desktop", iconThemes, baseDirs, [".svg", ".png"]); 514 assert(found == buildPath("test", "Tango", "scalable/places", "desktop.svg")); 515 516 //lookup with fallback 517 found = findLargestIcon("pidgin", iconThemes, baseDirs); 518 assert(found == buildPath("test", "pidgin.png")); 519 520 //lookup without fallback 521 found = findLargestIcon("pidgin", iconThemes, baseDirs, defaultIconExtensions, No.allowFallbackIcon); 522 assert(found.empty); 523 } 524 525 /** 526 * ditto, but with predefined extensions and fallback allowed. 527 * See_Also: $(D defaultIconExtensions) 528 */ 529 string findLargestIcon(alias subdirFilter = (a => true), IconThemes, BaseDirs)(string iconName, IconThemes iconThemes, BaseDirs searchIconDirs) 530 { 531 return findLargestIcon!subdirFilter(iconName, iconThemes, searchIconDirs, defaultIconExtensions); 532 } 533 534 /** 535 * Distance between desired size and minimum or maximum size value supported by icon theme subdirectory. 536 */ 537 @nogc @safe uint iconSizeDistance(in IconSubDir subdir, uint matchSize) nothrow pure 538 { 539 const uint size = subdir.size(); 540 const uint minSize = subdir.minSize(); 541 const uint maxSize = subdir.maxSize(); 542 const uint threshold = subdir.threshold(); 543 544 final switch(subdir.type()) { 545 case IconSubDir.Type.Fixed: 546 { 547 if (size > matchSize) { 548 return size - matchSize; 549 } else if (size < matchSize) { 550 return matchSize - size; 551 } else { 552 return 0; 553 } 554 } 555 case IconSubDir.Type.Scalable: 556 { 557 if (matchSize < minSize) { 558 return minSize - matchSize; 559 } else if (matchSize > maxSize) { 560 return matchSize - maxSize; 561 } else { 562 return 0; 563 } 564 } 565 case IconSubDir.Type.Threshold: 566 { 567 if (matchSize < size - threshold) { 568 return (size - threshold) - matchSize; 569 } else if (matchSize > size + threshold) { 570 return matchSize - (size + threshold); 571 } else { 572 return 0; 573 } 574 } 575 } 576 } 577 578 /// 579 unittest 580 { 581 auto fixed = IconSubDir(32, IconSubDir.Type.Fixed); 582 assert(iconSizeDistance(fixed, fixed.size()) == 0); 583 assert(iconSizeDistance(fixed, 30) == 2); 584 assert(iconSizeDistance(fixed, 35) == 3); 585 586 auto threshold = IconSubDir(32, IconSubDir.Type.Threshold, "", 0, 0, 5); 587 assert(iconSizeDistance(threshold, threshold.size()) == 0); 588 assert(iconSizeDistance(threshold, threshold.size() - threshold.threshold()) == 0); 589 assert(iconSizeDistance(threshold, threshold.size() + threshold.threshold()) == 0); 590 assert(iconSizeDistance(threshold, 26) == 1); 591 assert(iconSizeDistance(threshold, 39) == 2); 592 593 auto scalable = IconSubDir(32, IconSubDir.Type.Scalable, "", 24, 48); 594 assert(iconSizeDistance(scalable, scalable.size()) == 0); 595 assert(iconSizeDistance(scalable, scalable.minSize()) == 0); 596 assert(iconSizeDistance(scalable, scalable.maxSize()) == 0); 597 assert(iconSizeDistance(scalable, 20) == 4); 598 assert(iconSizeDistance(scalable, 50) == 2); 599 } 600 601 /** 602 * Check if matchSize belongs to subdir's size range. 603 */ 604 @nogc @safe bool matchIconSize(in IconSubDir subdir, uint matchSize) nothrow pure 605 { 606 const uint size = subdir.size(); 607 const uint minSize = subdir.minSize(); 608 const uint maxSize = subdir.maxSize(); 609 const uint threshold = subdir.threshold(); 610 611 final switch(subdir.type()) { 612 case IconSubDir.Type.Fixed: 613 return size == matchSize; 614 case IconSubDir.Type.Threshold: 615 return matchSize <= (size + threshold) && matchSize >= (size - threshold); 616 case IconSubDir.Type.Scalable: 617 return matchSize >= minSize && matchSize <= maxSize; 618 } 619 } 620 621 /// 622 unittest 623 { 624 auto fixed = IconSubDir(32, IconSubDir.Type.Fixed); 625 assert(matchIconSize(fixed, fixed.size())); 626 assert(!matchIconSize(fixed, fixed.size() - 2)); 627 628 auto threshold = IconSubDir(32, IconSubDir.Type.Threshold, "", 0, 0, 5); 629 assert(matchIconSize(threshold, threshold.size() + threshold.threshold())); 630 assert(matchIconSize(threshold, threshold.size() - threshold.threshold())); 631 assert(!matchIconSize(threshold, threshold.size() + threshold.threshold() + 1)); 632 assert(!matchIconSize(threshold, threshold.size() - threshold.threshold() - 1)); 633 634 auto scalable = IconSubDir(32, IconSubDir.Type.Scalable, "", 24, 48); 635 assert(matchIconSize(scalable, scalable.minSize())); 636 assert(matchIconSize(scalable, scalable.maxSize())); 637 assert(!matchIconSize(scalable, scalable.minSize() - 1)); 638 assert(!matchIconSize(scalable, scalable.maxSize() + 1)); 639 } 640 641 /** 642 * Find icon closest to the given size among given alternatives. 643 * Params: 644 * alternatives = range of $(D IconSearchResult)s, usually returned by $(D lookupIcon). 645 * matchSize = desired size of icon. 646 */ 647 string matchBestIcon(Range)(Range alternatives, uint matchSize) 648 { 649 uint minDistance = uint.max; 650 string closest; 651 652 foreach(t; alternatives) { 653 auto path = t[0]; 654 auto subdir = t[1]; 655 uint distance = iconSizeDistance(subdir, matchSize); 656 if (distance < minDistance) { 657 minDistance = distance; 658 closest = path; 659 } 660 if (minDistance == 0) { 661 return closest; 662 } 663 } 664 665 return closest; 666 } 667 668 private void openBaseThemesHelper(Range)(ref IconThemeFile[] themes, IconThemeFile iconTheme, 669 Range searchIconDirs, 670 IconThemeFile.IconThemeReadOptions options) 671 { 672 foreach(name; iconTheme.inherits()) { 673 if (!themes.canFind!(function(theme, name) { 674 return theme.internalName == name; 675 })(name)) { 676 try { 677 IconThemeFile f = openIconTheme(name, searchIconDirs, options); 678 if (f) { 679 themes ~= f; 680 openBaseThemesHelper(themes, f, searchIconDirs, options); 681 } 682 } catch(Exception e) { 683 684 } 685 } 686 } 687 } 688 689 /** 690 * Recursively find all themes the given theme is inherited from. 691 * Params: 692 * iconTheme = Original icon theme to search for its base themes. Included as first element in resulting array. 693 * searchIconDirs = Base icon directories to search icon themes. 694 * fallbackThemeName = Name of fallback theme which is loaded the last. Not used if empty. It's NOT loaded twice if some theme in inheritance tree has it as base theme. 695 * options = Options for $(D icontheme.file.IconThemeFile) reading. 696 * Returns: 697 * Array of unique $(D icontheme.file.IconThemeFile) objects represented base themes. 698 */ 699 IconThemeFile[] openBaseThemes(Range)(IconThemeFile iconTheme, 700 Range searchIconDirs, 701 string fallbackThemeName = "hicolor", 702 IconThemeFile.IconThemeReadOptions options = IconThemeFile.IconThemeReadOptions.init) 703 if(isForwardRange!Range && is(ElementType!Range : string)) 704 { 705 IconThemeFile[] themes; 706 openBaseThemesHelper(themes, iconTheme, searchIconDirs, options); 707 708 if (fallbackThemeName.length) { 709 auto fallbackFound = themes.filter!(theme => theme !is null).find!(theme => theme.internalName == fallbackThemeName); 710 if (fallbackFound.empty) { 711 IconThemeFile fallbackTheme; 712 collectException(openIconTheme(fallbackThemeName, searchIconDirs, options), fallbackTheme); 713 if (fallbackTheme) { 714 themes ~= fallbackTheme; 715 } 716 } 717 } 718 719 return themes; 720 } 721 722 /// 723 version(iconthemeFileTest) unittest 724 { 725 auto tango = openIconTheme("NewTango", ["test"]); 726 auto baseThemes = openBaseThemes(tango, ["test"]); 727 728 assert(baseThemes.length == 2); 729 assert(baseThemes[0].internalName() == "Tango"); 730 assert(baseThemes[1].internalName() == "hicolor"); 731 732 baseThemes = openBaseThemes(tango, ["test"], null); 733 assert(baseThemes.length == 1); 734 assert(baseThemes[0].internalName() == "Tango"); 735 }