Sorted ShellTreeView with non-default root reports error when user clicks on the root node.
* Lazarus/FPC Version: Lazarus 4.99 (rev main_4_99-1022-g9b4c81b56b) FPC 3.2.2 i386-win32-win32/win64 (bug exists at least since Laz 3.0)
* Operating System: Windows 11 (but others are affected, too)
* CPU / Bitness: Intel, 32 or 64 bit, not important
## What happens
The items in a ShellTreeView can be sorted. Under the following conditions an error occurs:
* The `Root` of the tree is not set to the root of the file system, but to a folder higher in the hierarchy, in this case C:\Windows\System32
* The tree is sorted
* The user clicks on the top-level node (C:\Windows\System32)
The error message is _"The selected item does not exist on disk:"C:\Windows\System32"; In file 'shellctrls.pas' at line 1158"_.
See also forum post https://forum.lazarus.freepascal.org/index.php/topic,61347.msg545078.html#msg545078
## What did you expect
No error
## Steps to reproduce
* Run the attached demo project which displays the file system entries starting at `Root`=C:\Windows\System32 (or `Root`=/usr/share/fonts on Linix).
* Click on one of the sorting options, e.g. "by name".
* Click on the root node --> error message saying that this folder does not exist (although it definitely does).
[41373_-_sorted_shelltreeview_error_non-default_root.zip](/uploads/2d607c0551bb14b6c05df5c727476780/41373_-_sorted_shelltreeview_error_non-default_root.zip)
## Analysis ##
When the user clicks on another tree node the method `DoSelectionChanged`
````pascal
procedure TCustomShellTreeView.DoSelectionChanged;
...
begin
...
CurrentNodePath := ChompPathDelim(GetPathFromNode(ANode));
if TShellTreeNode(ANode).IsDirectory then
...
else
begin
if not FileExistsUtf8(CurrentNodePath) then
Raise EShellCtrl.CreateFmt(sShellCtrlsSelectedItemDoesNotExists,[CurrentNodePath]);
...
end;
end;
end;
````
is called, and this is where the exception is raised when the click is on the top-level node. The debugger shows that `TShellTreeNode(ANode).IsDirectory` fails to detect that the root node is a regular folder of the file system.
The nodes of `TShellTreeView` are created as instances of `TShellTreeNode` which stores the `TSearchRec` data found during population of the tree in an extra field `FFileInfo`:
````pascal
type
TShellTreeNode = class(TTreeNode)
...
protected
FFileInfo: TSearchRec;
public
function IsDirectory: Boolean;
...
end;
function TShellTreeNode.IsDirectory: Boolean;
begin
Result := ((FFileInfo.Attr and faDirectory) > 0);
end;
````
The information whether the node represents a directory or not is taken from the `FFileInfo` record.
The debugger shows that the `FFileInfo` record of a non-default root node contains just initialized zero values, no real data. The `FFileInfo` elements, however, had been correctly set for all nodes when the tree was populated, e.g.
````pascal
// comments removed for clarity
procedure TCustomShellTreeView.PopulateWithBaseFiles;
....
NewNode := Items.AddChildObject(nil, ExcludeTrailingBackslash(pDrive), pDrive);
TShellTreeNode(NewNode).FFileInfo.Name := ExcludeTrailingBackslash(pDrive);
TShellTreeNode(NewNode).FFileInfo.Attr := faDirectory + faSysFile{%H-} + faHidden{%H-};
TShellTreeNode(NewNode).SetBasePath('');
function TCustomShellTreeView.PopulateTreeNodeWithFiles(
ANode: TTreeNode; ANodePath: string): Boolean;
...
NewNode := Items.AddChildObject(ANode, Files[i], nil);
TShellTreeNode(NewNode).FFileInfo := TFileItem(Files.Objects[i]).FileInfo;
TShellTreeNode(NewNode).SetBasePath(TFileItem(Files.Objects[i]).FBasePath);
procedure TCustomShellTreeView.SetRoot(const AValue: string);
...pascal
RootNode := Items.AddChild(nil, AValue);
TShellTreeNode(RootNode).FFileInfo.Attr := FileGetAttr(FRoot);
TShellTreeNode(RootNode).FFileInfo.Name := FRoot;
````
But when the tree is to be sorted all items are destroyed (along with their `FFileInfo` records) and recreated without assigning the value `FFileInfo`data again:
````pascal
procedure TCustomShellTreeView.SetFileSortType(const AValue: TFileSortType);
...
begin
RootNode := Items.AddChild(nil, FRoot);
RootNode.HasChildren := True;
RootNode.Expand(False);
if ExistsAndIsValid(CurrPath) then
SetPath(CurrPath);
end;
````
(The same in TCustomShellTreeView.SetOnSortCompare)
## Patch ##
When the `FFileInfo` of the root node is recreated and assigned to the root node in `SetFileSortType` and `SetOnSortCompare` the exception is not fired any more.
Since the (non-default) root is created at three different places I added a private method `CreateRootNode` with calls `FindFirst` with the directory name to determine the `TSearchRecord`. This is assigned to the `FFileIno` record of the `TShellTreeNode`. Finally `CreateRootNode` is called in `SetRoot`, `SetFileSortType` and `SetOnSortCompare` instead of `RootNode := Items.AddChild(nil, FRoot)`.
````pascal
function TCustomShellTreeView.CreateRootNode(const APath: string): TTreeNode;
var
dirInfo: TSearchRec;
begin
Result := Items.AddChild(nil, APath);
TShellTreeNode(Result).SetBasePath('');
FindFirstUTF8(APath, faAnyFile, dirInfo);
TShellTreeNode(Result).FFileInfo := dirInfo;
FindCloseUTF8(dirInfo);
Result.HasChildren := True;
Result.Expand(False);
end;
procedure TCustomShellTreeView.SetRoot(const AValue: string);
var
RootNode: TTreeNode;
...
FRoot := ExpandFileNameUtf8(FRoot);
RootNode := CreateRootNode(AValue);
...
procedure TCustomShellTreeView.SetFileSortType(const AValue: TFileSortType);
...
RootNode := CreateRootNode(FRoot);
...
procedure TCustomShellTreeView.SetOnSortCompare(AValue: TFileItemCompareEvent);
RootNode := CreateRootNode(FRoot);
...
````
See attached patch. [41373-shelltreeview.diff](/uploads/542f8c95b7ff1185f811132ae323be90/41373-shelltreeview.diff)
issue