{
 *****************************************************************************
  This file is part of a Lazarus Package, IconFinder.

  See the file COPYING.modifiedLGPL.txt, included in the Lazarus distribution,
  for details about the license.
 *****************************************************************************

 Icon thumbnails.
}

unit IconThumbnails;

{$mode ObjFPC}{$H+}
{$define OVERLAY_ICONS}
{$define SPEED_TIMER}
{$WARN 6058 off : Call to subroutine "$1" marked as inline is not inlined}
interface

uses
  Classes, SysUtils, fgl, contnrs, FPImage, FPReadBMP, FPReadPNG, StrUtils,
  LazLoggerBase, laz2_dom, laz2_xmlread, laz2_xmlwrite, IntfGraphics,
  FileUtil, LazFileUtils, Graphics, Controls, Dialogs, Menus, Forms,
  IconFinderStrConsts, BasicThumbnails;

type
  TIconItem = class;
  TIconThumbnailViewer = class;

  TIconStyle = (isAnyStyle, isClassic, isFlat, isOutline, isOutline2);

  TIconThumbnail = class(TBasicThumbnail)
  private
    FItem: TIconItem;
  public
    constructor Create(AItem: TIconItem); reintroduce;
    procedure DrawToCanvas(ACanvas: TCanvas; ARect: TRect); override;
    property Item: TIconItem read FItem;
  end;

  TIconItem = class(TObject)
  private
    FFileName: String;   // including path
    FWidth: Integer;
    FHeight: Integer;
    FStyle: TIconStyle;
    FKeywords: TStrings;
    FPicture: TPicture;
    FHidden: Boolean;
    FViewer: TIconThumbnailViewer;
    procedure SetStyleAsString(AValue: String);
  protected
    function GetDirectory: String;
    function GetKeywordCount: Integer;
    function GetKeywords(AIndex: Integer): String;
    function GetKeywordsAsString: String;
    function GetName: String;
    function GetNameBase: String;
    function GetPicture: TPicture;
    function GetSizeAsString: String;
    function GetStyleAsString: String;
  public
    constructor Create(AFileName, AKeywords: String; AStyle: TIconStyle; AWidth, AHeight: Integer);
    destructor Destroy; override;
    procedure CopyMetadataFrom(AIcon: TIconItem);
    procedure ExportKeywordsToStrings(AList: TStrings);
    function HasKeyword(AKeyword: String): Boolean;
    function HasKeywordPart(AKeywordPart: String): Boolean;
    procedure ReleasePicture;
    procedure SetKeywordsFromStrings(AList: TStrings);

    property Directory: String read GetDirectory;
    property FileName: String read FFileName;
    property Height: Integer read FHeight;
    property Hidden: Boolean read FHidden write FHidden;
    property KeywordCount: Integer read GetKeywordCount;
    property Keywords[AIndex: Integer]: String read GetKeywords;
    property KeywordsAsString: String read GetKeywordsAsString;
    property Name: String read GetName;
    property NameBase: String read GetNameBase;
    property Picture: TPicture read GetPicture;
    property SizeAsString: String read GetSizeAsString;
    property Style: TIconStyle read FStyle write FStyle;
    property StyleAsString: String read GetStyleAsString write SetStyleAsString;
    property Width: Integer read FWidth;
  end;

  TIconList = class(TFPHashObjectList)
  private
    function GetItem(AIndex: Integer): TIconItem;
    procedure SetItem(AIndex: Integer; AValue: TIconItem);
  public
    function IndexOfFileName(AFileName: String): Integer;
    property Items[AIndex: Integer]: TIconItem read GetItem write SetItem; default;
  end;

  TIconFolderItem = class
    FolderName: String;
    Hidden: Boolean;
    Dirty: Boolean;
  end;

  TIconFolderList = class(specialize TFPGObjectlist<TIconFolderItem>)
  public
    function AddFolder(AFolderName: String; IsHidden: Boolean): Integer;
    procedure Hide(AFolder: String);
    procedure Hide(AIndex: Integer);
    function IndexOf(AFolder: String): Integer;
    function IsHidden(AFolder: string): Boolean;
    function IsHidden(AIndex: Integer): Boolean;
    procedure Show(AFolder: string);
    procedure Show(AIndex: Integer);
    procedure Toggle(AFolder: String);
    procedure Toggle(AIndex: Integer);
  end;

  TIconThumbnailViewer = class(TBasicThumbnailViewer)
  private
    FIconFolders: TIconFolderList;
    FIconList: TIconList;
    FSizes: TStrings;
    FFilterByIconSize: String;
    FFilterByIconStyle: TIconStyle;
    FFilterByIconWidth: Integer;
    FFilterByIconHeight: Integer;
    FFilterByIconKeywords: String;
    FLargestIconWidth: Integer;
    FLargestIconHeight: Integer;
    FAutoThumbnailSize: Boolean;
    FFilterLock: Integer;
    FOnFilter: TNotifyEvent;
    function GetIconCount: Integer;
    procedure IconFolderClicked(Sender: TObject);
    procedure SetFilterByIconKeywords(AValue: String);
    procedure SetFilterByIconSize(AValue: String);
    procedure SetFilterByIconStyle(AValue: TIconStyle);

  protected
    FSelectedIcon: TIconItem;
    {$ifdef OVERLAY_ICONS}
    FOverlayIcons: array[0..2] of TGraphic;
    procedure DrawThumbnail(AThumbnail: TBasicThumbnail; ARect: TRect); override;
    {$endif}
    function AcceptIcon(AIcon: TIconItem): Boolean; virtual;
    function AcceptKeywords(AIcon: TIconItem): Boolean;
    function AddIcon(AFileName, AKeywords: String; AStyle: TIconStyle; AWidth, AHeight: Integer): TIconItem;
    procedure AddIconToMetafile(AFileName: String; AHidden: Boolean);
    procedure DeleteIconFolder(AFolder: String);
    procedure FilterIcons;
    function MetadataDirty: Boolean;
    procedure ReadIconFolder(AFolder: String);
    procedure ReadIcons(AFolder: String; AHidden: Boolean);
    procedure ReadMetadataFile(AFileName: String; AHidden: Boolean);
    procedure SetSelectedIndex(AValue: Integer); override;
    function ThumbnailMarked(AThumbnail: TBasicThumbnail): Boolean; override;
    procedure ThumbnailOutside(AThumbnail: TBasicThumbnail); override;

  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure AddIconFolder(AFolder: String; Hidden: Boolean = false);
    procedure Clear; override;
    procedure CopyMetadataToNameBase(AIcon: TIconItem);
    procedure DeleteIcon(AIcon: TIconItem);
    function FilterLocked: Boolean;
    function FindIconSize(AIcon: TIconItem; AWidth, AHeight: Integer): TIconItem;
    function FindLargestIcon(AIcon: TIconItem): TIconItem;
    procedure GetIconSizesAsStrings(AList: TStrings);
    procedure GetKeywordsAsStrings(AList: TStrings);
    function IndexOfFolder(AFolder: String): Integer;
    function IndexOfThumbnail(AIcon: TIconItem): Integer;
    procedure LockFilter;
    procedure PopulateIconFoldersMenu(AMenu: TMenu);
    procedure ReadIconFolders(AList: TStrings);
    function SelectIconInFile(AFileName: String): Boolean;
    procedure UnlockFilter;
    procedure UpdateIconFolders;
    procedure WriteIconFolders(AList: TStrings);
    procedure WriteMetadataFiles;

    property AutoThumbnailSize: Boolean read FAutoThumbnailSize write FAutoThumbnailSize default true;
    property FilterByIconKeywords: string read FFilterByIconKeywords write SetFilterByIconKeywords;
    property FilterByIconSize: string read FFilterByIconSize write SetFilterByIconSize;
    property FilterByIconStyle: TIconStyle read FFilterByIconStyle write SetFilterByIconStyle;
    property IconCount: Integer read GetIconCount;
    property IconFolders: TIconFolderList read FIconFolders;
    property LargestIconWidth: Integer read FLargestIconWidth;
    property LargestIconHeight: Integer read FLargestIconHeight;
    property SelectedIcon: TIconItem read FSelectedIcon;
    property OnFilter: TNotifyEvent read FOnFilter write FOnFilter;

  end;

