Recent

Author Topic: [SOLVED] Update TShell(Tree|List)View  (Read 15010 times)

tcrass

  • New Member
  • *
  • Posts: 10
[SOLVED] Update TShell(Tree|List)View
« on: November 03, 2023, 08:45:26 am »
Hello there,

I wonder if there's any way to ask a shell control to update itself in order to reflect changes in the file system -- preserving selection (and in the case of TShellTreeView: expansion) state, if possible?

Cheers -- tcrass
« Last Edit: November 04, 2023, 02:05:55 pm by tcrass »

Bart

  • Hero Member
  • *****
  • Posts: 5573
    • Bart en Mariska's Webstek
Re: Update TShell(Tree|List)View
« Reply #1 on: November 03, 2023, 10:30:42 am »
TShellTreeView has a refresh method, it'll refiil the given node.
I don't think it'll preserve the state of the tree though (did not test that myself though).

Bart

d7_2_laz

  • Hero Member
  • *****
  • Posts: 637
Re: Update TShell(Tree|List)View
« Reply #2 on: November 03, 2023, 11:15:12 am »
The requirement implies that the Shell tools do engage a file system monitor (directory and file watcher) for to have a correct refresh.

I don't think we could expect from the Shell tools to fullfill such a demand - it's up to the user application to have programmed it, such as a file manager.
In consequence there a lot of implications that would need to be programmed, example: why should it be be only uni-directional?
Should we expect that when removing, renaming, adding ... nodes or list items that reflects back to the file system also?
Why not expecting to have a system context menu, and what it should do when executing it's commands?
What’s about drag and drop (from inside as well as from outside the app)?
There would be never an end for the shell tools.
« Last Edit: November 03, 2023, 11:44:48 am by d7_2_laz »
Lazarus 4.0  FPC 3.2.2 Win10 64bit

tcrass

  • New Member
  • *
  • Posts: 10
Re: Update TShell(Tree|List)View
« Reply #3 on: November 03, 2023, 01:55:50 pm »
Quote
TShellTreeView has a refresh method, it'll refiil the given node.
I don't think it'll preserve the state of the tree though (did not test that myself though).

Yes, I've seen TShellTreeView's Refresh() method, but its description suggests that it only triggers a re-draw of any invalidated region -- so this seems to be related to rather low-level component graphics management. I can also confirm that this method doesn't add or remove any nodes in accordance to changes in the file system.

However, there is also a Refresh(TTreeNode) which really seems to cause the passed node to synchronize with the actual file system. I don't know (yet) if this happens recursively, but it's a start and probably for me the way to go.

Quote
The requirement implies that the Shell tools do engage a file system monitor (directory and file watcher) for to have a correct refresh.

Maybe my phrasing was misleading... I didn't mean to ask whether the shell views could be made to continuously monitor the file system, but whether they could be asked to refresh their contents on-demand. But anyway...

Quote
I don't think we could expect from the Shell tools to fullfill such a demand - it's up to the user application to have programmed it, such as a file manager.
In consequence there a lot of implications that would need to be programmed, example: why should it be be only uni-directional?
Should we expect that when removing, renaming, adding ... nodes or list items that reflects back to the file system also?
Why not expecting to have a system context menu, and what it should do when executing it's commands?
What’s about drag and drop (from inside as well as from outside the app)?
There would be never an end for the shell tools.

...I concur that implementing all the different functionalities different people would like to find readily provided by the shell tools would be a bottomless pit.

So I guess I'll just go ahead and write a little method which, in the case of TShellTreeView,
1. takes a snapshot of the view's nodes' expansion state,
2. calls Refresh on the root
3. and recursively restores all the nodes' expansion state and refreshes their children.

Cheers -- tcrass





d7_2_laz

  • Hero Member
  • *****
  • Posts: 637
Re: Update TShell(Tree|List)View
« Reply #4 on: November 03, 2023, 02:14:34 pm »
So a kind of “content refresh” of the whole tree (and list) preserving all expand states as well as selection ..
Sounds realistic that could be done by affordable means, but, just for interest (from a use cases view): from what event should that be triggered?
Lazarus 4.0  FPC 3.2.2 Win10 64bit

wp

  • Hero Member
  • *****
  • Posts: 12865
Re: Update TShell(Tree|List)View
« Reply #5 on: November 03, 2023, 02:50:10 pm »
So a kind of “content refresh” of the whole tree (and list) preserving all expand states as well as selection ..
Sounds realistic that could be done by affordable means, but, just for interest (from a use cases view): from what event should that be triggered?
I'd assume: it should be a method called by the user.

