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