function IconStyleToStr(AStyle: TIconStyle): String;
procedure IconStylesToStrings(AList: TStrings);
function StrToIconStyle(AText: String): TIconStyle;

const
  IMAGES_MASK = '*.png;*.bmp';

implementation

{$ifdef OVERLAY_ICONS}
 {$R overlay.res}
{$endif}

const
  ICON_MARGIN = 8;  // or, more precisely: double of margin
  METADATA_FILENAME = 'metadata.xml';
  ICONSTYLE_NAMES: Array[TIconStyle] of String = (
    '(any style)', 'classic', 'flat', 'outline', 'outline 2-color'
  );

function IconStyleToStr(AStyle: TIconStyle): String;
begin
  if AStyle = isAnyStyle then
    Result := ''
  else
    Result := ICONSTYLE_NAMES[AStyle];
end;

procedure IconStylesToStrings(AList: TStrings);
var
  style: TIconStyle;
begin
  AList.Clear;
  for style in TIconStyle do
    if style = isAnyStyle then
      AList.Add(RSMetadata_AnyStyle)
    else
      AList.Add(ICONSTYLE_NAMES[style]);
end;

function StrToIconStyle(AText: String): TIconStyle;
begin
  case lowercase(AText) of
    '':
      Result := isAnyStyle;
    'classic':
      Result := isClassic;
    'flat':
      Result := isFlat;
    'outline':
      Result := isOutline;
    'outline2', 'outline 2-color':
      Result := isOutline2;
    else
      raise Exception.Create('[StrToIconStyle] Unknown icon style');
  end;
end;


{ TIconThumbnail }

constructor TIconThumbnail.Create(AItem: TIconItem);
begin
  inherited Create;
  FItem := AItem;  // Do not destroy this item!
end;

procedure TIconThumbnail.DrawToCanvas(ACanvas: TCanvas; ARect: TRect);
var
  pic: TPicture;
  x, y: Integer;
begin
  pic := FItem.Picture;
  if pic <> nil then
  begin
    x := (Viewer.ThumbnailWidth - pic.Width) div 2;
    y := (Viewer.ThumbnailHeight - pic.Height) div 2;
    if Assigned(pic.Bitmap) then
      pic.Bitmap.Transparent := true;
    ACanvas.Draw(ARect.Left + x, ARect.Top + y, pic.Bitmap);
  end;
end;


{ TIconItem }

constructor TIconItem.Create(AFileName, AKeywords: String; AStyle: TIconStyle;
  AWidth, AHeight: Integer);
var
  i: Integer;
begin
  inherited Create;
  FFileName := AFileName;
  FStyle := AStyle;
  FWidth := AWidth;
  FHeight := AHeight;
  FKeywords := TStringList.Create;
  TStringList(FKeywords).Sorted := true;
  TStringList(FKeywords).CaseSensitive := false;
  TStringList(FKeywords).Duplicates := dupIgnore;
  FKeywords.Delimiter := ';';
  FKeywords.StrictDelimiter := true;
  FKeywords.DelimitedText := AKeywords;
  for i := FKeywords.Count-1 downto 0 do  // No empty keywords
    if FKeywords[i] = '' then FKeywords.Delete(i);
end;

destructor TIconItem.Destroy;
begin
  FKeywords.Free;
  FreeAndNil(FPicture);
  inherited;
end;

procedure TIconItem.CopyMetadataFrom(AIcon: TIconItem);
var
  folder: String;
  idx: Integer;
begin
  FKeywords.Assign(AIcon.FKeywords);
  FStyle := AIcon.FStyle;

  // Mark the folder of the icon as "dirty" so that it can be re-written.
  folder := AIcon.GetDirectory;
  idx := FViewer.IconFolders.IndexOf(folder);
  if idx > -1 then
    FViewer.IconFolders[idx].Dirty := true;
end;

function TIconItem.GetDirectory: String;
begin
  Result := ExtractFilePath(FFileName);