Just checked again, because I always tend to forget these details: ShellTreeView creates only the nodes at the current level. Therefore, if you want to refresh the tree you must iterate only over all *open* nodes and call ShellTreeView.Refresh for these only. This way also the expanded/collapsed state is automatically preserved.

ShellTreeView.Refresh(nil) refreshes the top-level nodes only and collapses the tree. But if you store the path to the selected item in a string before calling ShellTreeView.Refresh and assigning this to the ShellTreeView.Path afterwards the entire path to this node will re-open automatically (of course, any other expanded nodes will collapse). The attached mini project demonstrates this simplified Refresh. It creates a dummy directory "test" in the project folder and updates the tree, and it can also delete this "test" directory and update the tree again.

« Last Edit: November 03, 2023, 02:59:11 pm by wp »

d7_2_laz

  • Hero Member
  • *****
  • Posts: 637
Re: Update TShell(Tree|List)View
« Reply #6 on: November 03, 2023, 02:54:18 pm »
Forgot to mention:
you may take a look onto the possible values of property “ExpandCollapseMode” (*).
Those might help you (collapse a node  in refresh mode, re-open …). But: that would be have effect only  onto already expanded nodes.

(*)  ecmCollapseAndClear, ecmKeepChildren, ecmRefreshedExpanding   // afaik upfrom Laz 3.0
« Last Edit: November 03, 2023, 03:42:03 pm by d7_2_laz »
Lazarus 4.0  FPC 3.2.2 Win10 64bit

tcrass

  • New Member
  • *
  • Posts: 10
Re: Update TShell(Tree|List)View
« Reply #7 on: November 03, 2023, 07:43:35 pm »
Here's what I came up with. My main window has a TShellTreeView, which is named FileTree, and FileList is its associated TShellListView. It wasn't that hard once I understood how walk the tree!

