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