end;

function TIconItem.GetKeywordCount: Integer;
begin
  Result := FKeywords.Count;
end;

function TIconItem.GetKeywords(AIndex: Integer): String;
begin
  Result := FKeywords[AIndex];
end;

function TIconItem.GetKeywordsAsString: String;
var
  i: Integer;
begin
  if FKeywords.Count = 0 then
    Result := ''
  else
  begin
    Result := FKeywords[0];
    for i := 1 to FKeywords.Count-1 do
      Result := Result + ';' + FKeywords[i];
  end;
end;

function TIconItem.GetName: String;
begin
  Result := ChangeFileExt(ExtractFileName(FFileName), '');
end;

function TIconItem.GetNameBase: String;
var
  p, n: Integer;
  suffix: String;
begin
  Result := GetName;
  p := RPos('_', Result);
  if p > 0 then
  begin
    suffix := Copy(Result, p+1);
    if TryStrToInt(suffix, n) then
      Result := Copy(Result, 1, p-1);
  end;
end;

function TIconItem.GetPicture: TPicture;
begin
  if FPicture = nil then
  begin
    FPicture := TPicture.Create;
    FPicture.LoadFromFile(FFileName);
  end;
  Result := FPicture;
end;

function TIconItem.GetSizeAsString: String;
begin
  Result := Format('%d x %d', [FWidth, FHeight]);
end;

function TIconItem.GetStyleAsString: String;
begin
  Result := ICONSTYLE_NAMES[FStyle];
end;

function TIconItem.HasKeyword(AKeyword: String): Boolean;
begin
  Result := FKeywords.IndexOf(AKeyword) <> -1;
end;

function TIconItem.HasKeywordPart(AKeywordPart: String): Boolean;
var
  i: Integer;
begin
  Result := true;
  AKeywordPart := Lowercase(AKeywordPart);
  for i := 0 to FKeywords.Count-1 do
    if pos(AKeywordPart, Lowercase(FKeywords[i])) = 1 then
      exit;
  Result := false;
end;

procedure TIconItem.ExportKeywordsToStrings(AList: TStrings);
begin
  Assert(AList <> nil);
  AList.Assign(FKeywords);
end;

procedure TIconItem.ReleasePicture;
begin
  FreeAndNil(FPicture);
end;

procedure TIconItem.SetKeywordsFromStrings(AList: TStrings);
var
  i, j: Integer;
begin
  FKeywords.BeginUpdate;
  try
    FKeywords.Clear;
    for i := 0 to AList.Count-1 do
    begin
      j := FKeywords.IndexOf(AList[i]);
      if j = -1 then
        FKeywords.Add(AList[i]);
    end;
  finally
    FKeywords.EndUpdate;
  end;
end;

procedure TIconItem.SetStyleAsString(AValue: String);
begin
  FStyle := StrToIconStyle(AValue);
end;


{ TIconList }

function TIconList.GetItem(AIndex: Integer): TIconItem;
begin
  Result := TIconItem(inherited Items[AIndex]);
end;

function TIconList.IndexOfFileName(AFileName: String): Integer;
begin
  Result := FindIndexOf(AFileName);
end;

procedure TIconList.SetItem(AIndex: Integer; AValue: TIconItem);
begin
  inherited Items[AIndex] := AValue;
end;


{ TIconFolderList

  A list with the folder names stored in TIconFolderItem instances.
  Besides the folder names, each item contains a flag to hide the icons of
  that folder, as well as a Dirty flag to indicate that there are icons in
  that folder with modified metadata and that the folder's metadata need
  re-saving.

  Each folder name is stored with correct and trailing path delimiters.
}
function TIconFolderList.AddFolder(AFolderName: String; IsHidden: Boolean): Integer;
var
  item: TIconFolderItem;
begin
  item := TIconFolderItem.Create;
  item.FolderName := AppendPathDelim(SwitchPathDelims(AFolderName, true));
  item.Hidden := IsHidden;
  item.Dirty := false;
  Result := Add(item);
end;

procedure TIconFolderList.Hide(AFolder: String);
var
  idx: Integer;
begin
  idx := IndexOf(AFolder);
  if idx > -1 then
    Hide(idx);
end;

procedure TIconFolderList.Hide(AIndex: Integer);
begin
  Items[AIndex].Hidden := true;
end;

function TIconFolderList.IndexOf(AFolder: String): Integer;
var
  i: Integer;
begin
  AFolder := SwitchPathDelims(AppendPathDelim(AFolder), true);
  for i := 0 to Count-1 do
  begin
    if Items[i].FolderName = AFolder then
    begin
      Result := i;
      exit;
    end;
  end;
  Result := -1;
end;

function TIconFolderList.IsHidden(AFolder: string): Boolean;
var
  idx: Integer;
begin
  idx := IndexOf(AFolder);
  if idx > -1 then
    Result := Items[idx].Hidden
  else
    Result := false;
end;

function TIconFolderList.IsHidden(AIndex: Integer): Boolean;
begin
  Result := Items[AIndex].Hidden;
end;

procedure TIconFolderList.Show(AFolder: string);
var
  idx: Integer;
begin
  idx := IndexOf(AFolder);
  if idx > -1 then
    Show(idx);
end;

procedure TIconFolderList.Show(AIndex: Integer);
begin
  Items[AIndex].Hidden := false;
end;

procedure TIconFolderList.Toggle(AFolder: String);
var
  idx: Integer;
begin
  idx := IndexOf(AFolder);
  if idx > -1 then
  begin
    Toggle(idx);
  end;
end;

procedure TIconFolderList.Toggle(AIndex: Integer);
begin
  Items[AIndex].Hidden := not Items[AIndex].Hidden;
end;


{ TIconThumbnailViewer }

