mirror of
https://gitlab.com/freepascal.org/lazarus/lazarus.git
synced 2025-04-06 18:17:59 +02:00
347 lines
9.4 KiB
ObjectPascal
347 lines
9.4 KiB
ObjectPascal
{
|
|
/***************************************************************************
|
|
ImageListCache.pp
|
|
----------------
|
|
Initial Revision : Sun Nov 18 00:04:00 GMT+07 2007
|
|
|
|
|
|
***************************************************************************/
|
|
|
|
*****************************************************************************
|
|
This file is part of the Lazarus Component Library (LCL)
|
|
|
|
See the file COPYING.modifiedLGPL.txt, included in this distribution,
|
|
for details about the license.
|
|
*****************************************************************************
|
|
}
|
|
|
|
unit ImageListCache;
|
|
|
|
{$mode objfpc}{$H+}
|
|
{ $DEFINE VerboseImageListCache}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils,
|
|
Graphics, ImgList, Forms;
|
|
|
|
type
|
|
// interface that cache user should have to listen for cache changes
|
|
IImageCacheListener = interface
|
|
procedure CacheSetImageList(AImageList: TCustomImageList);
|
|
procedure CacheSetImageIndex(AIndex, AImageIndex: Integer);
|
|
end;
|
|
|
|
// cache item
|
|
TImageCacheItem = record
|
|
FImageList: TCustomImageList; // link to imagelist
|
|
FListener: IImageCacheListener; // link to listener
|
|
FImageIndexes: array of Integer; // indexes of imagelist that listener reserved
|
|
end;
|
|
PImageCacheItem = ^TImageCacheItem;
|
|
|
|
{ TImageCacheItems }
|
|
|
|
TImageCacheItems = class(TList)
|
|
private
|
|
function GetItem(AIndex: Integer): PImageCacheItem;
|
|
function GetItemForListener(AListener: IImageCacheListener): PImageCacheItem;
|
|
procedure SetItem(AIndex: Integer; const AValue: PImageCacheItem);
|
|
protected
|
|
procedure Notify(Ptr: Pointer; Action: TListNotification); override;
|
|
public
|
|
function GetNew: PImageCacheItem;
|
|
property Items[AIndex: Integer]: PImageCacheItem read GetItem write SetItem; default;
|
|
end;
|
|
|
|
{ TImageListCache }
|
|
|
|
TImageListCache = class
|
|
private
|
|
FItems: TImageCacheItems;
|
|
FImages: TList;
|
|
FListeners: TInterfaceList;
|
|
FObsoletedCount: Integer;
|
|
procedure CheckRebuildNeed;
|
|
function GetImageListFor(AWidth, AHeight: Integer): TCustomImageList;
|
|
|
|
procedure UnregisterBitmaps(AListener: IImageCacheListener);
|
|
public
|
|
constructor Create;
|
|
destructor Destroy; override;
|
|
|
|
function RegisterListener(AListener: IImageCacheListener): Integer;
|
|
procedure UnregisterListener(AListener: IImageCacheListener);
|
|
procedure RegisterBitmap(AListener: IImageCacheListener; ABitmap: TBitmap; ABitmapCount: Integer = 1);
|
|
procedure Rebuild;
|
|
end;
|
|
|
|
function GetImageListCache: TImageListCache;
|
|
|
|
implementation
|
|
|
|
const
|
|
// number of cache changes that can happen w/o rebuild
|
|
{$IFDEF VerboseImageListCache}
|
|
ImageListCacheRebuildThreshold = 1;
|
|
{$ELSE}
|
|
ImageListCacheRebuildThreshold = 20;
|
|
{$ENDIF}
|
|
|
|
var
|
|
FImageListCache: TImageListCache = nil;
|
|
|
|
function GetImageListCache: TImageListCache;
|
|
begin
|
|
if FImageListCache = nil then
|
|
FImageListCache := TImageListCache.Create;
|
|
Result := FImageListCache;
|
|
end;
|
|
|
|
|
|
{ TImageListCache }
|
|
|
|
procedure TImageListCache.CheckRebuildNeed;
|
|
begin
|
|
if (FObsoletedCount >= ImageListCacheRebuildThreshold) and not Application.Terminated then
|
|
Rebuild;
|
|
end;
|
|
|
|
function TImageListCache.GetImageListFor(AWidth, AHeight: Integer): TCustomImageList;
|
|
var
|
|
i: integer;
|
|
begin
|
|
for i := 0 to FImages.Count - 1 do
|
|
if (TCustomImageList(FImages[i]).Height = AHeight) and
|
|
(TCustomImageList(FImages[i]).Width = AWidth) then
|
|
begin
|
|
Result := TCustomImageList(FImages[i]);
|
|
exit;
|
|
end;
|
|
Result := TCustomImageList.Create(nil);
|
|
FImages.Add(Result);
|
|
with Result do
|
|
begin
|
|
Width := AWidth;
|
|
Height := AHeight;
|
|
Scaled := False;
|
|
{$IFDEF VerboseImageListCache}
|
|
debugln('Creating new imagelist in cache for Width=',Width,' Height=', Height, ' Count = ', FImages.Count);
|
|
if (Width <> 16) and (Width <> 24) then
|
|
DumpStack;
|
|
{$ENDIF}
|
|
end;
|
|
end;
|
|
|
|
procedure TImageListCache.UnregisterBitmaps(AListener: IImageCacheListener);
|
|
var
|
|
Item: PImageCacheItem;
|
|
begin
|
|
Item := FItems.GetItemForListener(AListener);
|
|
|
|
if (Item <> nil) then
|
|
begin
|
|
Item^.FListener := nil;
|
|
inc(FObsoletedCount, Length(Item^.FImageIndexes));
|
|
end;
|
|
CheckRebuildNeed;
|
|
end;
|
|
|
|
constructor TImageListCache.Create;
|
|
begin
|
|
FObsoletedCount := 0;
|
|
FItems := TImageCacheItems.Create;
|
|
FImages := TList.Create;
|
|
FListeners := TInterfaceList.Create;
|
|
end;
|
|
|
|
destructor TImageListCache.Destroy;
|
|
var
|
|
i: integer;
|
|
begin
|
|
FItems.Free;
|
|
for i := 0 to FImages.Count - 1 do
|
|
TObject(FImages[i]).Free;
|
|
FImages.Free;
|
|
FListeners.Free;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
function TImageListCache.RegisterListener(AListener: IImageCacheListener): Integer;
|
|
begin
|
|
Result := FListeners.IndexOf(AListener);
|
|
if Result = -1 then
|
|
Result := FListeners.Add(AListener);
|
|
end;
|
|
|
|
procedure TImageListCache.UnregisterListener(AListener: IImageCacheListener);
|
|
var
|
|
Index: Integer;
|
|
begin
|
|
Index := FListeners.IndexOf(AListener);
|
|
if Index <> -1 then
|
|
begin
|
|
UnregisterBitmaps(AListener);
|
|
FListeners.Remove(AListener);
|
|
end;
|
|
if FListeners.Count = 0 then
|
|
begin
|
|
FImageListCache := nil;
|
|
Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TImageListCache.RegisterBitmap(AListener: IImageCacheListener; ABitmap: TBitmap; ABitmapCount: Integer = 1);
|
|
var
|
|
i, AStart, OldLen: Integer;
|
|
Item: PImageCacheItem;
|
|
OldOnChange: TNotifyEvent;
|
|
begin
|
|
OldOnChange := ABitmap.OnChange;
|
|
ABitmap.OnChange := nil; // prevent further updates
|
|
|
|
try
|
|
RegisterListener(AListener);
|
|
Item := FItems.GetItemForListener(AListener);
|
|
if Item = nil then
|
|
begin
|
|
Item := FItems.GetNew;
|
|
Item^.FImageList := GetImageListFor(ABitmap.Width div ABitmapCount, ABitmap.Height);
|
|
Item^.FListener := AListener;
|
|
end;
|
|
|
|
AStart := Item^.FImageList.AddSliced(ABitmap, ABitmapCount, 1);
|
|
AListener.CacheSetImageList(Item^.FImageList);
|
|
OldLen := Length(Item^.FImageIndexes);
|
|
SetLength(Item^.FImageIndexes, OldLen + Item^.FImageList.Count - AStart);
|
|
|
|
for i := AStart to Item^.FImageList.Count - 1 do
|
|
begin
|
|
Item^.FImageIndexes[OldLen + i - AStart] := i;
|
|
AListener.CacheSetImageIndex(OldLen + i - AStart, i);
|
|
end;
|
|
finally
|
|
ABitmap.OnChange := OldOnChange;
|
|
end;
|
|
end;
|
|
|
|
// cache rebuild
|
|
procedure TImageListCache.Rebuild;
|
|
var
|
|
i, j, k, ACount: integer;
|
|
AListener: IImageCacheListener;
|
|
ADeleted: TBits;
|
|
AChanged: Boolean;
|
|
AIndexes: array of Integer;
|
|
AUpdates: TList;
|
|
begin
|
|
// 1. check what items to be deleted (their listerners are not assigned)
|
|
// 2. delete no more needed images from imagelists
|
|
// 3. notify listeners about new image indexes
|
|
|
|
// traverse all ImageLists
|
|
for i := 0 to FImages.Count - 1 do
|
|
begin
|
|
ACount := TCustomImageList(FImages[i]).Count;
|
|
ADeleted := TBits.Create(ACount);
|
|
AChanged := False;
|
|
AUpdates := TList.Create;
|
|
// traverse for all items
|
|
// if item is to be deleted then set flag in ADeleted, else add item to AUpdates array
|
|
for j := FItems.Count - 1 downto 0 do
|
|
if FItems[j]^.FImageList = TCustomImageList(FImages[i]) then
|
|
begin
|
|
for k := 0 to High(FItems[j]^.FImageIndexes) do
|
|
ADeleted.Bits[FItems[j]^.FImageIndexes[k]] := FItems[j]^.FListener = nil;
|
|
if FItems[j]^.FListener = nil then
|
|
begin
|
|
FItems.Delete(j);
|
|
AChanged := True;
|
|
end
|
|
else
|
|
AUpdates.Add(FItems[j]);
|
|
end;
|
|
// is something has been deleted from current imagelist then
|
|
// we continue processing
|
|
if AChanged then
|
|
begin
|
|
// AIndexes is our old=>new image indexes map
|
|
// at first step we set old=old and at same moment clearing our imagelist
|
|
SetLength(AIndexes, ACount);
|
|
for j := High(AIndexes) downto 0 do
|
|
begin
|
|
AIndexes[j] := j;
|
|
if ADeleted[j] then
|
|
TCustomImageList(FImages[i]).Delete(j);
|
|
end;
|
|
// we traversing our indexes map and set new values for old values
|
|
for j := 0 to High(AIndexes) do
|
|
if ADeleted[j] then
|
|
begin
|
|
for k := j + 1 to High(AIndexes) do
|
|
dec(AIndexes[k]);
|
|
end;
|
|
// all preparation done - we have old=>new map
|
|
// process all Items that needs to be updated
|
|
for j := 0 to AUpdates.Count - 1 do
|
|
begin
|
|
AListener := PImageCacheItem(AUpdates[j])^.FListener;
|
|
for k := 0 to High(PImageCacheItem(AUpdates[j])^.FImageIndexes) do
|
|
begin
|
|
// update cache item and notify listener
|
|
PImageCacheItem(AUpdates[j])^.FImageIndexes[k] := AIndexes[PImageCacheItem(AUpdates[j])^.FImageIndexes[k]];
|
|
AListener.CacheSetImageIndex(k, PImageCacheItem(AUpdates[j])^.FImageIndexes[k]);
|
|
end;
|
|
end;
|
|
end;
|
|
AUpdates.Free;
|
|
ADeleted.Free;
|
|
SetLength(AIndexes, 0);
|
|
end;
|
|
|
|
FObsoletedCount := 0;
|
|
end;
|
|
|
|
{ TImageCacheItems }
|
|
|
|
function TImageCacheItems.GetItem(AIndex: Integer): PImageCacheItem;
|
|
begin
|
|
Result := inherited Get(AIndex)
|
|
end;
|
|
|
|
procedure TImageCacheItems.SetItem(AIndex: Integer;
|
|
const AValue: PImageCacheItem);
|
|
begin
|
|
inherited Put(AIndex, AValue);
|
|
end;
|
|
|
|
procedure TImageCacheItems.Notify(Ptr: Pointer; Action: TListNotification);
|
|
begin
|
|
if (Action = lnDeleted) and (Ptr <> nil) then
|
|
Dispose(PImageCacheItem(Ptr));
|
|
end;
|
|
|
|
function TImageCacheItems.GetNew: PImageCacheItem;
|
|
begin
|
|
New(Result);
|
|
Add(Result);
|
|
end;
|
|
|
|
function TImageCacheItems.GetItemForListener(AListener: IImageCacheListener): PImageCacheItem;
|
|
var
|
|
i: integer;
|
|
begin
|
|
Result := nil;
|
|
for i := 0 to Count - 1 do
|
|
if Items[i]^.FListener = AListener then
|
|
begin
|
|
Result := Items[i];
|
|
break;
|
|
end;
|
|
end;
|
|
|
|
end.
|
|
|
|
|