mirror of
https://gitlab.com/freepascal.org/lazarus/lazarus.git
synced 2025-04-13 00:09:25 +02:00
1106 lines
30 KiB
ObjectPascal
1106 lines
30 KiB
ObjectPascal
{
|
|
/***************************************************************************
|
|
ShellCtrls.pas
|
|
------------
|
|
|
|
|
|
***************************************************************************/
|
|
|
|
*****************************************************************************
|
|
This file is part of the Lazarus Component Library (LCL)
|
|
|
|
See the file COPYING.modifiedLGPL.txt, included in this distribution,
|
|
for details about the license.
|
|
*****************************************************************************
|
|
}
|
|
unit ShellCtrls;
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils, Forms, Graphics, LCLType,
|
|
ComCtrls, FileUtil, LazUtf8;
|
|
|
|
{$if defined(Windows) or defined(darwin)}
|
|
{$define CaseInsensitiveFilenames}
|
|
{$endif}
|
|
{$IF defined(CaseInsensitiveFilenames) or defined(darwin)}
|
|
{$DEFINE NotLiteralFilenames}
|
|
{$ENDIF}
|
|
|
|
type
|
|
|
|
{ TObjectTypes }
|
|
|
|
TObjectType = (otFolders, otNonFolders, otHidden);
|
|
|
|
TObjectTypes = set of TObjectType;
|
|
|
|
TFileSortType = (fstNone, fstAlphabet, fstFoldersFirst);
|
|
|
|
{ Forward declaration of the classes }
|
|
|
|
TCustomShellTreeView = class;
|
|
TCustomShellListView = class;
|
|
|
|
{ TCustomShellTreeView }
|
|
|
|
TCustomShellTreeView = class(TCustomTreeView)
|
|
private
|
|
FObjectTypes: TObjectTypes;
|
|
FRoot: string;
|
|
FShellListView: TCustomShellListView;
|
|
FFileSortType: TFileSortType;
|
|
{ Setters and getters }
|
|
function GetPath: string;
|
|
procedure SetFileSortType(const AValue: TFileSortType);
|
|
procedure SetPath(AValue: string);
|
|
procedure SetRoot(const AValue: string);
|
|
procedure SetShellListView(const Value: TCustomShellListView);
|
|
protected
|
|
{ Other methods specific to Lazarus }
|
|
function PopulateTreeNodeWithFiles(
|
|
ANode: TTreeNode; ANodePath: string): Boolean;
|
|
procedure PopulateWithBaseFiles;
|
|
procedure DoSelectionChanged; override;
|
|
function CanExpand(Node: TTreeNode): Boolean; override;
|
|
public
|
|
{ Basic methods }
|
|
constructor Create(AOwner: TComponent); override;
|
|
destructor Destroy; override;
|
|
|
|
{ Methods specific to Lazarus - useful for other classes }
|
|
class function GetBasePath: string;
|
|
function GetRootPath: string;
|
|
class procedure GetFilesInDir(const ABaseDir: string;
|
|
AMask: string; AObjectTypes: TObjectTypes; AResult: TStrings; AFileSortType: TFileSortType = fstNone);
|
|
{ Other methods specific to Lazarus }
|
|
function GetPathFromNode(ANode: TTreeNode): string;
|
|
function GetSelectedNodePath: string;
|
|
procedure Refresh(ANode: TTreeNode); overload;
|
|
|
|
{ Properties }
|
|
property ObjectTypes: TObjectTypes read FObjectTypes write FObjectTypes;
|
|
property ShellListView: TCustomShellListView read FShellListView write SetShellListView;
|
|
property FileSortType: TFileSortType read FFileSortType write SetFileSortType;
|
|
property Root: string read FRoot write SetRoot;
|
|
property Path: string read GetPath write SetPath;
|
|
|
|
{ Protected properties which users may want to access, see bug 15374 }
|
|
property Items;
|
|
end;
|
|
|
|
{ TShellTreeView }
|
|
|
|
TShellTreeView = class(TCustomShellTreeView)
|
|
published
|
|
{ TCustomTreeView properties }
|
|
property Align;
|
|
property Anchors;
|
|
property AutoExpand;
|
|
property BorderSpacing;
|
|
//property BiDiMode;
|
|
property BackgroundColor;
|
|
property BorderStyle;
|
|
property BorderWidth;
|
|
property Color;
|
|
property Constraints;
|
|
property Enabled;
|
|
property ExpandSignType;
|
|
property Font;
|
|
property FileSortType;
|
|
property HideSelection;
|
|
property HotTrack;
|
|
property Images;
|
|
property Indent;
|
|
//property ParentBiDiMode;
|
|
property ParentColor default False;
|
|
property ParentFont;
|
|
property ParentShowHint;
|
|
property PopupMenu;
|
|
property ReadOnly;
|
|
property RightClickSelect;
|
|
property Root;
|
|
property RowSelect;
|
|
property ScrollBars;
|
|
property SelectionColor;
|
|
property ShowButtons;
|
|
property ShowHint;
|
|
property ShowLines;
|
|
property ShowRoot;
|
|
property TabOrder;
|
|
property TabStop default True;
|
|
property Tag;
|
|
property ToolTips;
|
|
property Visible;
|
|
property OnAdvancedCustomDraw;
|
|
property OnAdvancedCustomDrawItem;
|
|
property OnChange;
|
|
property OnChanging;
|
|
property OnClick;
|
|
property OnCustomDraw;
|
|
property OnCustomDrawItem;
|
|
property OnKeyDown;
|
|
property OnKeyPress;
|
|
property OnKeyUp;
|
|
property OnMouseDown;
|
|
property OnMouseMove;
|
|
property OnMouseUp;
|
|
property OnSelectionChanged;
|
|
property OnShowHint;
|
|
property OnUTF8KeyPress;
|
|
property Options;
|
|
property TreeLineColor;
|
|
property TreeLinePenStyle;
|
|
property ExpandSignColor;
|
|
{ TCustomShellTreeView properties }
|
|
property ObjectTypes;
|
|
property ShellListView;
|
|
end;
|
|
|
|
{ TCustomShellListView }
|
|
|
|
TCustomShellListView = class(TCustomListView)
|
|
private
|
|
FMask: string;
|
|
FObjectTypes: TObjectTypes;
|
|
FRoot: string;
|
|
FShellTreeView: TCustomShellTreeView;
|
|
{ Setters and getters }
|
|
procedure SetMask(const AValue: string);
|
|
procedure SetShellTreeView(const Value: TCustomShellTreeView);
|
|
procedure SetRoot(const Value: string);
|
|
protected
|
|
{ Methods specific to Lazarus }
|
|
procedure PopulateWithRoot();
|
|
procedure Resize; override;
|
|
public
|
|
{ Basic methods }
|
|
constructor Create(AOwner: TComponent); override;
|
|
destructor Destroy; override;
|
|
{ Methods specific to Lazarus }
|
|
function GetPathFromItem(ANode: TListItem): string;
|
|
{ Properties }
|
|
property Mask: string read FMask write SetMask; // Can be used to conect to other controls
|
|
property ObjectTypes: TObjectTypes read FObjectTypes write FObjectTypes;
|
|
property Root: string read FRoot write SetRoot;
|
|
property ShellTreeView: TCustomShellTreeView read FShellTreeView write SetShellTreeView;
|
|
{ Protected properties which users may want to access, see bug 15374 }
|
|
property Items;
|
|
end;
|
|
|
|
{ TShellListView }
|
|
|
|
TShellListView = class(TCustomShellListView)
|
|
published
|
|
{ TCustomListView properties
|
|
The same as TListView excluding data properties }
|
|
property Align;
|
|
property Anchors;
|
|
property BorderSpacing;
|
|
property BorderStyle;
|
|
property BorderWidth;
|
|
// property Checkboxes;
|
|
property Color default clWindow;
|
|
// property Columns;
|
|
// property ColumnClick;
|
|
property Constraints;
|
|
property DragCursor;
|
|
property DragMode;
|
|
// property DefaultItemHeight;
|
|
// property DropTarget;
|
|
property Enabled;
|
|
// property FlatScrollBars;
|
|
property Font;
|
|
// property FullDrag;
|
|
// property GridLines;
|
|
property HideSelection;
|
|
// property HotTrack;
|
|
// property HotTrackStyles;
|
|
// property HoverTime;
|
|
property LargeImages;
|
|
property MultiSelect;
|
|
// property OwnerData;
|
|
// property OwnerDraw;
|
|
property ParentColor default False;
|
|
property ParentFont;
|
|
property ParentShowHint;
|
|
property PopupMenu;
|
|
property ReadOnly;
|
|
property RowSelect;
|
|
property ScrollBars;
|
|
property ShowColumnHeaders;
|
|
property ShowHint;
|
|
// property ShowWorkAreas;
|
|
property SmallImages;
|
|
property SortColumn;
|
|
property SortType;
|
|
property StateImages;
|
|
property TabStop;
|
|
property TabOrder;
|
|
property ToolTips;
|
|
property Visible;
|
|
property ViewStyle default vsReport;
|
|
// property OnAdvancedCustomDraw;
|
|
// property OnAdvancedCustomDrawItem;
|
|
// property OnAdvancedCustomDrawSubItem;
|
|
property OnChange;
|
|
property OnClick;
|
|
property OnColumnClick;
|
|
property OnCompare;
|
|
property OnContextPopup;
|
|
// property OnCustomDraw;
|
|
// property OnCustomDrawItem;
|
|
// property OnCustomDrawSubItem;
|
|
property OnDblClick;
|
|
property OnDeletion;
|
|
property OnDragDrop;
|
|
property OnDragOver;
|
|
property OnEndDrag;
|
|
property OnKeyDown;
|
|
property OnKeyPress;
|
|
property OnKeyUp;
|
|
property OnMouseDown;
|
|
property OnMouseMove;
|
|
property OnMouseUp;
|
|
property OnResize;
|
|
property OnSelectItem;
|
|
property OnStartDrag;
|
|
property OnUTF8KeyPress;
|
|
{ TCustomShellListView properties }
|
|
property ObjectTypes;
|
|
property Root;
|
|
property ShellTreeView;
|
|
end;
|
|
|
|
EInvalidPath = class(Exception);
|
|
|
|
const
|
|
//ToDo: make it a resource string
|
|
SShellCtrlsInvalidRoot = 'Invalid pathname:'#13'"%s"';
|
|
SShellCtrlsInvalidPath = 'Invalid pathname:'#13'"%s"';
|
|
SShellCtrlsInvalidPathRelative = 'Invalid relative pathname:'#13'"%s"'#13
|
|
+'in relation to rootpath:'#13'"%s"';
|
|
|
|
procedure Register;
|
|
|
|
implementation
|
|
|
|
{$ifdef windows}
|
|
uses Windows;
|
|
{$endif}
|
|
|
|
{
|
|
uses ShlObj;
|
|
|
|
// $I shellctrlswin32.inc
|
|
|
|
procedure PopulateTreeViewWithShell(ATreeView: TCustomShellTreeView);
|
|
var
|
|
ShellFolder: IShellFolder = nil;
|
|
Win32ObjectTypes: Integer;
|
|
// pidl: LPITEMIDLIST;
|
|
pidlParent: LPITEMIDLIST;
|
|
begin
|
|
SHGetSpecialFolderLocation(0, CSIDL_DESKTOP, @pidl);
|
|
|
|
SHGetDesktopFolder(ShellFolder);
|
|
|
|
if ShellFolder = nil then Exit;
|
|
|
|
// Converts the control data into Windows constants
|
|
|
|
Win32ObjectTypes := 0;
|
|
|
|
if otFolders in ATreeView.ObjectTypes then
|
|
Win32ObjectTypes := Win32ObjectTypes or SHCONTF_FOLDERS;
|
|
|
|
if otNonFolders in ATreeView.ObjectTypes then
|
|
Win32ObjectTypes := Win32ObjectTypes or SHCONTF_NONFOLDERS;
|
|
|
|
if otHidden in ATreeView.ObjectTypes then
|
|
Win32ObjectTypes := Win32ObjectTypes or SHCONTF_INCLUDEHIDDEN;
|
|
|
|
// Now gets the name of the desktop folder
|
|
}
|
|
|
|
{ TCustomShellTreeView }
|
|
|
|
procedure TCustomShellTreeView.SetShellListView(
|
|
const Value: TCustomShellListView);
|
|
var
|
|
Tmp: TCustomShellListView;
|
|
begin
|
|
if FShellListView = Value then Exit;
|
|
|
|
if Assigned(FShellListView) then
|
|
begin
|
|
Tmp := FShellListView;
|
|
FShellListView := nil;
|
|
Tmp.ShellTreeView := nil;
|
|
end;
|
|
|
|
FShellListView := Value;
|
|
|
|
// Update the pair, it will then update itself
|
|
// in the setter of this property
|
|
// Updates only if necessary to avoid circular calls of the setters
|
|
if Assigned(Value) and (Value.ShellTreeView <> Self) then
|
|
Value.ShellTreeView := Self;
|
|
end;
|
|
|
|
procedure TCustomShellTreeView.SetRoot(const AValue: string);
|
|
var
|
|
RootNode: TTreeNode;
|
|
begin
|
|
if FRoot=AValue then exit;
|
|
//Delphi raises an unspecified exception in this case, but don't crash the IDE at designtime
|
|
if not (csDesigning in ComponentState)
|
|
and (AValue <> '')
|
|
and not DirectoryExistsUtf8(ExpandFilenameUtf8(AValue)) then
|
|
Raise Exception.CreateFmt(SShellCtrlsInvalidRoot,[ExpandFileNameUtf8(AValue)]);
|
|
if (AValue = '') then
|
|
FRoot := GetBasePath
|
|
else
|
|
FRoot:=AValue;
|
|
Items.Clear;
|
|
if FRoot = '' then
|
|
begin
|
|
PopulateWithBaseFiles()
|
|
end
|
|
else
|
|
begin
|
|
//Add a node for Root and expand it (issue #0024230)
|
|
//Make FRoot contain fully qualified pathname, we need it later in GetPathFromNode()
|
|
FRoot := ExpandFileNameUtf8(FRoot);
|
|
//Set RootNode.Text to AValue so user can choose if text is fully qualified path or not
|
|
RootNode := Items.AddChild(nil, AValue);
|
|
RootNode.HasChildren := True;
|
|
RootNode.Expand(False);
|
|
end;
|
|
if Assigned(ShellListView) then
|
|
ShellListView.Root := FRoot;
|
|
end;
|
|
|
|
// ToDo: Optimize, now the tree is populated in constructor, SetRoot and SetFileSortType.
|
|
// For some reason it does not show in performance really.
|
|
procedure TCustomShellTreeView.SetFileSortType(const AValue: TFileSortType);
|
|
var
|
|
RootNode: TTreeNode;
|
|
CurrPath: String;
|
|
begin
|
|
if FFileSortType=AValue then exit;
|
|
FFileSortType:=AValue;
|
|
CurrPath := GetPath;
|
|
try
|
|
BeginUpdate;
|
|
Items.Clear;
|
|
if FRoot = '' then
|
|
PopulateWithBaseFiles()
|
|
else
|
|
begin
|
|
RootNode := Items.AddChild(nil, FRoot);
|
|
RootNode.HasChildren := True;
|
|
RootNode.Expand(False);
|
|
try
|
|
SetPath(CurrPath);
|
|
except
|
|
// CurrPath may have been removed in the mean time by another process, just ignore
|
|
on E: EInvalidPath do ;//
|
|
end;
|
|
end;
|
|
finally
|
|
EndUpdate;
|
|
end;
|
|
end;
|
|
|
|
function TCustomShellTreeView.CanExpand(Node: TTreeNode): Boolean;
|
|
var
|
|
OldAutoExpand: Boolean;
|
|
begin
|
|
Result:=inherited CanExpand(Node);
|
|
if not Result then exit;
|
|
OldAutoExpand:=AutoExpand;
|
|
AutoExpand:=False;
|
|
Node.DeleteChildren;
|
|
Result := PopulateTreeNodeWithFiles(Node, GetPathFromNode(Node));
|
|
AutoExpand:=OldAutoExpand;
|
|
end;
|
|
|
|
constructor TCustomShellTreeView.Create(AOwner: TComponent);
|
|
begin
|
|
inherited Create(AOwner);
|
|
|
|
// Initial property values
|
|
|
|
ObjectTypes:= [otFolders];
|
|
|
|
// Populates the base dirs
|
|
|
|
PopulateWithBaseFiles();
|
|
end;
|
|
|
|
destructor TCustomShellTreeView.Destroy;
|
|
begin
|
|
ShellListView := nil;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
type
|
|
{ TFileItem }
|
|
TFileItem = class(TObject)
|
|
Name: string;
|
|
isFolder: Boolean;
|
|
//more data to sort by size, date... etc
|
|
constructor Create(const DirInfo: TSearchRec);
|
|
end;
|
|
|
|
{ TFileItem }
|
|
|
|
constructor TFileItem.Create(const DirInfo:TSearchRec);
|
|
begin
|
|
Name:=DirInfo.Name;
|
|
isFolder:=DirInfo.Attr and FaDirectory > 0;
|
|
end;
|
|
|
|
function FilesSortAlphabet(p1, p2: Pointer): Integer;
|
|
var
|
|
f1, f2: TFileItem;
|
|
begin
|
|
f1:=TFileItem(p1);
|
|
f2:=TFileItem(p2);
|
|
Result:=CompareText(f1.Name, f2.Name);
|
|
end;
|
|
|
|
function FilesSortFoldersFirst(p1,p2: Pointer): Integer;
|
|
var
|
|
f1, f2: TFileItem;
|
|
begin
|
|
f1:=TFileItem(p1);
|
|
f2:=TFileItem(p2);
|
|
if f1.isFolder=f2.isFolder then
|
|
Result:=FilesSortAlphabet(p1,p2)
|
|
else begin
|
|
if f1.isFolder then Result:=-1
|
|
else Result:=1;
|
|
end;
|
|
|
|
end;
|
|
|
|
{ Helper routine.
|
|
Finds all files/directories directly inside a directory.
|
|
Does not recurse inside subdirectories.
|
|
|
|
AMask may contain multiple file masks separated by ;
|
|
Don't add a final ; after the last mask.
|
|
}
|
|
class procedure TCustomShellTreeView.GetFilesInDir(const ABaseDir: string;
|
|
AMask: string; AObjectTypes: TObjectTypes; AResult: TStrings; AFileSortType: TFileSortType);
|
|
var
|
|
DirInfo: TSearchRec;
|
|
FindResult: Integer;
|
|
IsDirectory, IsValidDirectory, IsHidden, AddFile: Boolean;
|
|
ObjectData: TObject;
|
|
SearchStr: string;
|
|
MaskStr: string;
|
|
Files: TList;
|
|
FileItem: TFileItem;
|
|
i: Integer;
|
|
MaskStrings: TStringList;
|
|
{$if defined(windows) and not defined(wince)}
|
|
ErrMode : LongWord;
|
|
{$endif}
|
|
begin
|
|
{$if defined(windows) and not defined(wince)}
|
|
// disables the error dialog, while enumerating not-available drives
|
|
// for example listing A: path, without diskette present.
|
|
// WARNING: Since Application.ProcessMessages is called, it might effect some operations!
|
|
ErrMode:=SetErrorMode(SEM_FAILCRITICALERRORS or SEM_NOALIGNMENTFAULTEXCEPT or SEM_NOGPFAULTERRORBOX or SEM_NOOPENFILEERRORBOX);
|
|
try
|
|
{$endif}
|
|
|
|
if Trim(AMask) = '' then MaskStr := AllFilesMask
|
|
else MaskStr := AMask;
|
|
|
|
// The string list implements support for multiple masks separated
|
|
// by semi-comma ";"
|
|
MaskStrings := TStringList.Create;
|
|
try
|
|
MaskStrings.Delimiter := ';';
|
|
MaskStrings.DelimitedText := MaskStr;
|
|
|
|
if AFileSortType=fstNone then Files:=nil
|
|
else Files:=TList.Create;
|
|
|
|
for i := 0 to MaskStrings.Count - 1 do
|
|
begin
|
|
if MaskStrings.IndexOf(MaskStrings[i]) < i then Continue; // From patch from bug 17761: TShellListView Mask: duplicated items if mask is " *.ext;*.ext "
|
|
SearchStr := IncludeTrailingPathDelimiter(ABaseDir) + MaskStrings.Strings[i];
|
|
|
|
FindResult := FindFirstUTF8(SearchStr, faAnyFile, DirInfo);
|
|
|
|
while FindResult = 0 do
|
|
begin
|
|
Application.ProcessMessages;
|
|
|
|
IsDirectory := (DirInfo.Attr and FaDirectory = FaDirectory);
|
|
|
|
IsValidDirectory := (DirInfo.Name <> '.') and (DirInfo.Name <> '..');
|
|
|
|
IsHidden := (DirInfo.Attr and faHidden = faHidden);
|
|
{$IFDEF Unix}
|
|
if (DirInfo.Name<>'') and (DirInfo.Name[1]='.') then
|
|
IsHidden:=true;
|
|
{$ENDIF}
|
|
|
|
// First check if we show hidden files
|
|
if IsHidden then AddFile := (otHidden in AObjectTypes)
|
|
else AddFile := True;
|
|
|
|
// If it is a directory, check if it is a valid one
|
|
if IsDirectory then
|
|
AddFile := AddFile and ((otFolders in AObjectTypes) and IsValidDirectory)
|
|
else
|
|
AddFile := AddFile and (otNonFolders in AObjectTypes);
|
|
|
|
// AddFile identifies if the file is valid or not
|
|
if AddFile then
|
|
begin
|
|
if not Assigned(Files) then begin
|
|
// Mark if it is a directory (ObjectData <> nil)
|
|
if IsDirectory then ObjectData := AResult
|
|
else ObjectData := nil;
|
|
if AResult.IndexOf(DirInfo.Name) < 0 then // From patch from bug 17761: TShellListView Mask: duplicated items if mask is " *.ext;*.ext "
|
|
AResult.AddObject(DirInfo.Name, ObjectData)
|
|
end else
|
|
Files.Add ( TFileItem.Create(DirInfo));
|
|
end;
|
|
|
|
FindResult := FindNextUTF8(DirInfo);
|
|
end;
|
|
|
|
FindCloseUTF8(DirInfo);
|
|
end;
|
|
finally
|
|
MaskStrings.Free;
|
|
end;
|
|
|
|
if Assigned(Files) then begin
|
|
Objectdata:=AResult;
|
|
|
|
case AFileSortType of
|
|
fstAlphabet: Files.Sort(@FilesSortAlphabet);
|
|
fstFoldersFirst: Files.Sort(@FilesSortFoldersFirst);
|
|
end;
|
|
|
|
for i:=0 to Files.Count-1 do
|
|
begin
|
|
FileItem:=TFileItem(Files[i]);
|
|
if (i < Files.Count - 1) and (TFileItem(Files[i]).Name = TFileItem(Files[i + 1]).Name) then
|
|
Continue; // cause Files is sorted // From patch from bug 17761: TShellListView Mask: duplicated items if mask is " *.ext;*.ext "
|
|
if FileItem.isFolder then
|
|
AResult.AddObject(FileItem.Name, ObjectData)
|
|
else
|
|
AResult.AddObject(FileItem.Name, nil);
|
|
end;
|
|
for i:=0 to Files.Count-1 do
|
|
TFileItem(Files[i]).Free;
|
|
Files.Free;
|
|
end;
|
|
|
|
{$if defined(windows) and not defined(wince)}
|
|
finally
|
|
SetErrorMode(ErrMode);
|
|
end;
|
|
{$endif}
|
|
end;
|
|
|
|
class function TCustomShellTreeView.GetBasePath: string;
|
|
begin
|
|
{$if defined(windows) and not defined(wince)}
|
|
Result := '';
|
|
{$endif}
|
|
{$ifdef wince}
|
|
Result := '\';
|
|
{$endif}
|
|
{$ifdef unix}
|
|
Result := '/';
|
|
{$endif}
|
|
end;
|
|
|
|
function TCustomShellTreeView.GetRootPath: string;
|
|
begin
|
|
if FRoot <> '' then
|
|
Result := FRoot
|
|
else
|
|
Result := GetBasePath();
|
|
if Result <> '' then
|
|
Result := IncludeTrailingPathDelimiter(Result);
|
|
end;
|
|
|
|
{ Returns true if at least one item was added, false otherwise }
|
|
function TCustomShellTreeView.PopulateTreeNodeWithFiles(
|
|
ANode: TTreeNode; ANodePath: string): Boolean;
|
|
var
|
|
i: Integer;
|
|
Files: TStringList;
|
|
NewNode: TTreeNode;
|
|
begin
|
|
Result := False;
|
|
// avoids crashes in the IDE by not populating during design
|
|
if (csDesigning in ComponentState) then Exit;
|
|
|
|
Files := TStringList.Create;
|
|
try
|
|
GetFilesInDir(ANodePath, AllFilesMask, FObjectTypes, Files, FFileSortType);
|
|
Result := Files.Count > 0;
|
|
|
|
for i := 0 to Files.Count - 1 do
|
|
begin
|
|
NewNode := Items.AddChildObject(ANode, Files.Strings[i], nil); //@Files.Strings[i]);
|
|
NewNode.HasChildren := Files.Objects[i] <> nil; // This marks if the node is a directory
|
|
end;
|
|
finally
|
|
Files.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TCustomShellTreeView.PopulateWithBaseFiles;
|
|
{$if defined(windows) and not defined(wince)}
|
|
const
|
|
DRIVE_UNKNOWN = 0;
|
|
DRIVE_NO_ROOT_DIR = 1;
|
|
DRIVE_REMOVABLE = 2;
|
|
DRIVE_FIXED = 3;
|
|
DRIVE_REMOTE = 4;
|
|
DRIVE_CDROM = 5;
|
|
DRIVE_RAMDISK = 6;
|
|
var
|
|
r: LongWord;
|
|
Drives: array[0..128] of char;
|
|
pDrive: PChar;
|
|
NewNode: TTreeNode;
|
|
begin
|
|
// avoids crashes in the IDE by not populating during design
|
|
if (csDesigning in ComponentState) then Exit;
|
|
|
|
r := GetLogicalDriveStrings(SizeOf(Drives), Drives);
|
|
if r = 0 then Exit;
|
|
if r > SizeOf(Drives) then Exit;
|
|
// raise Exception.Create(SysErrorMessage(ERROR_OUTOFMEMORY));
|
|
|
|
pDrive := Drives;
|
|
while pDrive^ <> #0 do
|
|
begin
|
|
// r := GetDriveType(pDrive);
|
|
|
|
NewNode := Items.AddChildObject(nil, ExcludeTrailingBackslash(pDrive), pDrive);
|
|
NewNode.HasChildren := True;
|
|
|
|
Inc(pDrive, 4);
|
|
end;
|
|
end;
|
|
{$else}
|
|
var
|
|
NewNode: TTreeNode;
|
|
begin
|
|
// avoids crashes in the IDE by not populating during design
|
|
if (csDesigning in ComponentState) then Exit;
|
|
|
|
// This allows showing "/" in Linux, but in Windows it makes no sense to show the base
|
|
if GetBasePath() <> '' then
|
|
begin
|
|
NewNode := Items.AddChild(nil, GetBasePath());
|
|
NewNode.HasChildren := True;
|
|
PopulateTreeNodeWithFiles(NewNode, GetBasePath());
|
|
NewNode.Expand(False);
|
|
end
|
|
else
|
|
PopulateTreeNodeWithFiles(nil, GetBasePath());
|
|
end;
|
|
{$endif}
|
|
|
|
procedure TCustomShellTreeView.DoSelectionChanged;
|
|
begin
|
|
inherited DoSelectionChanged;
|
|
if Assigned(FShellListView) then
|
|
FShellListView.Root := GetPathFromNode(Selected);
|
|
end;
|
|
|
|
function TCustomShellTreeView.GetPathFromNode(ANode: TTreeNode): string;
|
|
begin
|
|
Result := '';
|
|
if ANode <> nil then // Will return the root if nothing is selected (ANode=nil)
|
|
begin
|
|
// Build the path. In the future use ANode.Data instead of ANode.Text
|
|
if (ANode.Parent = nil) and (GetRootPath <> '') then
|
|
//This node is RootNode and GetRooPath contains fully qualified root path
|
|
Result := ''
|
|
else
|
|
Result := ANode.Text;
|
|
while (ANode.Parent <> nil) do
|
|
begin
|
|
ANode := ANode.Parent;
|
|
if (ANode.Parent = nil) and (GetRootPath <> '') then
|
|
//Node.Text of rootnode may not be fully qualified
|
|
Result := GetRootPath + Result
|
|
else
|
|
Result := IncludeTrailingPathDelimiter(ANode.Text) + Result;
|
|
end;
|
|
end;
|
|
if not FilenameIsAbsolute(Result) then
|
|
Result := GetRootPath() + Result; // Include root directory
|
|
end;
|
|
|
|
function TCustomShellTreeView.GetSelectedNodePath: string;
|
|
begin
|
|
Result := GetPathFromNode(Selected);
|
|
end;
|
|
|
|
procedure TCustomShellTreeView.Refresh(ANode: TTreeNode);
|
|
//nil will refresh root
|
|
var
|
|
RootNodeText: String;
|
|
IsRoot: Boolean;
|
|
begin
|
|
if (Items.Count = 0) then Exit;
|
|
//writeln('GetFirstVisibleNode.Text = "',Items.GetFirstVisibleNode.Text,'"');
|
|
IsRoot := (ANode = nil) or ((ANode = Items.GetFirstVisibleNode) and (GetRootPath <> ''));
|
|
//writeln('IsRoot = ',IsRoot);
|
|
if (ANode = nil) and (GetRootPath <> '') then ANode := Items.GetFirstVisibleNode;
|
|
if IsRoot then
|
|
begin
|
|
if Assigned(ANode) then
|
|
RootNodeText := ANode.Text //this may differ from FRoot, so don't use FRoot here
|
|
else
|
|
RootNodeText := GetRootPath;
|
|
//writeln('IsRoot = TRUE, RootNodeText = "',RootNodeText,'"');
|
|
FRoot := #0; //invalidate FRoot
|
|
SetRoot(RootNodeText); //re-initialize the entire tree
|
|
end
|
|
else
|
|
begin
|
|
ANode.Expand(False);
|
|
end;
|
|
end;
|
|
|
|
function TCustomShellTreeView.GetPath: string;
|
|
begin
|
|
Result := GetPathFromNode(Selected);
|
|
end;
|
|
|
|
{
|
|
SetPath: Path can be
|
|
- Absolute like '/usr/lib'
|
|
- Relative like 'foo/bar'
|
|
This can be relative to:
|
|
- Self.Root (which takes precedence over)
|
|
- Current directory
|
|
}
|
|
procedure TCustomShellTreeView.SetPath(AValue: string);
|
|
var
|
|
sl: TStringList;
|
|
Node: TTreeNode;
|
|
i: integer;
|
|
FQRootPath, RelPath: String;
|
|
RootIsAbsolute: Boolean;
|
|
|
|
function GetAdjustedNodeText(ANode: TTreeNode): String;
|
|
begin
|
|
if (ANode = Items.GetFirstVisibleNode) and (FQRootPath <> '') then
|
|
begin
|
|
if not RootIsAbsolute then
|
|
Result := ''
|
|
else
|
|
Result := FQRootPath;
|
|
end
|
|
else Result := ANode.Text;
|
|
end;
|
|
begin
|
|
RelPath := '';
|
|
|
|
//writeln('SetPath: GetRootPath = "',getrootpath,'"',' AValue=',AValue);
|
|
|
|
if (GetRootPath <> '') then
|
|
//FRoot is already Expanded in SetRoot, just add PathDelim if needed
|
|
FQRootPath := IncludeTrailingPathDelimiter(GetRootPath)
|
|
else
|
|
FQRootPath := '';
|
|
RootIsAbsolute := (FQRootPath = '') or (FQRootPath = PathDelim)
|
|
or ((Length(FQRootPath) = 3) and (FQRootPath[2] = ':') and (FQRootPath[3] = PathDelim));
|
|
|
|
//writeln('SetPath: FQRootPath = ',fqrootpath);
|
|
//writeln('SetPath: RootIsAbsolute = ',RootIsAbsolute);
|
|
|
|
//IsRelPath := not FileNameIsAbsolute(AValue);
|
|
|
|
//writeln('SetPath: IsRelPath = ',not FileNameIsAbsolute(AValue));
|
|
|
|
if not FileNameIsAbsolute(AValue) then
|
|
begin
|
|
if DirectoryExistsUtf8(FQRootPath + AValue) then
|
|
begin
|
|
//Expand it, since it may be in the form of ../../foo
|
|
AValue := ExpandFileNameUtf8(FQRootPath + AValue);
|
|
end
|
|
else
|
|
begin
|
|
if not DirectoryExistsUtf8(ExpandFileNameUtf8(AValue)) then
|
|
Raise EInvalidPath.CreateFmt(SShellCtrlsInvalidPath,[ExpandFileNameUtf8(FQRootPath + AValue)]);
|
|
//Directory Exists
|
|
//Make it fully qualified
|
|
AValue := ExpandFileNameUtf8(AValue);
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
//AValue is an absoulte path to begin with
|
|
if not DirectoryExistsUtf8(AValue) then
|
|
Raise EInvalidPath.CreateFmt(SShellCtrlsInvalidPath,[AValue]);
|
|
end;
|
|
|
|
//AValue now is a fully qualified path and it exists
|
|
//Now check if it is a subdirectory of FQRootPath
|
|
RelPath := CreateRelativePath(AValue, FQRootPath, False);
|
|
|
|
//writeln('SetPath: CreaterealtivePath = ',RelPath);
|
|
|
|
if (RelPath <> '') and (RelPath[1] = '.') then
|
|
begin
|
|
// CreateRelativePath retruns a string beginning with ..
|
|
// so AValue is not a subdirectory of FRoot
|
|
Raise EInvalidPath.CreateFmt(SShellCtrlsInvalidPathRelative,[AValue, FQRootPath]);
|
|
end;
|
|
|
|
|
|
sl := TStringList.Create;
|
|
sl.Delimiter := PathDelim;
|
|
sl.StrictDelimiter := True;
|
|
sl.DelimitedText := RelPath;
|
|
if (sl.Count > 0) and (sl[0] = '') then // This happens when root dir is empty
|
|
sl[0] := PathDelim; // and PathDelim was the first char
|
|
|
|
//for i := 0 to sl.Count - 1 do writeln('sl[',i:2,']="',sl[i],'"');
|
|
|
|
|
|
BeginUpdate;
|
|
try
|
|
Node := Items.GetFirstVisibleNode;
|
|
//Root node doesn't have Siblings in this case, we need one level deeper
|
|
if (GetRootPath <> '') and Assigned(Node) then Node := Node.GetFirstVisibleChild;
|
|
|
|
for i := 0 to sl.Count-1 do
|
|
begin
|
|
{
|
|
write('i=',i,' sl[',i,']=',sl[i],' ');
|
|
if Node <> nil then write('GetAdjustedNodeText = ',GetAdjustedNodeText(Node))
|
|
else write('GetAdjustedNodeText = NIL');
|
|
writeln;
|
|
}
|
|
while (Node <> Nil) and
|
|
{$IF defined(CaseInsensitiveFilenames) or defined(NotLiteralFilenames)}
|
|
(Utf8LowerCase(GetAdjustedNodeText(Node)) <> Utf8LowerCase(sl[i]))
|
|
{$ELSE}
|
|
(GetAdjustedNodeText(Node) <> sl[i])
|
|
{$ENDIF}
|
|
do
|
|
begin
|
|
Node := Node.GetNextVisibleSibling;
|
|
{
|
|
write('i=',i,' sl[',i,']=',sl[i],' ');
|
|
if Node <> nil then write('GetAdjustedNodeText = ',GetAdjustedNodeText(Node))
|
|
else write('GetAdjustedNodeText = NIL');
|
|
writeln;
|
|
}
|
|
end;
|
|
if Node <> Nil then
|
|
begin
|
|
Node.Expanded := True;
|
|
Node.Selected := True;
|
|
Node := Node.GetFirstVisibleChild;
|
|
end
|
|
else
|
|
Break;
|
|
end;
|
|
finally
|
|
sl.free;
|
|
EndUpdate;
|
|
end;
|
|
end;
|
|
|
|
|
|
{ TCustomShellListView }
|
|
|
|
procedure TCustomShellListView.SetShellTreeView(
|
|
const Value: TCustomShellTreeView);
|
|
var
|
|
Tmp: TCustomShellTreeView;
|
|
begin
|
|
if FShellTreeView = Value then Exit;
|
|
if FShellTreeView <> nil then
|
|
begin
|
|
Tmp := FShellTreeView;
|
|
FShellTreeView := nil;
|
|
Tmp.ShellListView := nil;
|
|
end;
|
|
|
|
FShellTreeView := Value;
|
|
|
|
if not (csDestroying in ComponentState) then
|
|
Clear;
|
|
|
|
if Value <> nil then
|
|
begin
|
|
FRoot := Value.GetPathFromNode(Value.Selected);
|
|
PopulateWithRoot();
|
|
|
|
// Also update the pair, but only if necessary to avoid circular calls of the setters
|
|
if Value.ShellListView <> Self then Value.ShellListView := Self;
|
|
end;
|
|
|
|
end;
|
|
|
|
procedure TCustomShellListView.SetMask(const AValue: string);
|
|
begin
|
|
if AValue <> FMask then
|
|
begin
|
|
FMask := AValue;
|
|
Clear;
|
|
Items.Clear;
|
|
PopulateWithRoot();
|
|
end;
|
|
end;
|
|
|
|
procedure TCustomShellListView.SetRoot(const Value: string);
|
|
begin
|
|
if FRoot <> Value then
|
|
begin
|
|
//Delphi raises an unspecified exception in this case, but don't crash the IDE at designtime
|
|
if not (csDesigning in ComponentState)
|
|
and (Value <> '')
|
|
and not DirectoryExistsUtf8(ExpandFilenameUtf8(Value)) then
|
|
Raise Exception.CreateFmt(SShellCtrlsInvalidRoot,[Value]);
|
|
FRoot := Value;
|
|
Clear;
|
|
Items.Clear;
|
|
PopulateWithRoot();
|
|
end;
|
|
end;
|
|
|
|
constructor TCustomShellListView.Create(AOwner: TComponent);
|
|
begin
|
|
inherited Create(AOwner);
|
|
|
|
// Initial property values
|
|
ViewStyle := vsReport;
|
|
ObjectTypes := [otNonFolders];
|
|
|
|
Self.Columns.Add;
|
|
Self.Columns.Add;
|
|
Self.Columns.Add;
|
|
Self.Column[0].Caption := 'Name';
|
|
Self.Column[1].Caption := 'Size';
|
|
Self.Column[2].Caption := 'Type';
|
|
// Initial sizes, necessary under Windows CE
|
|
Resize;
|
|
end;
|
|
|
|
destructor TCustomShellListView.Destroy;
|
|
begin
|
|
ShellTreeView := nil;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
procedure TCustomShellListView.PopulateWithRoot();
|
|
var
|
|
i: Integer;
|
|
Files: TStringList;
|
|
NewItem: TListItem;
|
|
CurFileName, CurFilePath: string;
|
|
CurFileSize: Int64;
|
|
begin
|
|
// avoids crashes in the IDE by not populating during design
|
|
if (csDesigning in ComponentState) then Exit;
|
|
|
|
// Check inputs
|
|
if Trim(FRoot) = '' then Exit;
|
|
|
|
Files := TStringList.Create;
|
|
try
|
|
TCustomShellTreeView.GetFilesInDir(FRoot, FMask, FObjectTypes, Files);
|
|
|
|
for i := 0 to Files.Count - 1 do
|
|
begin
|
|
NewItem := Items.Add;
|
|
CurFileName := Files.Strings[i];
|
|
CurFilePath := IncludeTrailingPathDelimiter(FRoot) + CurFileName;
|
|
// First column - Name
|
|
NewItem.Caption := CurFileName;
|
|
// Second column - Size
|
|
// The raw size in bytes is stored in the data part of the item
|
|
CurFileSize := FileSize(CurFilePath); // in Bytes
|
|
NewItem.Data := Pointer(PtrInt(CurFileSize));
|
|
if CurFileSize < 1024 then
|
|
NewItem.SubItems.Add(IntToStr(CurFileSize) + ' bytes')
|
|
else if CurFileSize < 1024 * 1024 then
|
|
NewItem.SubItems.Add(IntToStr(CurFileSize div 1024) + ' kB')
|
|
else
|
|
NewItem.SubItems.Add(IntToStr(CurFileSize div (1024 * 1024)) + ' MB');
|
|
// Third column - Type
|
|
NewItem.SubItems.Add(ExtractFileExt(CurFileName));
|
|
end;
|
|
Sort;
|
|
finally
|
|
Files.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TCustomShellListView.Resize;
|
|
begin
|
|
inherited Resize;
|
|
{$ifdef DEBUG_SHELLCTRLS}
|
|
WriteLn(':>TCustomShellListView.HandleResize');
|
|
{$endif}
|
|
|
|
// The correct check is with count,
|
|
// if Column[0] <> nil then
|
|
// will raise an exception
|
|
if Self.Columns.Count < 3 then Exit;
|
|
|
|
// If the space available is small,
|
|
// alloc a larger percentage to the secondary
|
|
// fields
|
|
if Width < 400 then
|
|
begin
|
|
Column[0].Width := (50 * Width) div 100;
|
|
Column[1].Width := (25 * Width) div 100;
|
|
Column[2].Width := (25 * Width) div 100;
|
|
end
|
|
else
|
|
begin
|
|
Column[0].Width := (70 * Width) div 100;
|
|
Column[1].Width := (15 * Width) div 100;
|
|
Column[2].Width := (15 * Width) div 100;
|
|
end;
|
|
|
|
{$ifdef DEBUG_SHELLCTRLS}
|
|
WriteLn(':<TCustomShellListView.HandleResize C0.Width=',
|
|
Column[0].Width, ' C1.Width=', Column[1].Width,
|
|
' C2.Width=', Column[2].Width);
|
|
{$endif}
|
|
end;
|
|
|
|
function TCustomShellListView.GetPathFromItem(ANode: TListItem): string;
|
|
begin
|
|
Result := IncludeTrailingPathDelimiter(FRoot) + ANode.Caption;
|
|
end;
|
|
|
|
procedure Register;
|
|
begin
|
|
RegisterComponents('Misc',[TShellTreeView, TShellListView]);
|
|
end;
|
|
|
|
end.
|