constructor TIconThumbnailViewer.Create(AOwner: TComponent);
begin
  inherited;
  FIconFolders := TIconFolderList.Create;
  FIconList := TIconList.Create;
  FSizes := TStringList.Create;
  TStringList(FSizes).Sorted := true;
  FAutoThumbnailSize := true;
  {$ifdef OVERLAY_ICONS}
  FOverlayIcons[0] := TPortableNetworkGraphic.Create;
  FOverlayIcons[1] := TPortableNetworkGraphic.Create;
  FOverlayIcons[2] := TPortableNetworkGraphic.Create;
  FOverlayIcons[0].LoadFromResourceName(HINSTANCE, 'ovl_H_8');
  FOverlayIcons[1].LoadFromResourceName(HINSTANCE, 'ovl_H_12');
  FOverlayIcons[2].LoadFromResourceName(HINSTANCE, 'ovl_H_16');
  {$endif}
end;

destructor TIconThumbnailViewer.Destroy;
var
  res: Integer;
begin
  {$ifdef OVERLAY_ICONS}
  FOverlayIcons[2].Free;
  FOverlayIcons[1].Free;
  FOverlayIcons[0].Free;
  {$endif}
  if MetadataDirty then
  begin
    res := MessageDlg(RSIconViewer_AskSaveMetadata, mtConfirmation, [mbYes, mbNo], 0);
    if res = mrYes then
      WriteMetadataFiles;
  end;
  FSizes.Free;
  FIconList.Free;
  FIconFolders.Free;
  inherited;
end;

{ Main filtering method: Compares the icon properties with the filter conditions
  and returns true when the icon can be displayed as a thumbnail. }
function TIconThumbnailViewer.AcceptIcon(AIcon: TIconItem): Boolean;
begin
  Result := false;
  if AIcon.Hidden then
    exit;
  if (FFilterByIconSize <> '') and ((AIcon.Width <> FFilterByIconWidth) or (AIcon.Height <> FFilterByIconHeight)) then
    exit;
  if (FFilterByIconStyle <> isAnyStyle) and (AIcon.Style <> FFilterByIconStyle) then
    exit;
  if (FFilterByIconKeywords <> '') and not AcceptKeywords(AIcon) then
    exit;
  Result := true;
  if Result then
  begin
    if FLargestIconWidth < AIcon.Width then FLargestIconWidth := AIcon.Width;
    if FLargestIconHeight < AIcon.Height then FLargestIconHeight := AIcon.Height;
  end;
end;

{ Implements a simple parser for logical expressions between keywords, e.g.
     keyword1 AND keyword2
     keyword1 OR NOT keyword2 AND keyword3 AND NOT keyword4
     keyword1 keyword2  -- defaults to AND
  No brackets allowed! }
function TIconThumbnailViewer.AcceptKeywords(AIcon: TIconItem): Boolean;
type
  TOperationKind = (opkAND, opkOR, opkNOT);
  TOperation = record
    Kind: TOperationKind;
    Left, Right: Boolean;
    Complete: Boolean;
  end;
const
  DEFAULT_OPERATION = opkAND;
var
  operation: TOperation;
  i: Integer;
  parts: TStringArray;
begin
  Result := True;
  parts := FFilterByIconKeywords.Split(' ');

  operation := Default(TOperation);
  operation.Kind := DEFAULT_OPERATION;
  operation.Left := true;

  i := 0;
  while i < Length(parts) do
  begin
    case Uppercase(parts[i]) of
      'AND': operation.Kind := opkAND;
      'OR': operation.Kind := opkOR;
      'NOT':
        begin
          inc(i);
          operation.Right := not AIcon.HasKeywordPart(parts[i]);
          operation.Complete := true;
        end;
      else
        operation.Right := AIcon.HasKeywordPart(parts[i]);
        operation.Complete := true;
    end;
    if operation.Complete then
    begin
      case operation.Kind of
        opkAND: operation.Left := operation.Left and operation.Right;
        opkOR:  operation.Left := operation.Left or operation.Right;
      end;
      operation.Complete := false;
      operation.Kind := DEFAULT_OPERATION;
    end;
    inc(i);
  end;
  Result := operation.Left;
end;

{ Adds the associated icon to the unfiltered icon list. }
function TIconThumbnailViewer.AddIcon(AFileName, AKeywords: String; AStyle: TIconStyle;
  AWidth, AHeight: Integer): TIconItem;
var
  sizeStr: String;
  idx: Integer;
begin
  idx := FIconList.IndexOfFileName(AFileName);
  if idx = -1 then
  begin
    Result := TIconItem.Create(AFileName, AKeywords, AStyle, AWidth, AHeight);
    Result.FViewer := Self;
    FIconList.Add(AFileName, Result);
  end else
    Result := FIconList[idx];

  sizeStr := Format('%d x %d', [AWidth, AHeight]);
  if FSizes.IndexOf(sizeStr) = -1 then
    FSizes.Add(sizestr);
end;

procedure TIconThumbnailViewer.AddIconFolder(AFolder: String; Hidden: Boolean = false);
begin
  AFolder := AppendPathDelim(SwitchPathDelims(AFolder, true));

  if FIconFolders.IndexOf(AFolder) > -1 then   // Avoid duplicates
    DeleteIconFolder(AFolder);

  FIconFolders.AddFolder(AFolder, Hidden);
  ReadIconFolder(AFolder);

  FilterIcons;
  SelectedIndex := -1;
end;

procedure TIconThumbnailViewer.AddIconToMetaFile(AFileName: String; AHidden: Boolean);
var
  stream: TStream;
  reader: TFPCustomImageReaderClass;
  w, h: Integer;
begin
  stream := TFileStream.Create(AFileName, fmOpenRead or fmShareDenyNone);
  try
    reader := TFPCustomImage.FindReaderFromStream(stream);
    if reader <> nil then
    begin
      // Workaround for failure to get the image size when BGRABitmap is
      // installed since the bgra readers do not implement the ImageSize
      // method.
      case Lowercase(reader.ClassName) of
        'tbgrareaderpng': reader := TFPReaderPNG;
        'tbgrareaderbmp': reader := TFPReaderBMP;
      end;
     {$if FPC_FullVersion < 30301}
      // Workaround for FPReaderBMP not implementing "ImageSize"
      if reader = TFPReaderBMP then
        reader := TLazReaderBMP;
     {$ifend}

      stream.Position := 0;
      with reader.ImageSize(stream) do
      begin
        w := X;
        h := Y;
      end;
      AddIcon(AFileName, '', isAnyStyle, w, h).Hidden := AHidden;
    end;
  finally
    stream.Free;
  end;