Code: Pascal  [Select][+][-]
  1. procedure TMainWindow.FileTreeRefresh();
  2.  
  3.   procedure RecordNodeState(const node: TTreeNode; const expandedPaths: TStringList);
  4.   var
  5.     currentNode: TTreeNode;
  6.     firstChild: TTreeNode;
  7.   begin
  8.     currentNode := node;
  9.     while currentNode <> nil do
  10.     begin
  11.       if currentNode.Expanded then
  12.       begin
  13.         expandedPaths.Add(FileTree.GetPathFromNode(currentNode));
  14.         firstChild := currentNode.GetFirstChild();
  15.         if firstChild <> nil then
  16.           RecordNodeState(firstChild, expandedPaths);
  17.       end;
  18.       currentNode := currentNode.GetNextSibling();
  19.     end;
  20.   end;
  21.  
  22.   procedure RestoreNodeState(const node: TTreeNode; const refresh: boolean;
  23.   const expandedPaths: TStringList);
  24.   var
  25.     currentNode: TTreeNode;
  26.     firstChild: TTreeNode;
  27.   begin
  28.     currentNode := node;
  29.     while currentNode <> nil do
  30.     begin
  31.       if expandedPaths.IndexOf(FileTree.GetPathFromNode(currentNode)) >= 0 then
  32.       begin
  33.         currentNode.Expanded := True;
  34.         if refresh then
  35.           FileTree.Refresh(currentNode);
  36.         firstChild := currentNode.GetFirstChild();
  37.         if firstChild <> nil then
  38.           RestoreNodeState(firstChild, refresh, expandedPaths);
  39.       end
  40.       else
  41.         currentNode.Expanded := False;
  42.       currentNode := currentNode.GetNextSibling();
  43.     end;
  44.   end;
  45.  
  46. var
  47.   firstNode: TTreeNode;
  48.   selectedPath: string;
  49.   expandedPaths: TStringList;
  50.   selectedFile: string;
  51. begin
  52.   expandedPaths := TStringList.Create();
  53.  
  54.   selectedPath := FileTree.GetPathFromNode(FileTree.Selected);
  55.   if FileList.Selected <> nil then
  56.     selectedFile := FileList.Selected.Caption
  57.   else
  58.     selectedFile := '';
  59.  
  60.   firstNode := FileTree.Items.GetFirstNode();
  61.   RecordNodeState(firstNode, expandedPaths);
  62.   RestoreNodeState(firstNode, True, expandedPaths);
  63.  
  64.   try
  65.     FileTree.Path := selectedPath;
  66.   except
  67.     on EInvalidPath do
  68.       FileTree.Path := '';
  69.   end;
  70.   { Setting the path apparently also expands the selecte node, so
  71.     re-apply the recorded node state, but don't refresh nodes }
  72.   RestoreNodeState(firstNode, False, expandedPaths);
  73.  
  74.   { Force synchronization of associated TShellListView }
  75.   FileTree.ShellListView := nil;
  76.   FileTree.ShellListView := FileList;
  77.   FileList.Selected := FileList.FindCaption(0, selectedFile, False, True, False);
  78.  
  79.   expandedPaths.Free();
  80. end;
  81.  

Edit 2023-11-03T19:58: Also restore selection of item in associated TShellListView
Edit 2023-11-03T20:07: JEDI Code formatter applied
« Last Edit: November 03, 2023, 08:08:35 pm by tcrass »

d7_2_laz

  • Hero Member
  • *****
  • Posts: 637
Re: Update TShell(Tree|List)View
« Reply #8 on: November 03, 2023, 10:07:50 pm »
But, hm, much effort for to have the same as wp’s
Code: Pascal  [Select][+][-]
  1.    ShellTreeView1.Refresh(nil);
  2.    ShellTreeView1.Path := selDir;

Just for interest I did put both ways together on top of wp’s example.
Where I did modify the latter (for to have the same base) in so far that both buttons for to create/remove the project's subdirectroy ‘test’ won’t do the refresh itself, but that would be delegated to a button “Refresh (1)”.
Button “Refresh (2)” alternatively will call your TMainWindow.FileTreeRefresh.
Lazarus 4.0  FPC 3.2.2 Win10 64bit

d7_2_laz

  • Hero Member
  • *****
  • Posts: 637
Re: Update TShell(Tree|List)View
« Reply #9 on: November 03, 2023, 10:31:57 pm »
Correction – no, i see .. if subtrees are expanded somewhere, they will be preserved to be expanded after the “Refresh (2)” and that was the idea.  Sorry!
Lazarus 4.0  FPC 3.2.2 Win10 64bit

tcrass

  • New Member
  • *
  • Posts: 10
Re: Update TShell(Tree|List)View
« Reply #10 on: November 04, 2023, 08:17:16 am »
if subtrees are expanded somewhere, they will be preserved to be expanded after the “Refresh (2)” and that was the idea.

Exactly, that was the idea. I'm quite happy with how it works now!  :)

Cheers -- tcrass

P.S: I'd like to mark this topic as "solved", but I seem to be unable to find the control for editing the topic title...
« Last Edit: November 04, 2023, 08:19:30 am by tcrass »

d7_2_laz

  • Hero Member
  • *****
  • Posts: 637
Re: Update TShell(Tree|List)View
« Reply #11 on: November 04, 2023, 09:59:47 am »
Mark a topic as "solved" :
You can do this via "Modify" of the first post within a thread.
When editing the subject line here, this willl be visible within the forum's lists.
« Last Edit: November 04, 2023, 10:17:48 am by d7_2_laz »
Lazarus 4.0  FPC 3.2.2 Win10 64bit

wp

  • Hero Member
  • *****
  • Posts: 12865
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #12 on: November 04, 2023, 08:12:56 pm »
If you don't mind, I'd like to add your code to the shellctrls unit as "TShellTreeView.UpdateTree" in the following way:

Code: Pascal  [Select][+][-]
  1. procedure TCustomShellTreeView.UpdateTree;
  2.  
  3.   procedure RecordNodeState(const ANode: TTreeNode; const AExpandedPaths: TStringList);
  4.   var
  5.     currentNode: TTreeNode;
  6.     firstChild: TTreeNode;
  7.   begin
  8.     currentNode := ANode;
  9.     while currentNode <> nil do
  10.     begin
  11.       if currentNode.Expanded then
  12.       begin
  13.         AExpandedPaths.Add(GetPathFromNode(currentNode));
  14.         firstChild := currentNode.GetFirstChild();
  15.         if firstChild <> nil then
  16.           RecordNodeState(firstChild, AExpandedPaths);
  17.       end;
  18.       currentNode := currentNode.GetNextSibling();
  19.     end;
  20.   end;
  21.  
  22.   procedure RestoreNodeState(const ANode: TTreeNode; const ARefresh: boolean;
  23.     const AExpandedPaths: TStringList);
  24.   var
  25.     currentNode: TTreeNode;
  26.     firstChild: TTreeNode;
  27.   begin
  28.     currentNode := ANode;
  29.     while currentNode <> nil do
  30.     begin
  31.       if AExpandedPaths.IndexOf(GetPathFromNode(currentNode)) >= 0 then
  32.       begin
  33.         currentNode.Expanded := True;
  34.         if ARefresh then
  35.           Refresh(currentNode);
  36.         firstChild := currentNode.GetFirstChild();
  37.         if firstChild <> nil then
  38.           RestoreNodeState(firstChild, ARefresh, AExpandedPaths);
  39.       end
  40.       else
  41.         currentNode.Expanded := False;
  42.       currentNode := currentNode.GetNextSibling();
  43.     end;
  44.   end;
  45.  
  46. var
  47.   node: TTreeNode;
  48.   topNodePath: String;
  49.   selectedPath: String;
  50.   expandedPaths: TStringList;
  51.   selectedFile: String;
  52.   fileList: TCustomShellListView;
  53. begin
  54.   expandedPaths := TStringList.Create;
  55.   Items.BeginUpdate;
  56.   try
  57.     topNodePath := ChompPathDelim(GetPathFromNode(TopItem));
  58.     selectedPath := GetPathFromNode(Selected);
  59.     if Assigned(FShellListView) and Assigned(FShellListView.Selected) then
  60.       selectedFile := FShellListView.Selected.Caption
  61.     else
  62.       selectedFile := '';
  63.  
  64.     node := Items.GetFirstNode;
  65.     RecordNodeState(node, expandedPaths);
  66.     RestoreNodeState(node, true, expandedPaths);
  67.  
  68.     try
  69.       Path := selectedPath;
  70.       TopItem := Items.FindNodeWithTextPath(topNodePath);
  71.     except
  72.       on EInvalidPath do Path := '';
  73.     end;
  74.  
  75.     { Setting the path apparently also expands the selected node, thus re-apply
  76.       the recorded node state, but don't refresh nodes. }
  77.     //RestoreNodeState(node, False, expandedPaths);     // --- not needed (wp)
  78.  
  79.                                    (*  -- not needed (wp)
  80.     { Force synchronization of associated ShellListView }
  81.     if Assigned(FShellListView) then
  82.     begin
  83.       fileList := FShellListView;
  84.       ShellListView := nil;
  85.       ShellListView := fileList;
  86.       ShellListView.Selected := ShellListView.FindCaption(0, selectedFile, false, true, false);
  87.     end;
  88.     *)
  89.   finally
  90.     Items.EndUpdate;
  91.     expandedPaths.Free;
  92.   end;
  93. end;

Added a BeginUpdate/EndUpdate pair, because I thought there is some flickering. And I added code to restore the top-most item of the tree so that the selected node does not jump away after updating the tree.

As you can see, there are a few commented places which I think contain unnecessary code - please check, or describe exactly how I could see that they are needed.

However, I found a crash when the item to be deleted is exactly the selected node of the tree. Of course, this crash should be fixed before the code can be added to the LCL.

Attaching an updated demo project which contains also a ShellListView.

d7_2_laz

  • Hero Member
  • *****
  • Posts: 637
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #13 on: November 04, 2023, 09:12:50 pm »
 :) :) :)
