1 {
2  /***************************************************************************
3                                ImageListCache.pp
4                                ----------------
5                    Initial Revision  : Sun Nov 18 00:04:00 GMT+07 2007
6 
7 
8  ***************************************************************************/
9 
10  *****************************************************************************
11   This file is part of the Lazarus Component Library (LCL)
12 
13   See the file COPYING.modifiedLGPL.txt, included in this distribution,
14   for details about the license.
15  *****************************************************************************
16 }
17 
18 unit ImageListCache;
19 
20 {$mode objfpc}{$H+}
21 { $DEFINE VerboseImageListCache}
22 
23 interface
24 
25 uses
26   Classes, SysUtils, Graphics, ImgList, LCLProc, Forms;
27 
28 type
29   // interface that cache user should have to listen for cache changes
30   IImageCacheListener = interface
31     procedure CacheSetImageList(AImageList: TCustomImageList);
32     procedure CacheSetImageIndex(AIndex, AImageIndex: Integer);
33   end;
34 
35   // cache item
36   TImageCacheItem = record
37     FImageList: TCustomImageList;    // link to imagelist
38     FListener: IImageCacheListener;  // link to listener
39     FImageIndexes: array of Integer; // indexes of imagelist that listener reserved
40   end;
41   PImageCacheItem = ^TImageCacheItem;
42 
43   { TImageCacheItems }
44 
45   TImageCacheItems = class(TList)
46   private
GetItemnull47     function GetItem(AIndex: Integer): PImageCacheItem;
GetItemForListenernull48     function GetItemForListener(AListener: IImageCacheListener): PImageCacheItem;
49     procedure SetItem(AIndex: Integer; const AValue: PImageCacheItem);
50   protected
51     procedure Notify(Ptr: Pointer; Action: TListNotification); override;
52   public
GetNewnull53     function GetNew: PImageCacheItem;
54     property Items[AIndex: Integer]: PImageCacheItem read GetItem write SetItem; default;
55   end;
56 
57   { TImageListCache }
58 
59   TImageListCache = class
60   private
61     FItems: TImageCacheItems;
62     FImages: TList;
63     FListeners: TInterfaceList;
64     FObsoletedCount: Integer;
65     procedure CheckRebuildNeed;
GetImageListFornull66     function GetImageListFor(AWidth, AHeight: Integer): TCustomImageList;
67 
68     procedure UnregisterBitmaps(AListener: IImageCacheListener);
69   public
70     constructor Create;
71     destructor Destroy; override;
72 
RegisterListenernull73     function RegisterListener(AListener: IImageCacheListener): Integer;
74     procedure UnregisterListener(AListener: IImageCacheListener);
75     procedure RegisterBitmap(AListener: IImageCacheListener; ABitmap: TBitmap; ABitmapCount: Integer = 1);
76     procedure Rebuild;
77   end;
78 
GetImageListCachenull79   function GetImageListCache: TImageListCache;
80 
81 implementation
82 
83 const
84   // number of cache changes that can happen w/o rebuild
85 {$IFDEF VerboseImageListCache}
86   ImageListCacheRebuildThreshold = 1;
87 {$ELSE}
88   ImageListCacheRebuildThreshold = 20;
89 {$ENDIF}
90 
91 var
92   FImageListCache: TImageListCache = nil;
93 
GetImageListCachenull94 function GetImageListCache: TImageListCache;
95 begin
96   if FImageListCache = nil then
97     FImageListCache := TImageListCache.Create;
98   Result := FImageListCache;
99 end;
100 
101 
102 { TImageListCache }
103 
104 procedure TImageListCache.CheckRebuildNeed;
105 begin
106   if (FObsoletedCount >= ImageListCacheRebuildThreshold) and not Application.Terminated then
107     Rebuild;
108 end;
109 
GetImageListFornull110 function TImageListCache.GetImageListFor(AWidth, AHeight: Integer): TCustomImageList;
111 var
112   i: integer;
113 begin
114   for i := 0 to FImages.Count - 1 do
115     if (TCustomImageList(FImages[i]).Height = AHeight) and
116        (TCustomImageList(FImages[i]).Width = AWidth) then
117     begin
118       Result := TCustomImageList(FImages[i]);
119       exit;
120     end;
121   Result := TCustomImageList.Create(nil);
122   FImages.Add(Result);
123   with Result do
124   begin
125     Width := AWidth;
126     Height := AHeight;
127     Scaled := False;
128 {$IFDEF VerboseImageListCache}
129     debugln('Creating new imagelist in cache for Width=',Width,' Height=', Height, ' Count = ', FImages.Count);
130     if (Width <> 16) and (Width <> 24) then
131       DumpStack;
132 {$ENDIF}
133   end;
134 end;
135 
136 procedure TImageListCache.UnregisterBitmaps(AListener: IImageCacheListener);
137 var
138   Item: PImageCacheItem;
139 begin
140   Item := FItems.GetItemForListener(AListener);
141 
142   if (Item <> nil) then
143   begin
144     Item^.FListener := nil;
145     inc(FObsoletedCount, Length(Item^.FImageIndexes));
146   end;
147   CheckRebuildNeed;
148 end;
149 
150 constructor TImageListCache.Create;
151 begin
152   FObsoletedCount := 0;
153   FItems := TImageCacheItems.Create;
154   FImages := TList.Create;
155   FListeners := TInterfaceList.Create;
156 end;
157 
158 destructor TImageListCache.Destroy;
159 var
160   i: integer;
161 begin
162   FItems.Free;
163   for i := 0 to FImages.Count - 1 do
164     TObject(FImages[i]).Free;
165   FImages.Free;
166   FListeners.Free;
167   inherited Destroy;
168 end;
169 
TImageListCache.RegisterListenernull170 function TImageListCache.RegisterListener(AListener: IImageCacheListener): Integer;
171 begin
172   Result := FListeners.IndexOf(AListener);
173   if Result = -1 then
174     Result := FListeners.Add(AListener);
175 end;
176 
177 procedure TImageListCache.UnregisterListener(AListener: IImageCacheListener);
178 var
179   Index: Integer;
180 begin
181   Index := FListeners.IndexOf(AListener);
182   if Index <> -1 then
183   begin
184     UnregisterBitmaps(AListener);
185     FListeners.Remove(AListener);
186   end;
187   if FListeners.Count = 0 then
188   begin
189     FImageListCache := nil;
190     Free;
191   end;
192 end;
193 
194 procedure TImageListCache.RegisterBitmap(AListener: IImageCacheListener; ABitmap: TBitmap; ABitmapCount: Integer = 1);
195 var
196   i, AStart, OldLen: Integer;
197   Item: PImageCacheItem;
198   OldOnChange: TNotifyEvent;
199 begin
200   OldOnChange := ABitmap.OnChange;
201   ABitmap.OnChange := nil; // prevent further updates
202 
203   try
204     RegisterListener(AListener);
205     Item := FItems.GetItemForListener(AListener);
206     if Item = nil then
207     begin
208       Item := FItems.GetNew;
209       Item^.FImageList := GetImageListFor(ABitmap.Width div ABitmapCount, ABitmap.Height);
210       Item^.FListener := AListener;
211     end;
212 
213     AStart := Item^.FImageList.AddSliced(ABitmap, ABitmapCount, 1);
214     AListener.CacheSetImageList(Item^.FImageList);
215     OldLen := Length(Item^.FImageIndexes);
216     SetLength(Item^.FImageIndexes, OldLen + Item^.FImageList.Count - AStart);
217 
218     for i := AStart to Item^.FImageList.Count - 1 do
219     begin
220       Item^.FImageIndexes[OldLen + i - AStart] := i;
221       AListener.CacheSetImageIndex(OldLen + i - AStart, i);
222     end;
223   finally
224     ABitmap.OnChange := OldOnChange;
225   end;
226 end;
227 
228 // cache rebuild
229 procedure TImageListCache.Rebuild;
230 var
231   i, j, k, ACount: integer;
232   AListener: IImageCacheListener;
233   ADeleted: TBits;
234   AChanged: Boolean;
235   AIndexes: array of Integer;
236   AUpdates: TList;
237 begin
238   // 1. check what items to be deleted (their listerners are not assigned)
239   // 2. delete no more needed images from imagelists
240   // 3. notify listeners about new image indexes
241 
242   // traverse all ImageLists
243   for i := 0 to FImages.Count - 1 do
244   begin
245     ACount := TCustomImageList(FImages[i]).Count;
246     ADeleted := TBits.Create(ACount);
247     AChanged := False;
248     AUpdates := TList.Create;
249     // traverse for all items
250     // if item is to be deleted then set flag in ADeleted, else add item to AUpdates array
251     for j := FItems.Count - 1 downto 0 do
252       if FItems[j]^.FImageList = TCustomImageList(FImages[i]) then
253       begin
254         for k := 0 to High(FItems[j]^.FImageIndexes) do
255           ADeleted.Bits[FItems[j]^.FImageIndexes[k]] := FItems[j]^.FListener = nil;
256         if FItems[j]^.FListener = nil then
257         begin
258           FItems.Delete(j);
259           AChanged := True;
260         end
261         else
262           AUpdates.Add(FItems[j]);
263       end;
264     // is something has been deleted from current imagelist then
265     // we continue processing
266     if AChanged then
267     begin
268       // AIndexes is our old=>new image indexes map
269       // at first step we set old=old and at same moment clearing our imagelist
270       SetLength(AIndexes, ACount);
271       for j := High(AIndexes) downto 0 do
272       begin
273         AIndexes[j] := j;
274         if ADeleted[j] then
275           TCustomImageList(FImages[i]).Delete(j);
276       end;
277       // we traversing our indexes map and set new values for old values
278       for j := 0 to High(AIndexes) do
279         if ADeleted[j] then
280         begin
281           for k := j + 1 to High(AIndexes) do
282             dec(AIndexes[k]);
283         end;
284       // all preparation done - we have old=>new map
285       // process all Items that needs to be updated
286       for j := 0 to AUpdates.Count - 1 do
287       begin
288         AListener := PImageCacheItem(AUpdates[j])^.FListener;
289         for k := 0 to High(PImageCacheItem(AUpdates[j])^.FImageIndexes) do
290         begin
291           // update cache item and notify listener
292           PImageCacheItem(AUpdates[j])^.FImageIndexes[k] := AIndexes[PImageCacheItem(AUpdates[j])^.FImageIndexes[k]];
293           AListener.CacheSetImageIndex(k, PImageCacheItem(AUpdates[j])^.FImageIndexes[k]);
294         end;
295       end;
296     end;
297     AUpdates.Free;
298     ADeleted.Free;
299     SetLength(AIndexes, 0);
300   end;
301 
302   FObsoletedCount := 0;
303 end;
304 
305 { TImageCacheItems }
306 
GetItemnull307 function TImageCacheItems.GetItem(AIndex: Integer): PImageCacheItem;
308 begin
309   Result := inherited Get(AIndex)
310 end;
311 
312 procedure TImageCacheItems.SetItem(AIndex: Integer;
313   const AValue: PImageCacheItem);
314 begin
315   inherited Put(AIndex, AValue);
316 end;
317 
318 procedure TImageCacheItems.Notify(Ptr: Pointer; Action: TListNotification);
319 begin
320   if (Action = lnDeleted) and (Ptr <> nil) then
321     Dispose(PImageCacheItem(Ptr));
322 end;
323 
GetNewnull324 function TImageCacheItems.GetNew: PImageCacheItem;
325 begin
326   New(Result);
327   Add(Result);
328 end;
329 
GetItemForListenernull330 function TImageCacheItems.GetItemForListener(AListener: IImageCacheListener): PImageCacheItem;
331 var
332   i: integer;
333 begin
334   Result := nil;
335   for i := 0 to Count - 1 do
336     if Items[i]^.FListener = AListener then
337     begin
338       Result := Items[i];
339       break;
340     end;
341 end;
342 
343 end.
344 
345 
346