end;

procedure TIconThumbnailViewer.Clear;
begin
  SelectedIndex := -1;
  inherited;
  FLargestIconWidth := 0;
  FLargestIconHeight := 0;
  FSizes.Clear;
  FIconList.Clear;
  FIconFolders.Clear;
end;

{ Copies the metadata from the given icon to all other icons sharing the
  same name base (= part in name before the last '_') and directory. }
procedure TIconThumbnailViewer.CopyMetadataToNameBase(AIcon: TIconItem);
var
  i: Integer;
  item: TIconItem;
  itemDir, iconDir: String;
  iconNameBase: String;
begin
  iconNameBase := AIcon.NameBase;
  iconDir := AIcon.Directory;
  for i := 0 to FIconList.Count-1 do
  begin
    item := FIconList[i];
    itemDir := item.Directory;
    if SameText(iconNameBase, item.NameBase) and SameText(iconDir, itemDir) and (item <> AIcon) then
      item.CopyMetadataFrom(AIcon);
  end;
end;

procedure TIconThumbnailViewer.DeleteIcon(AIcon: TIconItem);
var
  selIdx: Integer = -1;
  iconIdx: Integer;
begin
  if AIcon = SelectedIcon then
    selIdx := SelectedIndex;

  iconIdx := FIconList.IndexOf(AIcon);
  if iconIdx <> -1 then
  begin
    FIconList.Delete(iconIdx);
    LockFilter;
    try
      if (selIdx <> -1) then
      begin
        if selIdx >= ThumbnailCount then selIdx := ThumbnailCount-1;
        SelectedIndex := selIdx;
      end;
    finally
      UnlockFilter;
    end;
  end;
end;

procedure TIconThumbnailViewer.DeleteIconFolder(AFolder: String);
var
  i: Integer;
  folder: String;
begin
  AFolder := AppendPathDelim(SwitchPathDelims(AFolder, true));

  for i := FIconFolders.Count-1 downto 0 do
    if FIconFolders[i].FolderName = AFolder then
      FIconFolders.Delete(i);

  for i := FIconList.Count-1 downto 0 do
  begin
    folder := ExtractFilePath(FIconList[i].FileName);
    if folder = AFolder then
      FIconList.Delete(i);
  end;
end;

{$ifdef OVERLAY_ICONS}
procedure TIconThumbnailViewer.DrawThumbnail(AThumbnail: TBasicThumbnail; ARect: TRect);
var
  ovl: TGraphic;
  ppi: Integer;
  item: TIconItem;
begin
  inherited;

  item := TIconThumbnail(AThumbnail).Item;
  if (item.KeywordCount = 0) or (item.Style = isAnyStyle) then
  begin
    ppi := Font.PixelsPerInch;
    if ppi < 120 then
      ovl := FOverlayIcons[0]
    else if ppi < 168 then
      ovl := FOverlayIcons[1]
    else
      ovl := FOverlayIcons[2];
    Canvas.Draw(ARect.Left+1, ARect.Top+1, ovl);
  end;
end;
{$endif}

procedure TIconThumbnailViewer.FilterIcons;
var
  i: Integer;
  item: TIconItem;
  oldNameBase: String = '';
begin
  if FilterLocked then
    exit;

  if SelectedIcon <> nil then
    oldNameBase := SelectedIcon.NameBase;

  ThumbnailList.Clear;

  for i := 0 to FIconList.Count-1 do
  begin
    item := FIconList[i];
    if AcceptIcon(item) then
      Add(TIconThumbnail.Create(item));
  end;

  if FAutoThumbnailSize then
  begin
    if FFilterByIconSize = '' then
    begin
      FThumbnailWidth := FLargestIconWidth + ICON_MARGIN;
      FThumbnailHeight := FLargestIconHeight + ICON_MARGIN;
    end else
    begin
      FThumbnailWidth := FFilterByIconWidth + ICON_MARGIN;
      FThumbnailHeight := FFilterByIconHeight + ICON_MARGIN;
    end;
  end;

  LayoutThumbnails;

  if oldNameBase <> '' then
    for i := 0 to ThumbnailCount-1 do
      if (TIconThumbnail(Thumbnail[i]).Item.NameBase = oldNameBase) then
      begin
        SelectedIndex := i;
        exit;
      end;

  SelectedIndex := -1;

  if Assigned(FOnFilter) then
    FOnFilter(self);
end;

function TIconThumbnailViewer.FilterLocked: Boolean;
begin
  Result := FFilterLock <> 0;
end;

{ Finds the icon list entry for the specified item which has the same
  namebase, but the specified size.
  When AWidth = -1 and AHeight = -1 then the file without appendix is used. }
function TIconThumbnailViewer.FindIconSize(AIcon: TIconItem; AWidth, AHeight: Integer): TIconItem;
var
  i: Integer;
  iconNameBase: String;
  item: TIconItem;
  itemNameBase: String;
begin
  iconNameBase := AIcon.NameBase;
  for i := 0 to FIconList.Count-1 do
  begin
    item := FIconList[i];
    itemNameBase := item.NameBase;
    if SameText(itemNameBase, iconNameBase) then
    begin
      // Items with appendix
      if Length(item.Name) <> Length(itemNameBase) then
      begin
        if (item.Width = AWidth) and (item.Height = AHeight) then
        begin
          Result := item;
          exit;
        end;
      end else
      begin
        // Item without appendix
        if ((item.Width = AWidth) and (item.Height = AHeight)) or ((AWidth = -1) and (AHeight = -1)) then
        begin
          Result := item;
          exit;
        end;
      end;
    end;
  end;
  Result := nil;
end;

function TIconThumbnailViewer.FindLargestIcon(AIcon: TIconItem): TIconItem;
var
  i: Integer;
  iconNameBase: String;
  item: TIconItem;
  itemNameBase: String;
  w: Integer;