Lazarus 4.0  FPC 3.2.2 Win10 64bit

wp

  • Hero Member
  • *****
  • Posts: 12865
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #14 on: November 04, 2023, 10:35:35 pm »
Of course, the crash happens only when the application runs inside the IDE... There is a "try-except" block around the critical part which catches and handles the exception. There is not exception outside the IDE.

But, really, I don't like to handle error situations in the LCL this way because most developers will not expect this to happen (just like me...). A better way would be to detect the error condition before the exception is raised...

One possibility would be to make the function "Exists(fn: String): Boolean", which is nested inside SetPath, available as a private or protected method. Then the "try-except" block around setting the Path could be replaced by an "if Exists(selectedPath)":

Code: Pascal  [Select][+][-]
  1. function TCustomShellTreeView.Exists(Fn: String): Boolean;
  2. //Fn should be fully qualified
  3. var
  4.   Attr: LongInt;
  5.   Dirs: TStringList;
  6.   i: Integer;
  7. begin
  8.   Result := False;
  9.   Attr := FileGetAttrUtf8(Fn);
  10.   {$ifdef debug_shellctrls}
  11.   debugln(['TCustomShellTreeView.Exists: Attr = ', Attr]);
  12.   {$endif}
  13.   if (Attr = -1) then Exit;
  14.   if not (otNonFolders in FObjectTypes) then
  15.     Result := ((Attr and faDirectory) > 0)
  16.   else
  17.     Result := True;
  18.   {$ifdef debug_shellctrls}
  19.   debugln(['TCustomShellTreeView.Exists: Result = ',Result]);
  20.   {$endif}
  21. end;
  22.  
  23. procedure TCustomShellTreeView.UpdateTree;
  24. ...
  25.     if Exists(selectedPath) then
  26.     begin
  27.       Path := selectedPath;
  28.       TopItem := Items.FindNodeWithTextPath(topNodePath);
  29.     end;  
  30. ...

This works for me.

 

TinyPortal © 2005-2018