Mega Code Archive

 
Categories / Delphi / Examples
 

How to create a desktop search tool part 1

Both the source and exe are included. It takes a few mins to scan your drive, but file searching is blazing. It uses a few linked lists to store the index, but the footprint is still pretty small. It only holds the index in memory, however, so you have to wait for the re-scan if you close it by accident. Disclaimer: This program is sub-standard, and due to quality/security/usability issues, should not be used by anyone. Avert your eyes, children. It may assume another form. { Issues: -------- - Sorting starts to get kind of slow with > 3000 items - Consider implementing the max/min bubble-sort enhancement - More internal docs needed Wish-List: ---------- - A good book, a glass of scotch and a warm breeze to fill my sails History: ------------ Oct 29/04 - Began development Nov 05/04 - Changed the Matches list box to a grid for ease of reading - Added the Size and Last Modified columns to the Matches grid - Added the asterisk wildcard to the search box - Consolidated the folder list to reduce the memory footprint Nov 09/04 - Implemented the Statistics page - Implemented the File Sizes statistics page Nov 10/04 - Implemented the Folders statistics page - Implemented the Modifications statistics page - Enhanced the CompareToSearchPhrase function so it handles >1 wildcard Nov 11/04 - Fixed a bug in the CompareToSearchPhrase function - Fixed several bugs relating to statistical graphing - Added a check to keep folders from being added if their parent already includes them Nov 12/04 - Added the "By Size" and "By Modified Time" search range options - Removed the "Last Modified File" display from the Modifications statistics page Nov 17/04 - Implemented a prototype of a TreeView-based directory selection page - Implemented auto-drive discovery for the TreeView - Implemented structure discovery for the selected drives of the TreeView Nov 19/04 - Implemented file discovery for the tree view - Remove all the list-based learning controls & methods - Implemented the Match/Doesn't Match checkbox for search by filename Nov 22/04 - Fixed a bug that kept files from being learned if the the folders weren't discovered first - Moved the scales to the left side of graphs, keeping the values on the right side Nov 24/04 - Added single-char wildcard searches (and made searching much cleaner) with donated code from Ritchie Annand. Nov 26/04 - Minor esthetic adjustment to labels on the Statistics pages. - Move the amounts to the left side of the Folder Statistics lists. - Began working on the click-results-column-to-sort functionality Nov 29/04 - Got bubble sort working (Ascending only) for the results grid. Nov 30/04 - Got the Ascending/Descending toggle to work on the search result sort - Allowed the selected item to be maintained during sorting } unit Unit1; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, FileCtrl, Grids, ExtCtrls, ComCtrls; {Information on each folder is stored in one of these records.} {There are stored in a separate linked-list and pointed to by the Folder attrib of TFilePtr. This saves the space used by storing the folder info which each file.} type TFolderPtr = ^TFolderElem; TFolderElem = record Folder: string; UCFolder: string; {Upper-case folder name. Increases sorting efficiency.} FileCount: Integer; {used on the Statistics pages.} TotalFileSize: Int64; {used on the Statistics pages.} Next: TFolderPtr; end; {Information on each file is stored in one of these records.} type TFilePtr = ^TFileElem; TFileElem = record FileName: string; UCFileName: string; {Upper-case file name. Increases sorting efficiency.} Folder: TFolderPtr; Size: Integer; LastModified: TDateTime; SearchMatch: Boolean; {...True, if this item matches the search criteria.} Next: TFilePtr; {...points to the next item, in natural order.} NextMatch: TFilePtr; {...points to the next item matching the search criteria. These links are used in the sorting routines.} end; type TfrmHereMain = class(TForm) pgcMain: TPageControl; tsLocate: TTabSheet; pnlLocateControls: TPanel; Label1: TLabel; edtFileName: TEdit; btnCopyFullPath: TButton; edtSelectedMatch: TEdit; lblMatchCount: TLabel; sgMatches: TStringGrid; tsStatistics: TTabSheet; pgcStatistics: TPageControl; tsFileSizes: TTabSheet; tsFolders: TTabSheet; tsModifications: TTabSheet; Panel2: TPanel; Label4: TLabel; lblLargestFileName: TLabel; Label6: TLabel; lblLargestFileSize: TLabel; Label8: TLabel; Label5: TLabel; lblLargestFileFolder: TLabel; lblAverageFileSize: TLabel; Label7: TLabel; lblTotalFiles: TLabel; pbxFileSizes: TPaintBox; Panel4: TPanel; Label9: TLabel; lblFoldersByFileCount: TLabel; Panel5: TPanel; lblFoldersByFileSize: TLabel; lbxFoldersByFileCount: TListBox; lbxFoldersByFileSize: TListBox; lblFolderCount: TLabel; Splitter1: TSplitter; pbxFileMods: TPaintBox; ckbSearchByFileName: TCheckBox; ckbSearchBySize: TCheckBox; ckbSearchByModTime: TCheckBox; edtSizeAmount: TEdit; edtModAmount: TEdit; cbxModUnits: TComboBox; btnSearch: TButton; cbxSizeUnits: TComboBox; edtModAmount2: TEdit; cbxModUnits2: TComboBox; ckbSearchByModTimeTo: TCheckBox; edtSizeAmount2: TEdit; cbxSizeUnits2: TComboBox; Label3: TLabel; tsLearn: TTabSheet; Panel1: TPanel; TreeView1: TTreeView; btnDiscoverFolders: TButton; btnLearnFilesNow: TButton; btnWhyDiscoverFolders: TButton; cbxIncludeParent: TCheckBox; pnlDiscovering: TPanel; Panel3: TPanel; Label2: TLabel; cbxFileNameMatchType: TComboBox; procedure btnCopyFullPathClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure FormResize(Sender: TObject); procedure FormShow(Sender: TObject); procedure sgMatchesSelectCell(Sender: TObject; ACol, ARow: Integer; var CanSelect: Boolean); procedure sgMatchesDrawCell(Sender: TObject; ACol, ARow: Integer; Rect: TRect; State: TGridDrawState); procedure tsFileSizesShow(Sender: TObject); procedure pbxFileSizesPaint(Sender: TObject); procedure tsFoldersShow(Sender: TObject); procedure tsModificationsShow(Sender: TObject); procedure pbxFileModsPaint(Sender: TObject); procedure edtSizeAmountKeyPress(Sender: TObject; var Key: Char); procedure btnSearchClick(Sender: TObject); procedure edtSizeAmountExit(Sender: TObject); procedure ckbSearchByModTimeClick(Sender: TObject); procedure edtSizeAmount2Exit(Sender: TObject); procedure TreeView1CustomDrawItem(Sender: TCustomTreeView; Node: TTreeNode; State: TCustomDrawState; var DefaultDraw: Boolean); procedure TreeView1Addition(Sender: TObject; Node: TTreeNode); procedure TreeView1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure TreeView1KeyPress(Sender: TObject; var Key: Char); procedure TreeView1Collapsing(Sender: TObject; Node: TTreeNode; var AllowCollapse: Boolean); procedure TreeView1Expanding(Sender: TObject; Node: TTreeNode; var AllowExpansion: Boolean); procedure btnDiscoverFoldersClick(Sender: TObject); procedure btnWhyDiscoverFoldersClick(Sender: TObject); procedure btnLearnFilesNowClick(Sender: TObject); procedure sgMatchesMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); private bResultSortAscending, bExpanding, bCollapsing, bFilesHaveBeenLearned, bCustomDraw: Boolean; sLargestFileName: string; iKnownFolderCount, iKnownFileCount, iResultsSortByCol, iAverageFileSizeCount, iLargestFileSize, iFolderCount: Integer; iMatchCount, iAverageFileSizeTotal, iSizeFloor, iSizeCeiling: Int64; dModTimeFloor, dModTimeCeiling: Double; pKnownFiles, pMatches: TFilePtr; pKnownFolders: TFolderPtr; aryFileSizes, aryModDaysAgo: array[0..12] of Integer; aryFoldersByFileCount, aryFoldersByFileSize: array[0..99] of TFolderPtr; oChecked, oPartiallyChecked: TObject; function AllDescendantsAreChecked(aParent: TTreeNode): Boolean; procedure BuildDriveList(var aList: TStringList); procedure BuildRecursiveFileList(aFolder: string; aSearchSubFolders: Boolean); procedure CheckTopFoldersByFileCount(var aFolder: TFolderPtr); procedure CheckTopFoldersByFileSize(var aFolder: TFolderPtr); procedure ClearAllListsAndCounts; procedure ClearFileList(var aList: TFilePtr); procedure ClearFolderList(var aList: TFolderPtr); procedure ClearResultsGrid; procedure ClearSearchMatches(var aList: TFilePtr); procedure DiscoverRootNodeFolders; procedure DisplaySearchResults; procedure DrawFileModChart; procedure DrawFileSizeChart; procedure FolderToTreeNodes(aFolder: string; aParentNode: TTreeNode); procedure GatherStatistics; function GetNodePath(var aNode: TTreeNode): string; procedure HandleNodeClick(aNode: TTreeNode); procedure InformChildNodes(aNode: TTreeNode); procedure InformParentNode(aParent: TTreeNode; aChecked: Boolean); procedure LearnFilesInFolder(aFolder: string); procedure LearnFilesInNode(var aNode: TTreeNode); procedure LinkMatches; procedure LoadDriveNodes; procedure PerformLocate; procedure PerformSort(aColumn: Integer; aAscending: Boolean); procedure SelectRowByObject(var aSelected: TFilePtr); procedure SetGridColWidths; procedure SetLearnButtonAccessability; procedure SortResults(aColumn: Integer); public end; var frmHereMain: TfrmHereMain; function Matches(const ASource, APattern: string; ACaseSensitive: Boolean=False): Boolean; function _StrComp(var aStr1, aStr2: string): Integer; implementation {$R *.dfm} procedure TfrmHereMain.PerformLocate; var pElem: TFilePtr; sSearchPhrase: string; bMeetsCriteria: Boolean; dtNow: TDateTime; begin sgMatches.Visible := False; ClearResultsGrid; dtNow := Now; sSearchPhrase := UpperCase(Trim(edtFileName.Text)); btnSearch.Enabled := False; btnCopyFullPath.Enabled := False; lblMatchCount.Caption := 'searching...'; lblMatchCount.Refresh; ClearSearchMatches(pKnownFiles); pElem := pKnownFiles; iMatchCount := 0; while pElem <> nil do begin bMeetsCriteria := True; if ckbSearchBySize.Checked then bMeetsCriteria := (pElem^.Size >= iSizeFloor) and (pElem^.Size <= iSizeCeiling); if bMeetsCriteria and ckbSearchByModTime.Checked then begin bMeetsCriteria := ((dtNow - dModTimeCeiling) <= pElem^.LastModified); if bMeetsCriteria and ckbSearchByModTimeTo.Checked then bMeetsCriteria := ((dtNow - dModTimeFloor) >= pElem^.LastModified); end; if bMeetsCriteria and ckbSearchByFileName.Checked then bMeetsCriteria := Matches(pElem^.UCFileName, sSearchPhrase, false) xor (cbxFileNameMatchType.ItemIndex = 1); if bMeetsCriteria then begin pElem^.SearchMatch := True; inc(iMatchCount); end; pElem := pElem^.Next; end; LinkMatches; DisplaySearchResults; sgMatches.Row := 1; edtSelectedMatch.Text := sgMatches.Cells[1, sgMatches.Row]+sgMatches.Cells[0, sgMatches.Row]; btnCopyFullPath.Enabled := (iMatchCount > 0); btnSearch.Enabled := True; sgMatches.Visible := True; end; procedure TfrmHereMain.LinkMatches; var pLastMatch, pSearcher: TFilePtr; bFirstMatch: Boolean; begin {This proc connects the search results via their NextMatch attrib.} bFirstMatch := True; pSearcher := pKnownFiles; while pSearcher <> nil do begin if pSearcher^.SearchMatch then begin if bFirstMatch then begin pMatches := pSearcher; bFirstMatch := False; end; if pLastMatch <> nil then pLastMatch^.NextMatch := pSearcher; pLastMatch := pSearcher; end; pSearcher := pSearcher^.Next; end; end; procedure TfrmHereMain.ClearSearchMatches(var aList: TFilePtr); var pElem: TFilePtr; begin {This proc undoes any evidence that any items ever matched the search criteria.} pElem := aList; while pElem <> nil do begin pElem^.SearchMatch := False; pElem^.NextMatch := nil; pElem := pElem^.Next; end; end; procedure TfrmHereMain.ClearResultsGrid; begin lblMatchCount.Caption := ''; with sgMatches do begin while RowCount > 2 do begin Objects[0, RowCount-1] := nil; Rows[RowCount-1].Clear; RowCount := RowCount - 1; end; Rows[1].Clear; end; end; procedure TfrmHereMain.DisplaySearchResults; var iRow: Integer; pElem: TFilePtr; sFileSize: string; begin ClearResultsGrid; pElem := pMatches; while pElem <> nil do with sgMatches do begin if pElem^.SearchMatch then begin if Cells[0,1] <> '' then RowCount := RowCount + 1; iRow := RowCount - 1; Cells[0, iRow] := pElem^.FileName; Cells[1, iRow] := pElem^.Folder^.Folder; sFileSize := FormatFloat('###,###,###,##0', Round(pElem^.Size/1024))+' KB'; Cells[2, iRow] := sFileSize; Cells[3, iRow] := FormatDateTime('MM/DD/YYYY HH:NN', pElem^.LastModified); Objects[0, iRow] := TObject(pElem); end; pElem := pElem^.NextMatch; end; lblMatchCount.Caption := FormatFloat('###,###,##0', iMatchCount) + ' matches'; end; function Matches(const ASource, APattern: string; ACaseSensitive: Boolean=False): Boolean; {This function was donated by Ritchie Annand.} function MatchPattern(ASourcePart, APatternPart: PChar): Boolean; begin if StrComp(APatternPart,'*')=0 then Result := True // * matches everything else if ASourcePart^=#0 then // end of the string Result := APatternPart^=#0 // is there still pattern remaining? else case APatternPart^ of '*' : if MatchPattern(ASourcePart,APatternPart+1) then Result := True else Result := MatchPattern(ASourcePart+1,APatternPart); '?' : Result := MatchPattern(ASourcePart+1,APatternPart+1); else if ACaseSensitive then if ASourcePart^=APatternPart^ then Result := MatchPattern(ASourcePart+1,APatternPart+1) else Result := False else if Upcase(ASourcePart^)=Upcase(APatternPart^) then Result := MatchPattern(ASourcePart+1,APatternPart+1) else Result := False; end; end; begin Result := MatchPattern(PChar(ASource),PChar(APattern)); end; procedure TfrmHereMain.btnCopyFullPathClick(Sender: TObject); begin edtSelectedMatch.SelectAll; edtSelectedMatch.CopyToClipboard; end; procedure TfrmHereMain.FormCreate(Sender: TObject); begin bFilesHaveBeenLearned := False; pKnownFiles := nil; pKnownFolders := nil; pKnownFolders := nil; bCustomDraw := True; oChecked := TObject.Create; oPartiallyChecked := TObject.Create; bExpanding := False; bCollapsing := False; iKnownFolderCount := 0; end; procedure TfrmHereMain.FormDestroy(Sender: TObject); begin ClearFileList(pKnownFiles); ClearFolderList(pKnownFolders); pKnownFiles := nil; pKnownFolders := nil; oChecked.Free; oPartiallyChecked.Free; end; procedure TfrmHereMain.ClearFileList(var aList: TFilePtr); var pKiller: TFilePtr; begin pKiller := aList; while pKiller <> nil do begin aList := aList^.Next; Dispose(pKiller); pKiller := aList; end; end; procedure TfrmHereMain.ClearFolderList(var aList: TFolderPtr); var pKiller: TFolderPtr; begin pKiller := aList; while pKiller <> nil do begin aList := aList^.Next; Dispose(pKiller); pKiller := aList; end; end; procedure TfrmHereMain.FormResize(Sender: TObject); begin pnlDiscovering.Left := Trunc((Width/2)-(pnlDiscovering.Width/2)); pnlDiscovering.Top := Trunc((Height/2)-(pnlDiscovering.Height/2)); SetGridColWidths; edtFileName.Width := (pnlLocateControls.Width - 30) - edtFileName.Left; btnSearch.Left := Round((pnlLocateControls.Width/2) - Round(btnSearch.Width/2)); lblMatchCount.Left := (pnlLocateControls.Width - lblMatchCount.Width) - 3; end; procedure TfrmHereMain.SetGridColWidths; begin with sgMatches do begin ColWidths[0] := Trunc(sgMatches.Width * 0.27); ColWidths[1] := Trunc(sgMatches.Width * 0.37); ColWidths[2] := Trunc(sgMatches.Width * 0.10); ColWidths[3] := Trunc(sgMatches.Width * 0.22); end; end; procedure TfrmHereMain.FormShow(Sender: TObject); begin pgcMain.ActivePageIndex := 0; pgcStatistics.ActivePageIndex := 0; tsLocate.TabVisible := False; tsStatistics.TabVisible := False; sgMatches.Cells[0,0] := 'File'; sgMatches.Cells[1,0] := 'Folder'; sgMatches.Cells[2,0] := 'Size'; sgMatches.Cells[3,0] := 'Last Modified'; SetGridColWidths; TreeView1.Items.Clear; LoadDriveNodes; end; procedure TfrmHereMain.sgMatchesSelectCell(Sender: TObject; ACol, ARow: Integer; var CanSelect: Boolean); begin edtSelectedMatch.Text := sgMatches.Cells[1, ARow]+sgMatches.Cells[0, ARow]; end; procedure TfrmHereMain.sgMatchesDrawCell(Sender: TObject; ACol, ARow: Integer; Rect: TRect; State: TGridDrawState); var iTextWidth: Integer; begin if (aCol = 2) {Size} then begin { determine the width the text will be, when displayed } iTextWidth := sgMatches.Canvas.TextExtent(sgMatches.Cells[ACol, ARow]).cx; { right justify the column text by re-drawing it manually } sgMatches.Canvas.TextRect(Rect, Rect.right - iTextWidth - 3,Rect.Top+2, sgMatches.Cells[ACol, ARow]); end; end; procedure TfrmHereMain.GatherStatistics; var pEye: TFilePtr; pFolder: TFolderPtr; i: Integer; begin iLargestFileSize := 0; sLargestFileName := ''; for i := 0 to 12 do aryFileSizes[i] := 0; for i := 0 to 12 do aryModDaysAgo[i] := 0; pEye := pKnownFiles; while pEye <> nil do begin {Largest File Size} if pEye^.Size > iLargestFileSize then begin iLargestFileSize := pEye^.Size; sLargestFileName := pEye^.Folder^.Folder+pEye^.FileName; end; {File Size graph} case pEye^.Size of 0..512: inc(aryFileSizes[0]); 513..1024: inc(aryFileSizes[1]); 1025..2048: inc(aryFileSizes[2]); 2049..4096: inc(aryFileSizes[3]); 4099..8192: inc(aryFileSizes[4]); 8193..16384: inc(aryFileSizes[5]); 16385..32768: inc(aryFileSizes[6]); 32769..65536: inc(aryFileSizes[7]); 65537..131072: inc(aryFileSizes[8]); 131073..262144: inc(aryFileSizes[9]); 262145..524288: inc(aryFileSizes[10]); 524289..1048576: inc(aryFileSizes[11]); else inc(aryFileSizes[12]); end; {File Modifications graph} case Round((Now-pEye^.LastModified)+1) of 0..1: inc(aryModDaysAgo[0]); 2: inc(aryModDaysAgo[1]); 3: inc(aryModDaysAgo[2]); 4: inc(aryModDaysAgo[3]); 5: inc(aryModDaysAgo[4]); 6: inc(aryModDaysAgo[5]); 7: inc(aryModDaysAgo[6]); 8..30: inc(aryModDaysAgo[7]); {1 week - 1 mo} 31..183: inc(aryModDaysAgo[8]); {1 mo - 6 mo} 184..365: inc(aryModDaysAgo[9]); {6 mo - 1 yr} 366..730: inc(aryModDaysAgo[10]); {1 yr - 2 yrs} 731..1096: inc(aryModDaysAgo[11]); {2 yrs - 3 yrs} else inc(aryModDaysAgo[12]); {3+ yrs} end; {Average File Size} iAverageFileSizeTotal := iAverageFileSizeTotal + pEye^.Size; inc(iAverageFileSizeCount); {Folder File Count} inc(pEye^.Folder^.FileCount); {Folder File Size Total} pEye^.Folder^.TotalFileSize := pEye^.Folder^.TotalFileSize + pEye^.Size; pEye := pEye^.Next; end; {# of Folders} iFolderCount := 0; pFolder := pKnownFolders; while pFolder <> nil do begin inc(iFolderCount); CheckTopFoldersByFileCount(pFolder); CheckTopFoldersByFileSize(pFolder); pFolder := pFolder^.Next; end; tsFileSizesShow(self); tsFoldersShow(self); tsModificationsShow(self); end; procedure TfrmHereMain.tsFileSizesShow(Sender: TObject); begin if not bFilesHaveBeenLearned then Exit; lblTotalFiles.Caption := FormatFloat('###,###,###,###,##0', iKnownFileCount); lblAverageFileSize.Caption := FormatFloat('###, ###, ###, ##0 KB', Round((iAverageFileSizeTotal/iAverageFileSizeCount)/1024)); lblLargestFileName.Caption := ExtractFileName(sLargestFileName); lblLargestFileFolder.Caption := ExtractFilePath(sLargestFileName); lblLargestFileSize.Caption := FormatFloat('###, ###, ###, ##0 KB', iLargestFileSize); DrawFileSizeChart; end; procedure TfrmHereMain.DrawFileSizeChart; const aryBars: array[0..12] of PChar = ('0-0.5KB', '0.5-1KB', '1-2KB', '2-4KB', '4-8KB', '8-16KB', '16-32KB', '32-64KB', '64-128KB', '128-256KB', '256-512KB','512-1MB','1MB+'); var iHighestCount, i, iX, iBarBottom, iBar, iBarThickness: Integer; iScaleFactor: Double; begin iBarBottom := pbxFileSizes.Left + 60; with pbxFileSizes.Canvas do begin Pen.Style := psSolid; Font.Size := 8; iBarThickness := 13; iHighestCount := 0; for i := 0 to 12 do if aryFileSizes[i] > iHighestCount then iHighestCount := aryFileSizes[i]; iScaleFactor := iHighestCount/(pbxFileSizes.Width-125); Pen.Color := clGray; iX := 0; for iBar := 0 to High(aryFileSizes) do begin if Pen.Color = clNavy then Pen.Color := clGray else Pen.Color := clNavy; for i := (iBar*iBarThickness) to ((iBar*iBarThickness)+12) do begin MoveTo(iBarBottom, i); iX := iBarBottom + Round(aryFileSizes[iBar]/iScaleFactor); LineTo(iX, i); end; {Legend} Brush.Color := clBtnFace; TextOut(2, (iBar*iBarThickness), aryBars[iBar]); TextOut(iX+2, (iBar*iBarThickness), FormatFloat('###,###,##0', aryFileSizes[iBar])); end; end; end; procedure TfrmHereMain.pbxFileSizesPaint(Sender: TObject); begin tsFileSizesShow(Sender); end; procedure TfrmHereMain.tsFoldersShow(Sender: TObject); var i: Integer; begin if not bFilesHaveBeenLearned then Exit; lblFolderCount.Caption := FormatFloat('###,###,##0', iFolderCount); lbxFoldersByFileCount.Items.Clear; lbxFoldersByFileSize.Items.Clear; for i := 0 to 99 do if aryFoldersByFileCount[i] <> nil then lbxFoldersByFileCount.Items.Add(FormatFloat('###,###,##0', aryFoldersByFileCount[i]^.FileCount) + ' - ' + aryFoldersByFileCount[i]^.Folder); lblFoldersByFileCount.Caption := 'Top ' + IntToStr(lbxFoldersByFileCount.Items.Count) + ' Folders (by File Count):'; for i := 0 to 99 do if aryFoldersByFileSize[i] <> nil then lbxFoldersByFileSize.Items.Add(FormatFloat('###,###,##0MB', aryFoldersByFileSize[i]^.TotalFileSize/1048576) + ' - ' + aryFoldersByFileSize[i]^.Folder); lblFoldersByFileSize.Caption := 'Top ' + IntToStr(lbxFoldersByFileSize.Items.Count) + ' Folders (by File Size):'; end; procedure TfrmHereMain.CheckTopFoldersByFileCount(var aFolder: TFolderPtr); var i, iFBFCIndex: Integer; pHold: TFolderPtr; aryTemp: array[0..99] of TFolderPtr; begin { Clear the temp array } for i := 0 to 99 do aryTemp[i] := nil; pHold := aFolder; iFBFCIndex := 0; { Insert pHold (aka aFolder) in the correct position, front & back-filled by the main array (aryFoldersByFileCount) } for i := 0 to 99 do if pHold = nil then begin aryTemp[i] := aryFoldersByFileCount[iFBFCIndex]; inc(iFBFCIndex); end else if aryFoldersByFileCount[i]=nil then begin aryTemp[i] := pHold; pHold := nil; end else if pHold^.FileCount > aryFoldersByFileCount[i]^.FileCount then begin aryTemp[i] := pHold; pHold := nil; end else begin aryTemp[i] := aryFoldersByFileCount[iFBFCIndex]; inc(iFBFCIndex); end; { Use the values from the temp array as the new main array } for i := 0 to 99 do aryFoldersByFileCount[i] := aryTemp[i]; end; procedure TfrmHereMain.CheckTopFoldersByFileSize(var aFolder: TFolderPtr); var i, iFBFSIndex: Integer; pHold: TFolderPtr; aryTemp: array[0..99] of TFolderPtr; begin { Clear the temp array } for i := 0 to 99 do aryTemp[i] := nil; pHold := aFolder; iFBFSIndex := 0; { Insert pHold (aka aFolder) in the correct position, front & back-filled by the main array (aryFoldersByFileSize) } for i := 0 to 99 do if pHold = nil then begin aryTemp[i] := aryFoldersByFileSize[iFBFSIndex]; inc(iFBFSIndex); end else if aryFoldersByFileSize[i]=nil then begin aryTemp[i] := pHold; pHold := nil; end else if pHold^.TotalFileSize > aryFoldersByFileSize[i]^.TotalFileSize then begin aryTemp[i] := pHold; pHold := nil; end else begin aryTemp[i] := aryFoldersByFileSize[iFBFSIndex]; inc(iFBFSIndex); end; { Use the values from the temp array as the new main array } for i := 0 to 99 do aryFoldersByFileSize[i] := aryTemp[i]; end; procedure TfrmHereMain.tsModificationsShow(Sender: TObject); begin if not bFilesHaveBeenLearned then Exit; DrawFileModChart; end; procedure TfrmHereMain.DrawFileModChart; const aryBars: array[0..12] of PChar = ('0-1 days', '2 days', '3 days', '4 days', '5 days', '6 days', '7 days', '8-30 days', '1-6 mo', '6 mo - 1 yr', '1 - 2 yrs','2 - 3 yrs','3+ yrs'); var iHighestCount, i, iX, iBarBottom, iBar, iBarThickness: Integer; iScaleFactor: Double; begin iBarBottom := pbxFileMods.Left + 60; with pbxFileMods.Canvas do begin Pen.Style := psSolid; Font.Size := 8; iBarThickness := 13; iHighestCount := 0; for i := 0 to 12 do if aryModDaysAgo[i] > iHighestCount then iHighestCount := aryModDaysAgo[i]; iScaleFactor := iHighestCount/(pbxFileMods.Width-125); Pen.Color := clGray; iX := 0; for iBar := 0 to High(aryModDaysAgo) do begin if Pen.Color = clNavy then Pen.Color := clGray else Pen.Color := clNavy; for i := (iBar*iBarThickness) to ((iBar*iBarThickness)+12) do begin MoveTo(iBarBottom, i); iX := iBarBottom + Round(aryModDaysAgo[iBar]/iScaleFactor); LineTo(iX, i); end; {Legend} Brush.Color := clBtnFace; TextOut(2, (iBar*iBarThickness), aryBars[iBar]); TextOut(iX+2, (iBar*iBarThickness), FormatFloat('###,###,##0', aryModDaysAgo[iBar])); end; end; end; procedure TfrmHereMain.pbxFileModsPaint(Sender: TObject); begin tsModificationsShow(sender); end; procedure TfrmHereMain.edtSizeAmountKeyPress(Sender: TObject; var Key: Char); begin { Only digits are allowed. #8 is back-space. } if Pos(Key, '1234567890'+#8) = 0 then Key := #0; end; procedure TfrmHereMain.btnSearchClick(Sender: TObject); var sTemp: string; iTemp: Int64; dTemp: Double; begin if ((not ckbSearchByFileName.Checked) and (not ckbSearchBySize.Checked) and (not ckbSearchByModTime.Checked)) then begin MessageDlg('Please include at least one of the search methods using the check-boxes on the left.', mtInformation, [mbok], 0); Exit; end; if ckbSearchByFileName.Checked then begin if Length(Trim(edtFileName.Text)) < 3 then begin MessageDlg('Please enter at least 3 characters for the file name pattern.', mtInformation, [mbok], 0); edtFileName.SetFocus; Exit; end; if Pos('**', Trim(edtFileName.Text)) > 0 then begin MessageDlg('Wildcards(*) may not be adjacent to each other.'+#13#13+'eg. **A, A**, ***', mtInformation, [mbok], 0); edtFileName.SetFocus; Exit; end; end; iSizeFloor := StrToInt(Trim(edtSizeAmount.Text)); case cbxSizeUnits.ItemIndex of 0: iSizeFloor := iSizeFloor*1024; {KBs} 1: iSizeFloor := iSizeFloor*1024*1024; {MBs} 2: iSizeFloor := iSizeFloor*1024*1024*1024; {GBs} end; iSizeCeiling := StrToInt(Trim(edtSizeAmount2.Text)); case cbxSizeUnits2.ItemIndex of 0: iSizeCeiling := iSizeCeiling*1024; {KBs} 1: iSizeCeiling := iSizeCeiling*1024*1024; {MBs} 2: iSizeCeiling := iSizeCeiling*1024*1024*1024; {GBs} end; if iSizeFloor > iSizeCeiling then begin iTemp := iSizeFloor; iSizeFloor := iSizeCeiling; iSizeCeiling := iTemp; end; dModTimeCeiling := StrToInt(Trim(edtModAmount2.Text)); case cbxModUnits2.ItemIndex of 0: dModTimeCeiling := (dModTimeCeiling/24)/60; {Minutes} 1: dModTimeCeiling := dModTimeCeiling/24; {Hours} 2: dModTimeCeiling := dModTimeCeiling; {Days} 3: dModTimeCeiling := dModTimeCeiling*7; {Weeks} 4: dModTimeCeiling := dModTimeCeiling*365; {Years} end; dModTimeFloor := StrToInt(Trim(edtModAmount.Text)); case cbxModUnits.ItemIndex of 0: dModTimeFloor := (dModTimeFloor/24)/60; {Minutes} 1: dModTimeFloor := dModTimeFloor/24; {Hours} 2: dModTimeFloor := dModTimeFloor; {Days} 3: dModTimeFloor := dModTimeFloor*7; {Weeks} 4: dModTimeFloor := dModTimeFloor*365; {Years} end; if (ckbSearchByModTime.Checked and ckbSearchByModTimeTo.Checked) then if dModTimeFloor > dModTimeCeiling then if MessageDlg('Your modification time range values are inverted. Would you like to fix this and continue searching?', mtWarning, [mbYes, mbNo], 0) = mrYes then begin sTemp := Trim(edtModAmount.Text); edtModAmount.Text := Trim(edtModAmount2.Text); edtModAmount2.Text := sTemp; iTemp := cbxModUnits.ItemIndex; cbxModUnits.ItemIndex := cbxModUnits2.ItemIndex; cbxModUnits2.ItemIndex := iTemp; dTemp := dModTimeFloor; dModTimeFloor := dModTimeCeiling; dModTimeCeiling := dTemp; end else Exit; PerformLocate; end; procedure TfrmHereMain.ckbSearchByModTimeClick(Sender: TObject); begin ckbSearchByModTimeTo.Enabled := ckbSearchByModTime.Checked; edtModAmount2.Enabled := ckbSearchByModTime.Checked; cbxModUnits2.Enabled := ckbSearchByModTime.Checked; end; procedure TfrmHereMain.edtSizeAmountExit(Sender: TObject); begin { Trim spaces } TEdit(Sender).Text := Trim(TEdit(Sender).Text); { Don't allow blank } if TEdit(Sender).Text = '' then TEdit(Sender).Text := '0'; {Zero is allowed} { Trim leading zeros } while (TEdit(Sender).Text[1] = '0') and (Length(TEdit(Sender).Text) > 1) do TEdit(Sender).Text := Copy(TEdit(Sender).Text, 2, Length(TEdit(Sender).Text)); { Don't allow blank } if TEdit(Sender).Text = '' then TEdit(Sender).Text := '0'; {Zero is allowed} end; procedure TfrmHereMain.edtSizeAmount2Exit(Sender: TObject); begin { Trim spaces } TEdit(Sender).Text := Trim(TEdit(Sender).Text); { Don't allow blank } if TEdit(Sender).Text = '' then TEdit(Sender).Text := '1'; {Zero is not allowed} { Trim leading zeros } while (TEdit(Sender).Text[1] = '0') and (Length(TEdit(Sender).Text) > 1) do TEdit(Sender).Text := Copy(TEdit(Sender).Text, 2, Length(TEdit(Sender).Text)); { Don't allow blank } if TEdit(Sender).Text = '' then TEdit(Sender).Text := '1'; {Zero is not allowed} end; procedure TfrmHereMain.TreeView1CustomDrawItem(Sender: TCustomTreeView; Node: TTreeNode; State: TCustomDrawState; var DefaultDraw: Boolean); var rRect, rBtnRect, rCheckBox: TRect; nParent: TTreeNode; procedure AdjustRectSize(var aRect: TRect; aAdjust: Integer); begin Dec(aRect.Left, aAdjust); Dec(aRect.Top, aAdjust); Inc(aRect.Right, aAdjust); Inc(aRect.Bottom, aAdjust); end; procedure DrawExpansionButton(aRect: TRect); begin with TCustomTreeView(Sender).Canvas do begin {Draw box} Pen.Style := psSolid; Pen.Color := clSilver; Rectangle(aRect); {Clear inner edge of box} Pen.Color := clWhite; AdjustRectSize(aRect, -1); Rectangle(aRect); {Draw plus/minus signs} Pen.Color := clBlack; AdjustRectSize(aRect, 1); if Node.Expanded then begin {Draw minus sign} MoveTo(aRect.Left + 2, aRect.Top+4); LineTo(aRect.Left + 7, aRect.Top+4); end else begin {Draw plus sign} MoveTo(aRect.Left + 4, aRect.Top+2); LineTo(aRect.Left + 4, aRect.Top+7); MoveTo(aRect.Left + 2, aRect.Top+4); LineTo(aRect.Left + 7, aRect.Top+4); end; end; {with} end; procedure DrawCheckBox(aRect: TRect); var iLine: Integer; begin with TCustomTreeView(Sender).Canvas do begin Pen.Color := clSilver; Rectangle(aRect); if Node.Data <> nil then begin {Draw checkmark} if Node.Data = oChecked then Pen.Color := clBlack {fully checked} else Pen.Color := clSilver; {partially checked & Partially checked include} for iLine := 0 to 2 do begin MoveTo(aRect.Left+2, aRect.Top+4+iLine); LineTo(aRect.Left+4, aRect.Top+6+iLine); LineTo(aRect.Left+9, aRect.Top+1+iLine); end; end; end; {with} end; begin DefaultDraw := not bCustomDraw; if not bCustomDraw then Exit; rRect := Node.DisplayRect(false); rBtnRect := Rect(rRect.Left+5+(Node.Level*19), rRect.Top+4, rRect.Left+14+(Node.Level*19), rRect.Top+13); with TCustomTreeView(Sender).Canvas do begin Pen.Color := clSilver; Pen.Style := psSolid; {Line from button to text} Pen.Color := clSilver; MoveTo(rRect.Left+9+(Node.Level*19), rRect.Top+8); LineTo(rRect.Left+18+(Node.Level*19), rRect.Top+8); if not Node.IsFirstNode then begin {All nodes, except the first, have a line from their center to the one above, be it parent or sibling} {Draw line from top of rect to middle} MoveTo(rBtnRect.Left+4, rRect.Top); LineTo(rBtnRect.Left+4, rRect.Top+Round((rRect.Bottom-rRect.Top)/2)); end; if (Node.getNextSibling <> nil) then begin {Node has a lower sibling} {Draw line from middle this node to bottom of Rect} MoveTo(rBtnRect.Left+4, rRect.Top+Round((rRect.Bottom-rRect.Top)/2)); LineTo(rBtnRect.Left+4, rRect.Bottom); end; nParent := Node.Parent; while nParent <> nil do begin if (nParent.getNextSibling <> nil) then begin {Draw Ancestor Line Segments} MoveTo(rRect.Left+9+(nParent.Level*TTreeView(Sender).Indent), rRect.Top); LineTo(rRect.Left+9+(nParent.Level*TTreeView(Sender).Indent), rRect.Bottom); end; nParent := nParent.Parent; end; if Node.HasChildren then DrawExpansionButton(rBtnRect); rCheckBox := Rect(rBtnRect.Right+6, rRect.Top+3, rBtnRect.Right+17, rRect.Bottom-2); DrawCheckBox(rCheckBox); TextOut(rCheckBox.Right+2, rRect.Top+1, Node.Text); end; {with} end; procedure TfrmHereMain.TreeView1Addition(Sender: TObject; Node: TTreeNode); begin {The portion of the node that captures click events starts with the check box, but only extends as far as the length of the text. Since the text is shifted to the right, spaces must be added to the end to ensure the the last few characters are clickable.} //tw - a more elegant fix is needed Node.Text := Node.Text+' '; Node.Data := nil; end; procedure TfrmHereMain.TreeView1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if bCollapsing then begin bCollapsing := False; Exit; end; if bExpanding then begin bExpanding := False; Exit; end; HandleNodeClick(TTreeView(Sender).Selected); end; procedure TfrmHereMain.TreeView1KeyPress(Sender: TObject; var Key: Char); begin if Key = ' ' then HandleNodeClick(TTreeView(Sender).Selected); end; procedure TfrmHereMain.HandleNodeClick(aNode: TTreeNode); begin if aNode = nil then Exit; if aNode.Data <> oChecked then aNode.Data := oChecked else aNode.Data := nil; if aNode.HasChildren then InformChildNodes(aNode); if aNode.Parent <> nil then InformParentNode(aNode.Parent, (aNode.Data <> nil)); aNode.TreeView.Refresh; SetLearnButtonAccessability; end; procedure TfrmHereMain.InformParentNode(aParent: TTreeNode; aChecked: Boolean); begin if aChecked then if AllDescendantsAreChecked(aParent) then aParent.Data := oChecked else aParent.Data := oPartiallyChecked else aParent.Data := oPartiallyChecked; if aParent.Parent <> nil then InformParentNode(aParent.Parent, (aParent.Data <> nil)); end; function TfrmHereMain.AllDescendantsAreChecked(aParent: TTreeNode): Boolean; var nDescendant: TTreeNode; begin result := True; nDescendant := aParent.getFirstChild; while (nDescendant <> nil) and result do begin if nDescendant.Data = nil then begin result := False; Break; end; if nDescendant.HasChildren then result := AllDescendantsAreChecked(nDescendant); nDescendant := aParent.GetNextChild(nDescendant); end; end; procedure TfrmHereMain.InformChildNodes(aNode: TTreeNode); var nChild: TTreeNode; begin nChild := aNode.getFirstChild; while nChild <> nil do begin nChild.Data := aNode.Data; if nChild.HasChildren then InformChildNodes(nChild); nChild := aNode.GetNextChild(nChild); end; end; procedure TfrmHereMain.TreeView1Collapsing(Sender: TObject; Node: TTreeNode; var AllowCollapse: Boolean); begin bCollapsing := True; end; procedure TfrmHereMain.TreeView1Expanding(Sender: TObject; Node: TTreeNode; var AllowExpansion: Boolean); begin bExpanding := True; end; procedure TfrmHereMain.LoadDriveNodes; var slDrives: TStringList; iFolder: Integer; begin slDrives := TStringList.Create; BuildDriveList(slDrives); for iFolder := 0 to (slDrives.Count - 1) do TreeView1.Items.AddChild(nil, slDrives[iFolder]); slDrives.Free; end; procedure TfrmHereMain.DiscoverRootNodeFolders; var nRootNode: TTreeNode; begin bFilesHaveBeenLearned := False; pnlDiscovering.Caption := 'Discovering Folders...'; pnlDiscovering.visible := True; pnlDiscovering.Refresh; ClearAllListsAndCounts; nRootNode := TreeView1.Items.GetFirstNode; while nRootNode <> nil do begin inc(iKnownFolderCount); if nRootNode.Data = oChecked then begin nRootNode.DeleteChildren; FolderToTreeNodes(Trim(nRootNode.Text), nRootNode); end; nRootNode := nRootNode.getNextSibling; end; pnlDiscovering.visible := False; end; procedure TfrmHereMain.BuildDriveList(var aList: TStringList); var DriveNum: Integer; DriveChar: Char; DriveBits: set of 0..25; begin aList.Clear; Integer(DriveBits) := GetLogicalDrives; for DriveNum := 0 to 25 do begin if not (DriveNum in DriveBits) then Continue; DriveChar := UpCase(Char(DriveNum + Ord('a'))); aList.Add(DriveChar+':'); end; {for} end; procedure TfrmHereMain.FolderToTreeNodes(aFolder: string; aParentNode: TTreeNode); var srSearcher: TSearchRec; sFolder: string; aNewNode: TTreeNode; begin sFolder := aFolder + '\*.*'; if FindFirst(sFolder, faDirectory, srSearcher) = 0 then begin repeat if (srSearcher.Attr and faDirectory) <> 0 then if (srSearcher.Name <> '.') and (srSearcher.Name <> '..') then begin aNewNode := TreeView1.Items.AddChild(aParentNode, srSearcher.Name); aNewNode.Data := oChecked; inc(iKnownFolderCount); if (iKnownFolderCount mod 100) = 0 then begin pnlDiscovering.Caption := 'Discovered ' + FormatFloat('###,###,###,##0',iKnownFolderCount) + ' Folders'; pnlDiscovering.Refresh; end; FolderToTreeNodes(aFolder + '\' + srSearcher.Name, aNewNode); end; until FindNext(srSearcher) <> 0; FindClose(srSearcher); end; end; procedure TfrmHereMain.btnDiscoverFoldersClick(Sender: TObject); begin if MessageDlg('This process may take a few minutes, especially if you''ve ' + 'selected any network drives.'+#13#13+'Would you like to proceed?', mtConfirmation, [mbyes, mbno], 0) = mrYes then begin iKnownFolderCount := 0; DiscoverRootNodeFolders; end; end; procedure TfrmHereMain.btnWhyDiscoverFoldersClick(Sender: TObject); begin MessageDlg('Why would I want to Discover Folders?' + #13#13 + 'Discovering the folders of the selected drives allows you to include/exclude ' + 'specific folders of these drives, rather than learning all the files on the ' + 'drive. However, the discovery process may take a few minutes. This process ' + 'may be considerably longer if you''ve selected any mapped network drives.', mtInformation, [mbok], 0); end; procedure TfrmHereMain.btnLearnFilesNowClick(Sender: TObject); var nRootNode: TTreeNode; begin bFilesHaveBeenLearned := False; pnlDiscovering.Caption := 'Discovering Files...'; pnlDiscovering.visible := True; pnlDiscovering.Refresh; frmHereMain.Enabled := False; ClearAllListsAndCounts; nRootNode := TreeView1.Items.GetFirstNode; while nRootNode <> nil do begin if nRootNode.Data <> nil then if nRootNode.HasChildren then LearnFilesInNode(nRootNode) else BuildRecursiveFileList(Trim(nRootNode.Text), True); nRootNode := nRootNode.getNextSibling; end; GatherStatistics; pnlDiscovering.visible := False; frmHereMain.Enabled := True; bFilesHaveBeenLearned := True; tsLocate.TabVisible := True; tsStatistics.TabVisible := True; end; procedure TfrmHereMain.BuildRecursiveFileList(aFolder: string; aSearchSubFolders: Boolean); var srSearcher: TSearchRec; sFolder: string; pNew: TFilePtr; pNewFolder: TFolderPtr; begin if aFolder[Length(aFolder)] <> '\' then aFolder := aFolder + '\'; New(pNewFolder); pNewFolder^.Folder := aFolder; pNewFolder^.UCFolder := UpperCase(aFolder); pNewFolder^.FileCount := 0; pNewFolder^.TotalFileSize := 0; pNewFolder^.Next := pKnownFolders; pKnownFolders := pNewFolder; sFolder := aFolder + '*.*'; if FindFirst(sFolder, faAnyFile, srSearcher) = 0 then begin repeat if (srSearcher.Attr and faDirectory) <> 0 then begin if (srSearcher.Name <> '.') and (srSearcher.Name <> '..') and aSearchSubFolders then BuildRecursiveFileList(aFolder + srSearcher.Name, aSearchSubFolders) end else begin New(pNew); pNew^.FileName := srSearcher.Name; pNew^.UCFileName := UpperCase(pNew^.FileName); pNew^.Folder := pNewFolder; pNew^.Size := srSearcher.Size; pNew^.LastModified := FileDateToDateTime(srSearcher.Time); pNew^.SearchMatch := False; pNew^.Next := pKnownFiles; pNew^.NextMatch := nil; pKnownFiles := pNew; inc(iKnownFileCount); end; if (iKnownFileCount mod 1000) = 0 then begin pnlDiscovering.Caption := 'Discovering Files...('+FormatFloat('###,###,###,##0', iKnownFileCount)+')'; pnlDiscovering.Refresh; end; until FindNext(srSearcher) <> 0; FindClose(srSearcher); end; end; procedure TfrmHereMain.ClearAllListsAndCounts; var i: Integer; begin tsLocate.TabVisible := False; tsStatistics.TabVisible := False; iKnownFileCount := 0; ClearFileList(pKnownFiles); ClearFolderList(pKnownFolders); for i := 0 to 99 do begin aryFoldersByFileCount[i] := nil; aryFoldersByFileSize[i] := nil; end; end; function TfrmHereMain.GetNodePath(var aNode: TTreeNode): string; var nCurrNode: TTreeNode; begin result := ''; nCurrNode := aNode; while nCurrNode <> nil do begin result := Trim(nCurrNode.Text) + '\' +result; nCurrNode := nCurrNode.Parent; end; end; procedure TfrmHereMain.LearnFilesInNode(var aNode: TTreeNode); var nChild: TTreeNode; begin if (aNode.Data = oChecked) or ((aNode.Data = oPartiallyChecked) and (cbxIncludeParent.Checked)) then LearnFilesInFolder(GetNodePath(aNode)); if (aNode.Data = oChecked) or (aNode.Data = oPartiallyChecked) then begin {Traverse children} nChild := aNode.GetFirstChild; while nChild <> nil do begin LearnFilesInNode(nChild); nChild := nChild.getNextSibling; end; end; end; procedure TfrmHereMain.LearnFilesInFolder(aFolder: string); var srSearcher: TSearchRec; sFolder: string; pNew: TFilePtr; pNewFolder: TFolderPtr; begin aFolder := Trim(aFolder); if aFolder[Length(aFolder)] <> '\' then aFolder := aFolder + '\'; New(pNewFolder); pNewFolder^.Folder := aFolder; pNewFolder^.FileCount := 0; pNewFolder^.TotalFileSize := 0; pNewFolder^.Next := pKnownFolders; pKnownFolders := pNewFolder; sFolder := aFolder + '*.*'; if FindFirst(sFolder, faAnyFile, srSearcher) = 0 then begin repeat if (srSearcher.Attr and faDirectory) = 0 then begin New(pNew); pNew^.FileName := srSearcher.Name; pNew^.UCFileName := UpperCase(pNew^.FileName); pNew^.Folder := pNewFolder; pNew^.Size := srSearcher.Size; pNew^.LastModified := FileDateToDateTime(srSearcher.Time); pNew^.SearchMatch := False; pNew^.Next := pKnownFiles; pNew^.NextMatch := nil; pKnownFiles := pNew; inc(iKnownFileCount); end; if (iKnownFileCount mod 1000) = 0 then begin pnlDiscovering.Caption := 'Discovered ' + FormatFloat('###,###,###,##0', iKnownFileCount) + ' Files'; pnlDiscovering.Refresh; end; until FindNext(srSearcher) <> 0; FindClose(srSearcher); end; end; procedure TfrmHereMain.SetLearnButtonAccessability; var iRootNodesChecked: Integer; nRootNode: TTreeNode; begin iRootNodesChecked := 0; nRootNode := TreeView1.Items.GetFirstNode; while nRootNode <> nil do begin if nRootNode.Data <> nil then begin inc(iRootNodesChecked); break; end else nRootNode := nRootNode.getNextSibling; end; btnDiscoverFolders.Enabled := (iRootNodesChecked > 0); btnLearnFilesNow.Enabled := (iRootNodesChecked > 0); cbxIncludeParent.Enabled := (iRootNodesChecked > 0); end; procedure TfrmHereMain.sgMatchesMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var Column, Row: Longint; begin if Button = mbLeft then begin sgMatches.MouseToCell(X, Y, Column, Row); if Row = 0 then SortResults(Column); end; end; procedure TfrmHereMain.SortResults(aColumn: Integer); var pSelected: TFilePtr; begin if aColumn = iResultsSortByCol then bResultSortAscending := not bResultSortAscending; iResultsSortByCol := aColumn; lblMatchCount.Caption := 'sorting...'; lblMatchCount.Refresh; sgMatches.Visible := False; pSelected := nil; if sgMatches.Row > 0 then pSelected := TFilePtr(sgMatches.Objects[0, sgMatches.Row]); PerformSort(iResultsSortByCol, bResultSortAscending); DisplaySearchResults; SelectRowByObject(pSelected); sgMatches.Visible := True; edtSelectedMatch.Text := sgMatches.Cells[1, sgMatches.Row]+sgMatches.Cells[0, sgMatches.Row]; lblMatchCount.Caption := FormatFloat('###,###,##0', iMatchCount) + ' matches'; end; procedure TfrmHereMain.PerformSort(aColumn: Integer; aAscending: Boolean); var pElem1, pElem2, pPrefix, pOrigin: TFilePtr; bChanged, SwitchElems: boolean; begin {This proc sorts the entire list of files(not just the search matches), based on a given grid column.} if (pKnownFiles = nil) then Exit; {can't sort 0 items!} if (pKnownFiles^.Next = nil) then Exit; {can't sort just 1 item!} {Create a temporary new origin for the list. This allows the first item to be treated the same as items 2..n.} New(pOrigin); pOrigin^.NextMatch := pMatches; bChanged := True; while bChanged do begin bChanged := False; pPrefix := pOrigin; while (pPrefix <> nil) do begin pElem1 := pPrefix^.NextMatch; if pElem1 = nil then break; pElem2 := pElem1^.NextMatch; if pElem2 = nil then break; SwitchElems := False; if aAscending then begin case aColumn of 0: SwitchElems := (_StrComp(pElem1^.UCFileName, pElem2^.UCFileName) > 0); {file name} 1: SwitchElems := (_StrComp(pElem1^.Folder^.UCFolder , pElem2^.Folder^.UCFolder) > 0); {folder} 2: SwitchElems := (pElem1^.Size > pElem2^.Size); {Size} 3: SwitchElems := (pElem1^.LastModified > pElem2^.LastModified); {last modified} end; end else begin case aColumn of 0: SwitchElems := (_StrComp(pElem1^.UCFileName, pElem2^.UCFileName) < 0); {file name} 1: SwitchElems := (_StrComp(pElem1^.Folder^.UCFolder , pElem2^.Folder^.UCFolder) < 0); {folder} 2: SwitchElems := (pElem1^.Size < pElem2^.Size); {Size} 3: SwitchElems := (pElem1^.LastModified < pElem2^.LastModified); {last modified} end; end; if SwitchElems then begin bChanged := True; pElem1^.NextMatch := pElem2^.NextMatch; pElem2^.NextMatch := pElem1; pPrefix^.NextMatch := pElem2; end; pPrefix := pPrefix^.NextMatch; end; {while (pSortPrefix...} end; {while bChanged...} pMatches := pOrigin^.NextMatch; Dispose(pOrigin); end; procedure TfrmHereMain.SelectRowByObject(var aSelected: TFilePtr); var iRow: Integer; begin for iRow := 1 to (sgMatches.RowCount - 1) do if TFilePtr(sgMatches.Objects[0, iRow]) = aSelected then begin sgMatches.Row := iRow; Exit; end; end; function _StrComp(var aStr1, aStr2: string): Integer; var iChar, iLen1, iLen2: Integer; begin result := 0; {Get the length of the shorter of the two strings} iLen1 := Length(aStr1); iLen2 := Length(aStr2); if (iLen1 < iLen2) and (iLen1 = 0) then result := -1; if (iLen2 < iLen1) and (iLen2 = 0) then result := 1; if result <> 0 then Exit; if iLen1 > iLen2 then iLen1 := iLen2; {store minimum length in iLen1} iChar := 1; while (iChar <= iLen1) and (result = 0) do begin Result := (Ord(aStr1[iChar]) - Ord(aStr2[iChar])); inc(iChar); end; if result = 0 then if Length(aStr1) > Length(aStr2) then result := 1 else if Length(aStr1) < Length(aStr2) then result := -1; end; end.