begin
  Result := nil;
  iconNameBase := AIcon.NameBase;
  w := 0;
  for i := 0 to FIconList.Count-1 do
  begin
    item := FIconList[i];
    itemNameBase := item.NameBase;
    if SameText(itemNameBase, iconNameBase) then
    begin
      if item.Width > w then  // considering only width
      begin
        w := item.Width;
        Result := item;
      end;
    end;
  end;
end;

{ Returns the number of unfiltered icons loaded. }
function TIconThumbnailViewer.GetIconCount: Integer;
begin
  Result := FIconList.Count;
end;

{ Returns a sorted list with all available icon sizes, formatted as "width x height" }
procedure TIconThumbnailViewer.GetIconSizesAsStrings(AList: TStrings);
begin
  AList.Assign(FSizes);
end;

procedure TIconThumbnailViewer.GetKeywordsAsStrings(AList: TStrings);
var
  i, j, k: Integer;
  keyword: String;
  item: TIconItem;
  list: TStringList;
begin
  list := TStringList.Create;
  try
    list.CaseSensitive := false;
    list.Sorted := true;
    for i := 0 to FIconList.Count-1 do
    begin
      item := FIconList[i];
      for j := 0 to item.KeywordCount-1 do
      begin
        keyword := item.Keywords[j];
        if not list.Find(keyword, k) then
          list.Add(keyword);
      end;
    end;
    AList.Assign(list);
  finally
    list.Free;
  end;
end;

{ OnClick handler for the menu items created by PopulateIconFoldersMenu to
  show/hide the icons of the clicked folder.
  The FIconFolders index of the folder is stored in the Tag of the menu item.
  Special tags: -1 --> show all folders, -2 --> hide all folders. }
procedure TIconThumbnailViewer.IconFolderClicked(Sender: TObject);
var
  i, idx: Integer;
begin
  if TMenuItem(Sender).Tag < 0 then
  begin
    for i := 0 to FIconFolders.Count -1 do
    begin
      case TMenuItem(Sender).Tag of
        -1: // "Show all"
            TIconFolderList(FIconFolders).Show(i);
        -2: // "Hide all"
            TIconFolderList(FIconFolders).Hide(i);
        else
            exit;
      end;
    end;
  end else
  begin
    idx := TMenuItem(Sender).Tag;
    TIconFolderList(FIconFolders).Toggle(idx);
  end;

//  LockFilter;
//  try
    UpdateIconFolders;
//  finally
//    UnlockFilter;
//  end;
end;

function TIconThumbnailViewer.IndexOfFolder(AFolder: String): Integer;
begin
  for Result := 0 to FIconFolders.Count-1 do
    if FIconFolders[Result].FolderName = AFolder then
      exit;
  Result := -1;
end;

function TIconThumbnailViewer.IndexOfThumbnail(AIcon: TIconItem): Integer;
var
  i: Integer;
begin
  Result := -1;
  if AIcon = nil then
    exit;

  for i := 0 to ThumbnailCount-1 do
    if TIconThumbnail(Thumbnail[i]).Item = AIcon then
    begin
      Result := i;
      exit;
    end;
end;

procedure TIconThumbnailViewer.LockFilter;
begin
  inc(FFilterLock);
end;

{ Returns true if at least one of the icon folders has been marked as "dirty"
  after changing its metadata.
  Called when the IconViewer is destroyed. Is evaluated for re-writing the metadata. }
function TIconThumbnailViewer.MetadataDirty: Boolean;
var
  i: Integer;
begin
  for i := 0 to FIconFolders.Count-1 do
    if FIconFolders[i].Dirty then
    begin
      Result := true;
      exit;
    end;
  Result := false;
end;

{ Populates the given menu with the names of all folders from which icons have
  been loaded. Hidden folders (having a non-nil Objects property in the
  IconFolder list) are not checked in the menu. }
procedure TIconThumbnailViewer.PopulateIconFoldersMenu(AMenu: TMenu);
var
  i: Integer;
  menuitem: TMenuItem;
begin
  AMenu.Items.Clear;

  menuItem := TMenuItem.Create(AMenu);
  menuItem.Caption := RSIconViewer_ShowAll;
  menuItem.Tag := -1;
  menuItem.OnClick := @IconFolderClicked;
  AMenu.Items.Add(menuItem);

  menuItem := TMenuItem.Create(AMenu);
  menuItem.Caption := RSIconViewer_HideAll;
  menuItem.Tag := -2;
  menuItem.OnClick := @IconFolderClicked;
  AMenu.Items.Add(menuItem);

  menuItem := TMenuItem.Create(AMenu);
  menuItem.Caption := '-';
  AMenu.Items.Add(menuItem);

  for i := 0 to FIconFolders.Count-1 do
  begin
    menuItem := TMenuItem.Create(AMenu);
    menuItem.Checked := not FIconFolders[i].Hidden;
    menuItem.Caption := FIconFolders[i].FolderName;
    menuItem.AutoCheck := true;
    menuItem.Tag := i;
    menuItem.OnClick := @IconFolderClicked;
    AMenu.Items.Add(menuItem);
  end;
end;

{ Reads the icons found in the specified folder. }
procedure TIconThumbnailViewer.ReadIconFolder(AFolder: String);
var
  isHidden: Boolean;
begin
  if AFolder = '' then
    exit;
  AFolder := AppendPathDelim(SwitchPathDelims(AFolder, true));
  isHidden := TIconFolderList(FIconFolders).IsHidden(AFolder);
  if (not DirectoryExists(AFolder)) or isHidden then
    exit;

  if FileExists(AFolder + METADATA_FILENAME) then
    ReadMetadataFile(AFolder + METADATA_FILENAME, isHidden)
  else
    ReadIcons(AFolder, isHidden);
end;

{ Reads the icons found in the folders of the given list.
  List items with a non-nil Objects property are marked as being hidden.
  Their names are stored but their icons are not displayed. }
procedure TIconThumbnailViewer.ReadIconFolders(AList: TStrings);
var
  i: Integer;
  selectedIconFileName: String = '';
  folder: String;
  isHidden: Boolean;
