unit RecentFiles;

{
[RecentFiles] [1.0]
Delphi 2005
December 2005

LICENSE

The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
"http://www.mozilla.org/MPL/"

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
the specific language governing rights and limitations under the License.

The Original Code is "[RecentFiles.pas]".

The Initial Developer of the Original Code is Martin Holmes (Victoria,
BC, Canada, "http://www.mholmes.com/"). Copyright (C) 2005 Martin Holmes 
and the University of Victoria Computing and Media Centre. The code was 
co-developed for university and personal projects, and rights are shared
by Martin Holmes and the University of Victoria. All Rights Reserved.
}

{
Written by Martin Holmes, September 2005, using Delpi 2005, and only tested with
 Delphi 2005.

This is a general-purpose tool which is designed to keep track of recently-saved and
recently-opened files, and represent them on a submenu on an associated form.

The list of recent files is saved in a TWideStringList, and saved and loaded from disk
in the ApplicationData folder. The storage format is XML.

Some of the code in this class is inspired by TMRUFiles, by Kambiz R. Khojasteh, at
The Delphi Area (http://www.delphiarea.com/). In particular, I learned how to attach
a click event to a dynamically-created component from TMRUFiles.

HOW TO USE THIS CLASS:

In any form, create a private field:

  FRecentFiles: TRecentFiles;

Create a procedure in the form to handle the opening of files, based on this template:

  procedure OpenRecent(Sender: TObject; const FileName: WideString);

Create a TTntMenuItem whose submenu will display the recent files.

In the FormShow event, do this:

  FRecentFiles := TRecentFiles.Create(Self, [The_TntMenuItem], OpenRecent);

In the FormClose event, do this:

  FreeAndNil(FRecentFiles);

At the end of every save and load routine in your form's code, add this:

  FRecentFiles.AddNewFile(TheFileName);

OTHER USEFUL PROPERTIES:

FilesToDisplay is an integer which governs how many items appear on the menu.

FilesToStore is an integer which governs how many items are stored in the disk file.

LastFile is a read-only property containing the last used file in the list (if
there is one). You might use this, for example, to reload the last-used file
on startup of the application.

}

interface

uses Classes, SysUtils, Windows, Forms, WideStrings, VersionInfo, TntMenus, GenFunctions, TntForms,
     XMLGlobals, XMLRoutines, jclUnicode, TntSysUtils;

type
  TRFClickEvent = procedure(Sender: TObject; const FileName: WideString) of object;

  TRecentFiles = class
  private
    FOwnerForm: TTntForm;
    FVersionInfo: TAppVersionInfo;
    FProductID: WideString;
    FDirPath: WideString;
    FFilePath: WideString;
    FFilesToDisplay: integer;
    FFilesToStore: integer;
    FLinkedMenu: TTntMenuItem;
    FOnClick: TRFClickEvent;
    FileList: TWideStringList;
    function ReadListFromFile: Boolean;
    function WriteListToFile: Boolean;
    procedure MenuItemClicked(Sender: TObject);
    function GetLastFile: WideString;
  protected
    procedure DoClick(Index: integer); virtual;

  public

    constructor Create(OwnerForm: TTntForm; LinkedMenu: TTntMenuItem;
                      ClickEvent: TRFClickEvent); overload;
    constructor Create(OwnerForm: TTntForm; LinkedMenu: TTntMenuItem;
              ClickEvent: TRFClickEvent; SpecialFileName: WideString); overload;
    destructor Destroy; override;
    function AddNewFile(NewFile: WideString): Boolean;
    function RebuildMenu: Boolean;
  published
    property FilesToDisplay: integer read FFilesToDisplay write FFilesToDisplay default 8;
    property FilesToStore: integer read FFilesToStore write FFilesToStore default 16;
    property OnClick: TRFClickEvent read FOnClick write FOnClick;
    property LastFile: WideString read GetLastFile;
  end;

implementation

{ TRecentFiles }

function TRecentFiles.AddNewFile(NewFile: WideString): Boolean;
var
FoundAt: integer;

begin
  Result := False;//Default
  try
    FoundAt := FileList.IndexOf(NewFile);
    if FoundAt < 0 then
      FileList.Insert(0, NewFile)
    else
      FileList.Move(FoundAt, 0);
    RebuildMenu;
    Result := True;
  except
    //False return is enough for now.
  end;
end;

constructor TRecentFiles.Create(OwnerForm: TTntForm; LinkedMenu: TTntMenuItem;
                                ClickEvent: TRFClickEvent);
var
i: integer;
wsAppDir: WideString;
PortableMode: Boolean;

begin
  inherited Create;
  FOwnerForm := OwnerForm;
  FileList := TWideStringList.Create;
  FLinkedMenu := LinkedMenu;
  OnClick := ClickEvent;
//Build the application data path for storing the info
  FVersionInfo := TAppVersionInfo.Create;
  FProductID := FVersionInfo.ProductName + ' v.' + IntToStr(FVersionInfo.V1) {+ '.' +
                IntToStr(FVersionInfo.V2)};
  {We need to detect if this application is being run in "portable" mode, of in a
regular install. In portable mode, the state info is stored in a directory
parallel with the one in which the app is running. If it's in normal mode,
we use the regular AppData directory.  }
  PortableMode := False;
  if ParamCount > 0 then
    for i := 0 to ParamCount do
      if ParamStr(i) = '-portable' then
        PortableMode := True;

  if (PortableMode = True) then
    begin

      FDirPath := WideExpandFileName(Application.ExeName + '\..\..\..\Data');
      if not (WideDirectoryExists(FDirPath)) then
        begin
          try
            WideForceDirectories(FDirPath);
          except
          //Just silently fail
          end;
        end;
      if not(WideDirectoryExists(FDirPath)) then
        FDirPath := AppDataFolder + '\' + FVersionInfo.CompanyName + '\' +
               FProductID;;
    end
  else
    begin
      FDirPath := AppDataFolder + '\' + FVersionInfo.CompanyName + '\' +
               FProductID;
    end;
  if not (WideDirectoryExists(FDirPath)) then
    WideForceDirectories(FDirPath);


  FFilePath := FDirPath + '\' + FOwnerForm.Name + '_recent_files.xml';
  FilesToDisplay := 8; //default
  FilesToStore := 16; //default
  ReadListFromFile;
  RebuildMenu;
end;

constructor TRecentFiles.Create(OwnerForm: TTntForm; LinkedMenu: TTntMenuItem;
                                ClickEvent: TRFClickEvent; SpecialFileName: WideString);
begin
  inherited Create;
  FOwnerForm := OwnerForm;
  FileList := TWideStringList.Create;
  FLinkedMenu := LinkedMenu;
  OnClick := ClickEvent;
//Build the application data path for storing the info
  FVersionInfo := TAppVersionInfo.Create;
  FProductID := FVersionInfo.ProductName + ' v.' + IntToStr(FVersionInfo.V1) {+ '.' +
                IntToStr(FVersionInfo.V2)};
  FDirPath := AppDataFolder + '\' + FVersionInfo.CompanyName + '\' +
               FProductID;
  if not (DirectoryExists(FDirPath)) then
    ForceDirectories(FDirPath);

  FFilePath := FDirPath + '\' + SpecialFileName;
  FilesToDisplay := 8; //default
  FilesToStore := 16; //default
  ReadListFromFile;
  RebuildMenu;
end;

destructor TRecentFiles.Destroy;
begin
  WriteListToFile;
  FreeAndNil(FVersionInfo);
  FreeAndNil(FileList);
  inherited Destroy;
end;

procedure TRecentFiles.DoClick(Index: integer);
begin
  if Assigned(FOnClick) then
    FOnClick(Self, FileList[Index]);
end;

function TRecentFiles.GetLastFile: WideString;
begin
  if FileList.Count > 0 then
    Result := FileList[0]
  else
    Result := '';
end;

procedure TRecentFiles.MenuItemClicked(Sender: TObject);
begin
  if Sender is TTntMenuItem then
    DoClick(TTntMenuItem(Sender).Tag);
end;

function TRecentFiles.ReadListFromFile: Boolean;
var
InStream: TFileStream;
InString: string;
WInString: WideString;
FilesXML: WideString;
FileItem: WideString;
FoundItem: Boolean;

begin
  Result := False; //default
  try
    FileList.Clear;
//get the data from the file
    if not FileExists(FFilePath) then
      Exit;
    InStream := TFileStream.Create(FFilePath, fmOpenRead);
    try
      if InStream.Size > 0 then
        begin
          SetLength(InString, InStream.Size);
          InStream.ReadBuffer(InString[1], InStream.Size);
          WInString := UTF8ToWideString(InString);
//Read the files from the XML data
          WReadElement(False, 'recent-files', WInString, FilesXML);
          FoundItem := WReadElement(True, 'file', FilesXML, FileItem);
          while FoundItem = True do
            begin
              if FileExists(FileItem) then
                FileList.Add(FileItem);
              FoundItem := WReadElement(True, 'file', FilesXML, FileItem);
            end;
        end;
    finally
      InStream.Free;
    end;

    Result := True;
  except
    //False return is sufficient for now
  end;
end;

function TRecentFiles.RebuildMenu: Boolean;
var
i: integer;
TotalToShow: integer;
NewMenuItem: TTntMenuItem;

begin
  Result := False;  //Default
  try
    if FLinkedMenu = nil then
      Exit;
    FLinkedMenu.Clear;
    TotalToShow := FilesToDisplay;
    if FileList.Count < FilesToDisplay then
      TotalToShow := FileList.Count;

    for i := 0 to TotalToShow-1 do
      begin
        NewMenuItem := TTntMenuItem.Create(FOwnerForm);
        NewMenuItem.Caption := ExtractFileName(FileList[i]);
        NewMenuItem.Tag := i;
        NewMenuItem.OnClick := MenuItemClicked;
        FLinkedMenu.Add(NewMenuItem);
      end;
    Result := True;
  except
    //False return is enough for now
  end;
end;

function TRecentFiles.WriteListToFile: Boolean;
var
i: integer;
WOutString: WideString;
OutString: string;
OutStream: TFileStream;

begin
  Result := False; //default
  try
//write the data to the file
    OutStream := TFileStream.Create(FFilePath, fmCreate);
    try
      WOutString := WUTF8Header + #13#10 + '<recent-files>';
//Delete extras we don't need to store
      while FileList.Count > FilesToStore do
        FileList.Delete(FileList.Count-1);
      if FileList.Count > 0 then
        for i := 0 to FileList.Count - 1 do
          WOutString := WOutString + '<file>' + FileList[i] + '</file>';
      WOutString := WOutString + '</recent-files>';
      OutString := WideStringToUTF8(WOutString);
      OutStream.WriteBuffer(OutString[1], Length(OutString));
      Result := True;
    finally
      OutStream.Free;
    end;
  except
    //False return is sufficient for now
  end;
end;

end.
