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 + ''; //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 + '' + FileList[i] + ''; WOutString := WOutString + ''; 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.