begin
  if SelectedIcon <> nil then
    selectedIconFileName := SelectedIcon.FileName;
  SelectedIndex := -1;  // this sets FSelectedIcon to nil.
  FIconFolders.Clear;
  FIconList.Clear;
  for i := 0 to AList.Count-1 do
  begin
    folder := AList[i];
    isHidden := AList.Objects[i] <> nil;
    if FIconFolders.IndexOf(folder) > -1 then    // Avoid duplicates
      DeleteIconFolder(folder);
    TIconFolderList(FIconFolders).AddFolder(folder, isHidden);
    ReadIconFolder(AList[i]);
  end;

  if not FilterLocked then
  begin
    FilterIcons;
    SelectIconInFile(selectedIconFileName);
  end;
end;

{ Looks for image files (*.png, *.bmp) in the given folder and adds them to
  the viewer.
  When AHidden is true all icons are marked as hidden, i.e. are not displayed. }
procedure TIconThumbnailViewer.ReadIcons(AFolder: String; AHidden: Boolean);
var
  files: TStrings;
  i: Integer;
begin
  files := TStringList.Create;
  try
    FindAllFiles(files, AFolder, IMAGES_MASK, false);
    for i := 0 to files.Count-1 do
      AddIconToMetaFile(files[i], AHidden);
  finally
    files.Free;
  end;
end;

procedure TIconThumbnailViewer.ReadMetadataFile(AFileName: String;
  AHidden: Boolean);
var
  doc: TXMLDocument = nil;
  iconsNode, iconNode: TDOMNode;
  keywordsNode, keywordNode: TDOMNode;
  folder, fn: String;
  i, idx: Integer;
  w, h: Integer;
  style: TIconStyle;
  s: String;
  keywords: String;
  files: TStringList = nil;
  {$ifdef SPEED_TIMER}
  t1, t2: TDateTime;
  {$endif}
begin
  {$ifdef SPEED_TIMER}
  t1 := Now();
  {$endif}
  folder := ExtractFilePath(AFileName);
  files := TStringList.Create;
  try
    files.Sorted := true;
    FindAllFiles(files, folder, IMAGES_MASK, false);
    ReadXMLFile(doc, AFileName);
    iconsNode := doc.DocumentElement.FindNode('icons');
    iconNode := iconsNode.FindNode('icon');
    {$ifdef SPEED_TIMER}
    t2 := Now();
    {$endif}
    while iconNode <> nil do begin
      fn := '';
      style := isAnystyle;
      if iconNode.HasAttributes then
        for i := 0 to iconNode.Attributes.Length-1 do
        begin
          s := iconNode.Attributes[i].NodeValue;
          case iconNode.Attributes[i].NodeName of
            'filename':
              begin
                fn := folder + s;
                // Check existence of icon file fn
                if not files.Find(fn, idx) then   // faster than FileExists(fn)
                begin
                  fn := '';
                  break;
                end;
              end;
            'width':
              w := StrToIntDef(s, 0);
            'height':
              h := StrToIntDef(s, 0);
            'style':
              style := StrToIconStyle(s);
          end;
        end;

      // ignore metadata entries for which the icon files do not exist.
      if fn <> '' then
      begin
        keywords := '';
        keywordsNode := iconNode.FindNode('keywords');
        if keywordsNode <> nil then
        begin
          keywordNode := keywordsNode.FindNode('keyword');
          while keywordNode <> nil do
          begin
            s := keywordNode.TextContent;
            keywords := keywords + ';' + s;
            keywordNode := keywordNode.NextSibling;
          end;
        end;
        if keywords <> '' then
          System.Delete(keywords, 1, 1);

        AddIcon(fn, keywords, style, w, h).Hidden := AHidden;

        // Delete the processed filename from the files list
        if files.Find(fn, i) then
          files.Delete(i);
      end;

      iconNode := iconNode.NextSibling;
    end;

    // Every image which exists in the metadata file has been deleted from
    // the files list. The entries which are left identify new files. Add them
    // to the metafile.
    for i := 0 to files.Count-1 do
      AddIconToMetafile(files[i], AHidden);

    {$ifdef SPEED_TIMER}
    t2 := Now()-t2;
    {$endif}

  finally
    doc.Free;
    files.Free;
  end;

  {$ifdef SPEED_TIMER}
  t1 := Now() - t1;
  DebugLn(['[TIconThumbnailViewer.ReadMetadataFile] Total time=', FormatDateTime('s.zzz "sec"', t1), ', Loop time=', FormatdateTime('s.zzz "sec"', t2)]);
  {$endif}

end;

procedure TIconThumbnailViewer.SetFilterByIconKeywords(AValue: String);
begin
  if FFilterByIconKeywords <> AValue then
  begin
    FFilterByIconKeywords := AValue;
    if not FilterLocked then
    begin
      FilterIcons;
      Invalidate;
    end;
  end;
end;

procedure TIconThumbnailViewer.SetFilterByIconSize(AValue: string);
var
  sa: TStringArray;
begin
  if FFilterByIconSize <> AValue then
  begin
    FFilterByIconSize := AValue;
    if AValue = '' then
    begin
      FFilterByIconWidth := -1;
      FFilterByIconHeight := -1;
    end else
    begin
      sa := AValue.Split('x');
      FFilterByIconWidth := StrToInt(Trim(sa[0]));
      FFilterByIconHeight := StrToInt(Trim(sa[1]));
    end;
    if not FilterLocked then
    begin
      FilterIcons;
      Invalidate;
    end;
  end;
end;

procedure TIconThumbnailViewer.SetFilterByIconStyle(AValue: TIconStyle);
begin
  if FFilterByIconStyle <> AValue then
  begin
    FFilterByIconStyle := AValue;
    if not FilterLocked then
    begin
      FilterIcons;
      Invalidate;
    end;
  end;
end;

{ Selects, among all visible thumbnails, the icon which has the given filename.}
function TIconThumbnailViewer.SelectIconInFile(AFileName: String): Boolean;
var
  i, idx: Integer;
  folder: String;
