1 // Written in the D programming language. 2 3 /** 4 DLANGUI library. 5 6 This module contains resource management and drawables implementation. 7 8 imageCache is RAM cache of decoded images (as DrawBuf). 9 10 drawableCache is cache of Drawables. 11 12 Supports nine-patch PNG images in .9.png files (like in Android). 13 14 Supports state drawables using XML files similar to ones in Android). 15 16 Synopsis: 17 18 ---- 19 import dlangui.graphics.resources; 20 21 ---- 22 23 Copyright: Vadim Lopatin, 2014 24 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 25 Authors: $(WEB coolreader.org, Vadim Lopatin) 26 */ 27 module dlangui.graphics.resources; 28 29 import dlangui.graphics.images; 30 import dlangui.graphics.drawbuf; 31 import dlangui.core.logger; 32 import std.file; 33 import std.algorithm; 34 import std.xml; 35 import std.algorithm; 36 import std.conv; 37 38 39 class Drawable : RefCountedObject { 40 //private static int _instanceCount; 41 this() { 42 //Log.d("Created drawable, count=", ++_instanceCount); 43 } 44 ~this() { 45 //Log.d("Destroyed drawable, count=", --_instanceCount); 46 } 47 abstract void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0); 48 @property abstract int width(); 49 @property abstract int height(); 50 @property Rect padding() { return Rect(0,0,0,0); } 51 } 52 53 class EmptyDrawable : Drawable { 54 override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) { 55 } 56 @property override int width() { return 0; } 57 @property override int height() { return 0; } 58 } 59 60 class SolidFillDrawable : Drawable { 61 protected uint _color; 62 this(uint color) { 63 _color = color; 64 } 65 override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) { 66 if ((_color >> 24) != 0xFF) // not fully transparent 67 buf.fillRect(rc, _color); 68 } 69 @property override int width() { return 1; } 70 @property override int height() { return 1; } 71 } 72 73 class ImageDrawable : Drawable { 74 protected DrawBufRef _image; 75 protected bool _tiled; 76 //private int _instanceCount; 77 this(ref DrawBufRef image, bool tiled = false, bool ninePatch = false) { 78 _image = image; 79 _tiled = tiled; 80 if (ninePatch) 81 _image.detectNinePatch(); 82 //Log.d("Created ImageDrawable, count=", ++_instanceCount); 83 } 84 ~this() { 85 _image.clear(); 86 //Log.d("Destroyed ImageDrawable, count=", --_instanceCount); 87 } 88 @property override int width() { 89 if (_image.isNull) 90 return 0; 91 if (_image.hasNinePatch) 92 return _image.width - 2; 93 return _image.width; 94 } 95 @property override int height() { 96 if (_image.isNull) 97 return 0; 98 if (_image.hasNinePatch) 99 return _image.height - 2; 100 return _image.height; 101 } 102 @property override Rect padding() { 103 if (!_image.isNull && _image.hasNinePatch) 104 return _image.ninePatch.padding; 105 return Rect(0,0,0,0); 106 } 107 private static void correctFrameBounds(ref int n1, ref int n2, ref int n3, ref int n4) { 108 if (n1 > n2) { 109 //assert(n2 - n1 == n4 - n3); 110 int middledist = (n1 + n2) / 2 - n1; 111 n1 = n2 = n1 + middledist; 112 n3 = n4 = n3 + middledist; 113 } 114 } 115 override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) { 116 if (_image.isNull) 117 return; 118 if (_image.hasNinePatch) { 119 // draw nine patch 120 const NinePatch * p = _image.ninePatch; 121 //Log.d("drawing nine patch image with frame ", p.frame, " padding ", p.padding); 122 int w = width; 123 int h = height; 124 Rect dstrect = rc; 125 Rect srcrect = Rect(1, 1, w + 1, h + 1); 126 if (true) { //buf.applyClipping(dstrect, srcrect)) { 127 int x0 = srcrect.left; 128 int x1 = srcrect.left + p.frame.left; 129 int x2 = srcrect.right - p.frame.right; 130 int x3 = srcrect.right; 131 int y0 = srcrect.top; 132 int y1 = srcrect.top + p.frame.top; 133 int y2 = srcrect.bottom - p.frame.bottom; 134 int y3 = srcrect.bottom; 135 int dstx0 = rc.left; 136 int dstx1 = rc.left + p.frame.left; 137 int dstx2 = rc.right - p.frame.right; 138 int dstx3 = rc.right; 139 int dsty0 = rc.top; 140 int dsty1 = rc.top + p.frame.top; 141 int dsty2 = rc.bottom - p.frame.bottom; 142 int dsty3 = rc.bottom; 143 //Log.d("x bounds: ", x0, ", ", x1, ", ", x2, ", ", x3, " dst ", dstx0, ", ", dstx1, ", ", dstx2, ", ", dstx3); 144 //Log.d("y bounds: ", y0, ", ", y1, ", ", y2, ", ", y3, " dst ", dsty0, ", ", dsty1, ", ", dsty2, ", ", dsty3); 145 146 correctFrameBounds(x1, x2, dstx1, dstx2); 147 correctFrameBounds(y1, y2, dsty1, dsty2); 148 149 //correctFrameBounds(x1, x2); 150 //correctFrameBounds(y1, y2); 151 //correctFrameBounds(dstx1, dstx2); 152 //correctFrameBounds(dsty1, dsty2); 153 if (y0 < y1 && dsty0 < dsty1) { 154 // top row 155 if (x0 < x1 && dstx0 < dstx1) 156 buf.drawFragment(dstx0, dsty0, _image.get, Rect(x0, y0, x1, y1)); // top left 157 if (x1 < x2 && dstx1 < dstx2) 158 buf.drawRescaled(Rect(dstx1, dsty0, dstx2, dsty1), _image.get, Rect(x1, y0, x2, y1)); // top center 159 if (x2 < x3 && dstx2 < dstx3) 160 buf.drawFragment(dstx2, dsty0, _image.get, Rect(x2, y0, x3, y1)); // top right 161 } 162 if (y1 < y2 && dsty1 < dsty2) { 163 // middle row 164 if (x0 < x1 && dstx0 < dstx1) 165 buf.drawRescaled(Rect(dstx0, dsty1, dstx1, dsty2), _image.get, Rect(x0, y1, x1, y2)); // middle center 166 if (x1 < x2 && dstx1 < dstx2) 167 buf.drawRescaled(Rect(dstx1, dsty1, dstx2, dsty2), _image.get, Rect(x1, y1, x2, y2)); // center 168 if (x2 < x3 && dstx2 < dstx3) 169 buf.drawRescaled(Rect(dstx2, dsty1, dstx3, dsty2), _image.get, Rect(x2, y1, x3, y2)); // middle center 170 } 171 if (y2 < y3 && dsty2 < dsty3) { 172 // bottom row 173 if (x0 < x1 && dstx0 < dstx1) 174 buf.drawFragment(dstx0, dsty2, _image.get, Rect(x0, y2, x1, y3)); // bottom left 175 if (x1 < x2 && dstx1 < dstx2) 176 buf.drawRescaled(Rect(dstx1, dsty2, dstx2, dsty3), _image.get, Rect(x1, y2, x2, y3)); // bottom center 177 if (x2 < x3 && dstx2 < dstx3) 178 buf.drawFragment(dstx2, dsty2, _image.get, Rect(x2, y2, x3, y3)); // bottom right 179 } 180 } 181 } else if (_tiled) { 182 // tiled 183 } else { 184 // rescaled or normal 185 if (rc.width != _image.width || rc.height != _image.height) 186 buf.drawRescaled(rc, _image.get, Rect(0, 0, _image.width, _image.height)); 187 else 188 buf.drawImage(rc.left, rc.top, _image); 189 } 190 } 191 } 192 193 string attrValue(Element item, string attrname, string attrname2) { 194 if (attrname in item.tag.attr) 195 return item.tag.attr[attrname]; 196 if (attrname2 in item.tag.attr) 197 return item.tag.attr[attrname2]; 198 return null; 199 } 200 201 string attrValue(ref string[string] attr, string attrname, string attrname2) { 202 if (attrname in attr) 203 return attr[attrname]; 204 if (attrname2 in attr) 205 return attr[attrname2]; 206 return null; 207 } 208 209 void extractStateFlag(ref string[string] attr, string attrName, string attrName2, State state, ref uint stateMask, ref uint stateValue) { 210 string value = attrValue(attr, attrName, attrName2); 211 if (value !is null) { 212 if (value.equal("true")) 213 stateValue |= state; 214 stateMask |= state; 215 } 216 } 217 218 /// converts XML attribute name to State (see http://developer.android.com/guide/topics/resources/drawable-resource.html#StateList) 219 void extractStateFlags(ref string[string] attr, ref uint stateMask, ref uint stateValue) { 220 extractStateFlag(attr, "state_pressed", "android:state_pressed", State.Pressed, stateMask, stateValue); 221 extractStateFlag(attr, "state_focused", "android:state_focused", State.Focused, stateMask, stateValue); 222 extractStateFlag(attr, "state_hovered", "android:state_hovered", State.Hovered, stateMask, stateValue); 223 extractStateFlag(attr, "state_selected", "android:state_selected", State.Selected, stateMask, stateValue); 224 extractStateFlag(attr, "state_checkable", "android:state_checkable", State.Checkable, stateMask, stateValue); 225 extractStateFlag(attr, "state_checked", "android:state_checked", State.Checked, stateMask, stateValue); 226 extractStateFlag(attr, "state_enabled", "android:state_enabled", State.Enabled, stateMask, stateValue); 227 extractStateFlag(attr, "state_activated", "android:state_activated", State.Activated, stateMask, stateValue); 228 extractStateFlag(attr, "state_window_focused", "android:state_window_focused", State.WindowFocused, stateMask, stateValue); 229 } 230 231 /* 232 sample: 233 (prefix android: is optional) 234 235 <?xml version="1.0" encoding="utf-8"?> 236 <selector xmlns:android="http://schemas.android.com/apk/res/android" 237 android:constantSize=["true" | "false"] 238 android:dither=["true" | "false"] 239 android:variablePadding=["true" | "false"] > 240 <item 241 android:drawable="@[package:]drawable/drawable_resource" 242 android:state_pressed=["true" | "false"] 243 android:state_focused=["true" | "false"] 244 android:state_hovered=["true" | "false"] 245 android:state_selected=["true" | "false"] 246 android:state_checkable=["true" | "false"] 247 android:state_checked=["true" | "false"] 248 android:state_enabled=["true" | "false"] 249 android:state_activated=["true" | "false"] 250 android:state_window_focused=["true" | "false"] /> 251 </selector> 252 */ 253 254 /// Drawable which is drawn depending on state (see http://developer.android.com/guide/topics/resources/drawable-resource.html#StateList) 255 class StateDrawable : Drawable { 256 257 static struct StateItem { 258 uint stateMask; 259 uint stateValue; 260 ColorTransform transform; 261 DrawableRef drawable; 262 @property bool matchState(uint state) { 263 return (stateMask & state) == stateValue; 264 } 265 } 266 // list of states 267 protected StateItem[] _stateList; 268 // max paddings for all states 269 protected Rect _paddings; 270 // max drawable size for all states 271 protected Point _size; 272 273 void addState(uint stateMask, uint stateValue, string resourceId, ref ColorTransform transform) { 274 StateItem item; 275 item.stateMask = stateMask; 276 item.stateValue = stateValue; 277 item.drawable = drawableCache.get(resourceId, transform); 278 itemAdded(item); 279 } 280 281 void addState(uint stateMask, uint stateValue, DrawableRef drawable) { 282 StateItem item; 283 item.stateMask = stateMask; 284 item.stateValue = stateValue; 285 item.drawable = drawable; 286 itemAdded(item); 287 } 288 289 private void itemAdded(ref StateItem item) { 290 _stateList ~= item; 291 if (!item.drawable.isNull) { 292 if (_size.x < item.drawable.width) 293 _size.x = item.drawable.width; 294 if (_size.y < item.drawable.height) 295 _size.y = item.drawable.height; 296 _paddings.setMax(item.drawable.padding); 297 } 298 } 299 300 /// parse 4 comma delimited integers 301 static bool parseList4(T)(string value, ref T[4] items) { 302 int index = 0; 303 int p = 0; 304 int start = 0; 305 for (;p < value.length && index < 4; p++) { 306 while (p < value.length && value[p] != ',') 307 p++; 308 if (p > start) { 309 int end = p; 310 string s = value[start .. end]; 311 items[index++] = to!T(s); 312 start = p + 1; 313 } 314 } 315 return index == 4; 316 } 317 private static uint colorTransformFromStringAdd(string value) { 318 if (value is null) 319 return COLOR_TRANSFORM_OFFSET_NONE; 320 int n[4]; 321 if (!parseList4(value, n)) 322 return COLOR_TRANSFORM_OFFSET_NONE; 323 foreach (ref item; n) { 324 item = item / 2 + 0x80; 325 if (item < 0) 326 item = 0; 327 if (item > 0xFF) 328 item = 0xFF; 329 } 330 return (n[0] << 24) | (n[1] << 16) | (n[2] << 8) | (n[3] << 0); 331 } 332 private static uint colorTransformFromStringMult(string value) { 333 if (value is null) 334 return COLOR_TRANSFORM_MULTIPLY_NONE; 335 float n[4]; 336 uint nn[4]; 337 if (!parseList4!float(value, n)) 338 return COLOR_TRANSFORM_MULTIPLY_NONE; 339 for(int i = 0; i < 4; i++) { 340 int res = cast(int)(n[i] * 0x40); 341 if (res < 0) 342 res = 0; 343 if (res > 0xFF) 344 res = 0xFF; 345 nn[i] = res; 346 } 347 return (nn[0] << 24) | (nn[1] << 16) | (nn[2] << 8) | (nn[3] << 0); 348 } 349 350 bool load(Element element) { 351 foreach(item; element.elements) { 352 if (item.tag.name.equal("item")) { 353 string drawableId = attrValue(item, "drawable", "android:drawable"); 354 if (drawableId.startsWith("@drawable/")) 355 drawableId = drawableId[10 .. $]; 356 ColorTransform transform; 357 transform.addBefore = colorTransformFromStringAdd(attrValue(item, "color_transform_add1", "android:transform_color_add1")); 358 transform.multiply = colorTransformFromStringMult(attrValue(item, "color_transform_mul", "android:transform_color_mul")); 359 transform.addAfter = colorTransformFromStringAdd(attrValue(item, "color_transform_add2", "android:transform_color_add2")); 360 if (drawableId !is null) { 361 uint stateMask, stateValue; 362 extractStateFlags(item.tag.attr, stateMask, stateValue); 363 if (drawableId !is null) { 364 addState(stateMask, stateValue, drawableId, transform); 365 } 366 } 367 } 368 } 369 return _stateList.length > 0; 370 } 371 372 /// load from XML file 373 bool load(string filename) { 374 import std.file; 375 import std.string; 376 377 try { 378 string s = cast(string)std.file.read(filename); 379 380 // Check for well-formedness 381 //check(s); 382 383 // Make a DOM tree 384 auto doc = new Document(s); 385 386 return load(doc); 387 } catch (CheckException e) { 388 Log.e("Invalid XML file ", filename); 389 return false; 390 } catch (Throwable e) { 391 Log.e("Cannot read drawable resource from file ", filename); 392 return false; 393 } 394 } 395 396 override void drawTo(DrawBuf buf, Rect rc, uint state = 0, int tilex0 = 0, int tiley0 = 0) { 397 foreach(ref item; _stateList) 398 if (item.matchState(state)) { 399 if (!item.drawable.isNull) { 400 if (state & State.Checked) { 401 Log.d("Found item for checked state: ", item.stateMask, " ", item.stateValue); 402 } 403 item.drawable.drawTo(buf, rc, state, tilex0, tiley0); 404 } 405 return; 406 } 407 } 408 409 @property override int width() { 410 return _size.x; 411 } 412 @property override int height() { 413 return _size.y; 414 } 415 @property override Rect padding() { 416 return _paddings; 417 } 418 } 419 420 alias DrawableRef = Ref!Drawable; 421 422 423 424 425 426 /// decoded image cache 427 class ImageCache { 428 429 static class ImageCacheItem { 430 string _filename; 431 DrawBufRef _drawbuf; 432 DrawBufRef[ColorTransform] _transformMap; 433 434 bool _error; // flag to avoid loading of file if it has been failed once 435 bool _used; 436 this(string filename) { 437 _filename = filename; 438 } 439 /// get normal image 440 @property ref DrawBufRef get() { 441 if (!_drawbuf.isNull || _error) { 442 _used = true; 443 return _drawbuf; 444 } 445 _drawbuf = loadImage(_filename); 446 if (_filename.endsWith(".9.png")) 447 _drawbuf.detectNinePatch(); 448 _used = true; 449 if (_drawbuf.isNull) 450 _error = true; 451 return _drawbuf; 452 } 453 /// get color transformed image 454 @property ref DrawBufRef get(ref ColorTransform transform) { 455 if (transform.empty) 456 return get(); 457 if (transform in _transformMap) 458 return _transformMap[transform]; 459 DrawBufRef src = get(); 460 if (src.isNull) 461 _transformMap[transform] = src; 462 else { 463 DrawBufRef t = src.transformColors(transform); 464 _transformMap[transform] = t; 465 } 466 return _transformMap[transform]; 467 } 468 /// remove from memory, will cause reload on next access 469 void compact() { 470 if (!_drawbuf.isNull) 471 _drawbuf.clear(); 472 } 473 /// mark as not used 474 void checkpoint() { 475 _used = false; 476 } 477 /// cleanup if unused since last checkpoint 478 void cleanup() { 479 if (!_used) 480 compact(); 481 } 482 } 483 ImageCacheItem[string] _map; 484 485 /// get and cache image 486 ref DrawBufRef get(string filename) { 487 if (filename in _map) { 488 return _map[filename].get; 489 } 490 ImageCacheItem item = new ImageCacheItem(filename); 491 _map[filename] = item; 492 return item.get; 493 } 494 /// get and cache color transformed image 495 ref DrawBufRef get(string filename, ref ColorTransform transform) { 496 if (transform.empty) 497 return get(filename); 498 if (filename in _map) { 499 return _map[filename].get(transform); 500 } 501 ImageCacheItem item = new ImageCacheItem(filename); 502 _map[filename] = item; 503 return item.get(transform); 504 } 505 // clear usage flags for all entries 506 void checkpoint() { 507 foreach (item; _map) 508 item.checkpoint(); 509 } 510 // removes entries not used after last call of checkpoint() or cleanup() 511 void cleanup() { 512 foreach (item; _map) 513 item.cleanup(); 514 } 515 516 this() { 517 Log.i("Creating ImageCache"); 518 } 519 ~this() { 520 Log.i("Destroying ImageCache"); 521 foreach (ref item; _map) { 522 destroy(item); 523 item = null; 524 } 525 _map.clear(); 526 } 527 } 528 529 __gshared ImageCache _imageCache; 530 /// image cache singleton 531 @property ImageCache imageCache() { return _imageCache; } 532 /// image cache singleton 533 @property void imageCache(ImageCache cache) { 534 if (_imageCache !is null) 535 destroy(_imageCache); 536 _imageCache = cache; 537 } 538 539 __gshared DrawableCache _drawableCache; 540 /// drawable cache singleton 541 @property DrawableCache drawableCache() { return _drawableCache; } 542 /// drawable cache singleton 543 @property void drawableCache(DrawableCache cache) { 544 if (_drawableCache !is null) 545 destroy(_drawableCache); 546 _drawableCache = cache; 547 } 548 549 shared static this() { 550 _imageCache = new ImageCache(); 551 _drawableCache = new DrawableCache(); 552 } 553 554 class DrawableCache { 555 static class DrawableCacheItem { 556 string _id; 557 string _filename; 558 bool _tiled; 559 bool _error; 560 bool _used; 561 DrawableRef _drawable; 562 DrawableRef[ColorTransform] _transformed; 563 564 //private int _instanceCount; 565 this(string id, string filename, bool tiled) { 566 _id = id; 567 _filename = filename; 568 _tiled = tiled; 569 _error = filename is null; 570 //Log.d("Created DrawableCacheItem, count=", ++_instanceCount); 571 } 572 ~this() { 573 _drawable.clear(); 574 //Log.d("Destroyed DrawableCacheItem, count=", --_instanceCount); 575 } 576 /// remove from memory, will cause reload on next access 577 void compact() { 578 if (!_drawable.isNull) 579 _drawable.clear(); 580 } 581 /// mark as not used 582 void checkpoint() { 583 _used = false; 584 } 585 /// cleanup if unused since last checkpoint 586 void cleanup() { 587 if (!_used) 588 compact(); 589 } 590 /// returns drawable (loads from file if necessary) 591 @property ref DrawableRef drawable() { 592 _used = true; 593 if (!_drawable.isNull || _error) 594 return _drawable; 595 if (_filename !is null) { 596 // reload from file 597 if (_filename.endsWith(".xml")) { 598 // XML drawables support 599 StateDrawable d = new StateDrawable(); 600 if (!d.load(_filename)) { 601 destroy(d); 602 _error = true; 603 } else { 604 _drawable = d; 605 } 606 } else { 607 // PNG/JPEG drawables support 608 DrawBufRef image = imageCache.get(_filename); 609 if (!image.isNull) { 610 bool ninePatch = _filename.endsWith(".9.png"); 611 _drawable = new ImageDrawable(image, _tiled, ninePatch); 612 } else 613 _error = true; 614 } 615 } 616 return _drawable; 617 } 618 /// returns drawable (loads from file if necessary) 619 @property ref DrawableRef drawable(ref ColorTransform transform) { 620 if (transform.empty) 621 return drawable(); 622 if (transform in _transformed) 623 return _transformed[transform]; 624 _used = true; 625 if (!_drawable.isNull || _error) 626 return _drawable; 627 if (_filename !is null) { 628 // reload from file 629 if (_filename.endsWith(".xml") || _filename.endsWith(".XML")) { 630 // XML drawables support 631 StateDrawable d = new StateDrawable(); 632 if (!d.load(_filename)) { 633 Log.e("failed to load .xml drawable from ", _filename); 634 destroy(d); 635 _error = true; 636 } else { 637 Log.d("loaded .xml drawable from ", _filename); 638 _drawable = d; 639 } 640 } else { 641 // PNG/JPEG drawables support 642 DrawBufRef image = imageCache.get(_filename, transform); 643 if (!image.isNull) { 644 bool ninePatch = _filename.endsWith(".9.png") || _filename.endsWith(".9.PNG"); 645 _transformed[transform] = new ImageDrawable(image, _tiled, ninePatch); 646 return _transformed[transform]; 647 } else { 648 Log.e("failed to load image from ", _filename); 649 _error = true; 650 } 651 } 652 } 653 return _drawable; 654 } 655 } 656 void clear() { 657 Log.d("DrawableCache.clear()"); 658 _idToFileMap.clear(); 659 foreach(DrawableCacheItem item; _idToDrawableMap) 660 item.drawable.clear(); 661 _idToDrawableMap.clear(); 662 } 663 // clear usage flags for all entries 664 void checkpoint() { 665 foreach (item; _idToDrawableMap) 666 item.checkpoint(); 667 } 668 // removes entries not used after last call of checkpoint() or cleanup() 669 void cleanup() { 670 foreach (item; _idToDrawableMap) 671 item.cleanup(); 672 } 673 string[] _resourcePaths; 674 string[string] _idToFileMap; 675 DrawableCacheItem[string] _idToDrawableMap; 676 DrawableRef _nullDrawable; 677 ref DrawableRef get(string id) { 678 if (id.equal("@null")) 679 return _nullDrawable; 680 if (id in _idToDrawableMap) 681 return _idToDrawableMap[id].drawable; 682 string resourceId = id; 683 bool tiled = false; 684 if (id.endsWith(".tiled")) { 685 resourceId = id[0..$-6]; // remove .tiled 686 tiled = true; 687 } 688 string filename = findResource(resourceId); 689 DrawableCacheItem item = new DrawableCacheItem(id, filename, tiled); 690 _idToDrawableMap[id] = item; 691 return item.drawable; 692 } 693 ref DrawableRef get(string id, ref ColorTransform transform) { 694 if (transform.empty) 695 return get(id); 696 if (id.equal("@null")) 697 return _nullDrawable; 698 if (id in _idToDrawableMap) 699 return _idToDrawableMap[id].drawable(transform); 700 string resourceId = id; 701 bool tiled = false; 702 if (id.endsWith(".tiled")) { 703 resourceId = id[0..$-6]; // remove .tiled 704 tiled = true; 705 } 706 string filename = findResource(resourceId); 707 DrawableCacheItem item = new DrawableCacheItem(id, filename, tiled); 708 _idToDrawableMap[id] = item; 709 return item.drawable(transform); 710 } 711 @property string[] resourcePaths() { 712 return _resourcePaths; 713 } 714 /// set resource directory paths as variable number of parameters 715 void setResourcePaths(string[] paths ...) { 716 resourcePaths(paths); 717 } 718 /// set resource directory paths array (only existing dirs will be added) 719 @property void resourcePaths(string[] paths) { 720 string[] existingPaths; 721 foreach(path; paths) { 722 if (exists(path) && isDir(path)) { 723 existingPaths ~= path; 724 Log.d("DrawableCache: adding path ", path, " to resource dir list."); 725 } else { 726 Log.d("DrawableCache: path ", path, " does not exist."); 727 } 728 } 729 _resourcePaths = existingPaths; 730 clear(); 731 } 732 /// concatenates path with resource id and extension, returns pathname if there is such file, null if file does not exist 733 private string checkFileName(string path, string id, string extension) { 734 char[] fn = path.dup; 735 fn ~= id; 736 fn ~= extension; 737 if (exists(fn) && isFile(fn)) 738 return fn.dup; 739 return null; 740 } 741 string findResource(string id) { 742 if (id in _idToFileMap) 743 return _idToFileMap[id]; 744 foreach(string path; _resourcePaths) { 745 string fn; 746 fn = checkFileName(path, id, ".xml"); 747 if (fn is null) 748 fn = checkFileName(path, id, ".png"); 749 if (fn is null) 750 fn = checkFileName(path, id, ".9.png"); 751 if (fn is null) 752 fn = checkFileName(path, id, ".jpg"); 753 if (fn !is null) { 754 _idToFileMap[id] = fn; 755 return fn; 756 } else { 757 Log.w("resource ", id, " is not found"); 758 } 759 } 760 return null; 761 } 762 this() { 763 Log.i("Creating DrawableCache"); 764 } 765 ~this() { 766 Log.i("Destroying DrawableCache"); 767 foreach (ref item; _idToDrawableMap) { 768 destroy(item); 769 item = null; 770 } 771 _idToDrawableMap.clear(); 772 } 773 } 774