begin
  Result := false;
  if AFileName = '' then
  begin
    SelectedIndex := -1;
    exit;
  end;

  // Make sure that folder of file is not hidden
  folder := ExtractFilePath(AFileName);
  if TIconFolderList(FIconFolders).IsHidden(folder) then
  begin
    SelectedIndex := -1;
    exit;
  end;

  // Find the index of the icon with the given filename among all thumbnails.
  idx := -1;
  for i := 0 to ThumbnailCount-1 do
  begin
    if (Thumbnail[i] = nil) or (TIconThumbnail(Thumbnail[i]).Item = nil) then
      Continue;
    if TIconThumbnail(Thumbnail[i]).Item.FileName = AFileName then
    begin
      Result := true;
      idx := i;
      break;
    end;
  end;

  SelectedIndex := idx;
end;

procedure TIconThumbnailViewer.SetSelectedIndex(AValue: Integer);
var
  thumb: TIconThumbnail;
begin
  if AValue = SelectedIndex then
    exit;

  if (AValue > -1) and (AValue < ThumbnailCount) then
  begin
    thumb := Thumbnail[AValue] as TIconThumbnail;
    FSelectedIcon := thumb.Item;
  end else
    FSelectedIcon := nil;

  inherited;
end;

{ This is for emphasizing icons in the viewer which do not yet have keywords
  or have an unspecified style. }
function TIconThumbnailViewer.ThumbnailMarked(AThumbnail: TBasicThumbnail): Boolean;
var
  item: TIconItem;
begin
  item := TIconThumbnail(AThumbnail).Item;
  Result := (item.KeywordCount = 0) or (item.Style = isAnyStyle);
end;

{ The specified thumbnail is outside the drawing area. In this implementation
  of the icon viewer we release the associated picture to avoid running out
  of memory, in particular in the 32-bit IDE. }
procedure TIconThumbnailViewer.ThumbnailOutside(AThumbnail: TBasicThumbnail);
var
  item: TIconItem;
begin
  item := TIconThumbnail(AThumbnail).Item;
  if item <> nil then item.ReleasePicture;
end;

procedure TIconThumbnailViewer.UnlockFilter;
begin
  dec(FFilterLock);
  if FFilterLock = 0 then
  begin
    FilterIcons;
    Invalidate;
  end;
end;

{ Folders and all their icons can be hidden by setting the Hidden flag of the
  folder record to true.
  This procedure iterates over all icons and sets their Hidden flag when
  their folder is hidden. }
procedure TIconThumbnailViewer.UpdateIconFolders;
var
  i, j: Integer;
  hiddenFolders: TStringList;
  folder: String;
  item: TIconItem;
begin
  hiddenFolders := TStringList.Create;
  try
    // Collect all hidden folders...
    hiddenFolders.Sorted := true;
    for i := 0 to FIconFolders.count-1 do
    begin
      folder := FIconFolders[i].FolderName;
      if FIconFolders[i].Hidden then
        hiddenFolders.Add(AppendPathDelim(folder));
    end;

    // ... find the icons in the hidden folders and set their Hidden flag.
    for i := 0 to FIconList.Count-1 do
    begin
      item := FIconList[i];
      folder := item.Directory;
      item.Hidden := hiddenfolders.Find(folder, j);
    end;

    FilterIcons;
    Invalidate;
  finally
    hiddenFolders.Free;
  end;
end;

{ Copies the names of the stored icon folders to the given list. Hidden folders
  are marked by putting a non-nil value in the Objects of the output list. }
procedure TIconThumbnailViewer.WriteIconFolders(AList: TStrings);
var
  i: Integer;
  folder: String;
  isHidden: Boolean;
begin
  for i := 0 to FIconFolders.Count-1 do
  begin
    folder := FIconFolders[i].FolderName;
    isHidden := TIconFolderList(FIconFolders).IsHidden(i);
    if isHidden then
      AList.AddObject(folder, TObject(PtrUInt(1)))
    else
      AList.Add(folder);
  end;
end;

procedure TIconThumbnailViewer.WriteMetadataFiles;
var
  folder, filename: String;
  doc: TXMLDocument;
  root, iconsNode, iconNode, keywordsNode, keywordNode: TDOMNode;
  i, j, k: Integer;
  item: TIconItem;
begin
  Screen.Cursor := crHourglass;
  try
    Application.ProcessMessages;
    for i := 0 to FIconFolders.Count-1 do
    begin
      folder := AppendPathDelim(FIconFolders[i].FolderName);

      // Write only unmodified metadata
      if not FIconFolders[i].Dirty then
        Continue;

      doc := TXMLDocument.Create;
      try
        root := doc.CreateElement('metadata');
        doc.AppendChild(root);
        iconsNode := doc.CreateElement('icons');
        root.AppendChild(iconsNode);
        for j := 0 to FIconList.Count-1 do
        begin
          item := FIconList[j];
          filename := ExtractFileName(item.FileName);
          if ExtractFilePath(item.FileName) = folder then
          begin
            iconNode := doc.CreateElement('icon');
            iconsNode.AppendChild(iconNode);
            TDOMElement(iconNode).SetAttribute('filename', filename);
            TDOMElement(iconNode).SetAttribute('width', IntToStr(item.Width));
            TDOMElement(iconNode).SetAttribute('height', IntToStr(item.Height));
            if item.Style <> isAnyStyle then
              TDOMElement(iconNode).SetAttribute('style', item.StyleAsString);
            if item.KeywordCount > 0 then
            begin
              keywordsNode := doc.CreateElement('keywords');
              iconNode.AppendChild(keywordsNode);
              for k := 0 to item.KeywordCount-1 do
              begin
                keywordNode := doc.CreateElement('keyword');
                keywordsNode.AppendChild(keywordNode);
                keywordNode.AppendChild(doc.CreateTextNode(item.Keywords[k]));
              end;
            end;
          end;
        end;
        if FileExists(folder + METADATA_FILENAME) then
          RenameFile(folder + METADATA_FILENAME, folder + METADATA_FILENAME + '.bak');

        WriteXMLFile(doc, folder + METADATA_FILENAME);
        FIconFolders[i].Dirty := false;
      finally
        doc.Free;
      end;
    end;
  finally
    Screen.Cursor := crDefault;
  end;
end;

end.

