Recent

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

d7_2_laz

  • Hero Member
  • *****
  • Posts: 649
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #30 on: November 13, 2023, 07:35:35 pm »
“dirnode_S_is_Selected” is selected in this case, so it equals the listview’s FRoot.
 “dirnode_A” and “dirnode_B” have changes and for those nodes resp. their paths
UpdateView will  be called in this example.
As a real image is better, so one below  (the names are a bit different).

The relevant part is only that those latter paths (A, B) are outside of the hierarchy of the selected path, so that from the beginning  it is clear that a listview update would have no meaning.

« Last Edit: November 13, 2023, 07:37:11 pm by d7_2_laz »
Lazarus 4.4  FPC 3.2.2 Win10 64bit

wp

  • Hero Member
  • *****
  • Posts: 13264
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #31 on: November 13, 2023, 07:52:19 pm »
Thank you, now I understand. Committed your code.

d7_2_laz

  • Hero Member
  • *****
  • Posts: 649
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #32 on: November 13, 2023, 08:09:36 pm »
That was the last missing piece of the puzzle imo, now i think it's perfect.  Thank you!
Lazarus 4.4  FPC 3.2.2 Win10 64bit

jcmontherock

  • Sr. Member
  • ****
  • Posts: 322
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #33 on: November 13, 2023, 10:20:04 pm »
Could you give us a complete and functional example ?
Windows 11 UTF8-64 - Lazarus 4.4-64 - FPC 3.2.2

wp

  • Hero Member
  • *****
  • Posts: 13264
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #34 on: November 14, 2023, 12:03:36 am »
A demo is in reply #26.

d7_2_laz

  • Hero Member
  • *****
  • Posts: 649
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #35 on: November 14, 2023, 12:49:59 am »
For the today’s update you can do as additional check:
Create a test folder somewhere and add this in the treeview's refresh routine. Something like:

Code: Pascal  [Select][+][-]
  1. procedure TForm1.btnTreeUpateviewClick(Sender: TObject);
  2. begin
  3.  
  4.  ShellTreeView1.UpdateView(ShellTreeView1.GetPathFromNode(ShellTreeView1.Selected));
  5.  
  6.  ShellTreeView1.UpdateView(' D:\__Temp\myTestfolder');   // Very statically, just for test
  7. end;

Start the program.  Expand the parent dir of your test folder, but select the project folder.
Both should be expanded.

Now you simulate a folder watcher's result processing manually;
Create some subfolder beyond your test folder from outside the app, eg. Windows Explorer
Now press "ShellTreeView.UpdateView".
---> Check if you see that your newly created subfolder does appear.

This simulation does mean:
if someone (folder watcher) would report a specific change to you, you can do a specific tree refresh action for it without having involved the list view for that.
That was not possible before (Edit: not without a repeated listview's population for it's same FRoot).
« Last Edit: November 14, 2023, 12:58:44 am by d7_2_laz »
Lazarus 4.4  FPC 3.2.2 Win10 64bit

wp

  • Hero Member
  • *****
  • Posts: 13264
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #36 on: November 14, 2023, 11:31:11 am »
Must revisit this issue because it was pointed out in the GitLab commit (https://gitlab.com/freepascal.org/lazarus/lazarus/-/commit/cd92f94f6b5f57f0b3ca9d8c3a94fad994fa3665) that a substring comparison is not appropriate here. The following code (not comitted yet) runs up the subtree of selected node towards root and checks whether the StartDir is along the way. If not, there is no need to update the listview. Any logical flaws in there?

Code: Pascal  [Select][+][-]
  1. { Rebuilds the tree for all expanded nodes from the node corresponding to
  2.   AStartDir (or from root if AStartDir is empty) to react on changes in the
  3.   file system. Collapsed nodes will be updated anyway when they are expanded. }
  4. procedure TCustomShellTreeView.UpdateView(AStartDir: String = '');
  5.  
  6.   procedure RecordNodeState(const ANode: TTreeNode; const AExpandedPaths: TStringList);
  7.   var
  8.     currentNode: TTreeNode;
  9.     firstChild: TTreeNode;
  10.   begin
  11.     currentNode := ANode;
  12.     while currentNode <> nil do
  13.     begin
  14.       if currentNode.Expanded then
  15.       begin
  16.         AExpandedPaths.Add(GetPathFromNode(currentNode));
  17.         firstChild := currentNode.GetFirstChild();
  18.         if firstChild <> nil then
  19.           RecordNodeState(firstChild, AExpandedPaths);
  20.       end;
  21.       currentNode := currentNode.GetNextSibling();
  22.     end;
  23.   end;
  24.  
  25.   procedure RestoreNodeState(const ANode: TTreeNode; const ARefresh: boolean;
  26.     const AExpandedPaths: TStringList);
  27.   var
  28.     currentNode: TTreeNode;
  29.     firstChild: TTreeNode;
  30.   begin
  31.     currentNode := ANode;
  32.     while currentNode <> nil do
  33.     begin
  34.       if AExpandedPaths.IndexOf(GetPathFromNode(currentNode)) >= 0 then
  35.       begin
  36.         currentNode.Expanded := True;
  37.         if ARefresh then
  38.           Refresh(currentNode);
  39.         firstChild := currentNode.GetFirstChild();
  40.         if firstChild <> nil then
  41.           RestoreNodeState(firstChild, ARefresh, AExpandedPaths);
  42.       end
  43.       else
  44.         currentNode.Expanded := False;
  45.       currentNode := currentNode.GetNextSibling();
  46.     end;
  47.   end;
  48.  
  49. var
  50.   node: TTreeNode;
  51.   firstNode: TTreeNode;
  52.   startNode: TTreeNode;
  53.   topNodePath: String;
  54.   selectedPath: String;
  55.   selectedWasExpanded: Boolean = false;
  56.   expandedPaths: TStringList;
  57.   listviewRefreshNeeded: Boolean;
  58. begin
  59.   if FUpdateLock <> 0 then
  60.     exit;
  61.  
  62.   expandedPaths := TStringList.Create;
  63.   Items.BeginUpdate;
  64.   try
  65.     topNodePath := ChompPathDelim(GetPathFromNode(TopItem));
  66.     selectedPath := GetPathFromNode(Selected);
  67.     if Assigned(Selected) then
  68.       selectedWasExpanded := Selected.Expanded;
  69.  
  70.     firstNode := Items.GetFirstNode;
  71.     if AStartDir = '' then
  72.     begin
  73.       startNode := firstNode;
  74.       listViewRefreshNeeded := true;
  75.     end else
  76.     begin
  77.       startNode := Items.FindNodeWithTextPath(ChompPathDelim(AStartDir));
  78.       // Avoid starting at a non-existing folder
  79.       while not Exists(GetPathFromNode(startNode)) and (startNode <> firstNode) do
  80.         startNode := startNode.Parent;
  81.       // Find out whether the StartDir is in the subtree ending at the selected
  82.       // node. In this case the listview must be refreshed, too.
  83.       node := Selected;
  84.       while (node <> startNode) and (node <> nil) do
  85.         node := node.Parent;
  86.       listviewRefreshNeeded := (node <> nil);
  87.     end;
  88.  
  89.     RecordNodeState(startNode, expandedPaths);
  90.     RestoreNodeState(startNode, true, expandedPaths);
  91.  
  92.     if Exists(selectedPath) then
  93.     begin
  94.       Path := selectedPath;
  95.       // Setting the path expands the selected node --> apply the stored state.
  96.       Selected.Expanded := selectedWasExpanded;
  97.       // Avoid selected node to scroll away.
  98.       TopItem := Items.FindNodeWithTextPath(topNodePath);
  99.     end;
  100.  
  101.     // Force synchronization of associated ShellListView, but only if the
  102.     // refresh affects the selected tree node.
  103.     if Assigned(FShellListView) and listViewRefreshNeeded then
  104.     begin
  105.       inc(FUpdateLock);
  106.       try
  107.         FShellListView.UpdateView;
  108.       finally
  109.         dec(FUpdateLock);
  110.       end;
  111.     end;
  112.   finally
  113.     Items.EndUpdate;
  114.     expandedPaths.Free;
  115.   end;
  116. end;
« Last Edit: November 14, 2023, 12:12:38 pm by wp »

d7_2_laz

  • Hero Member
  • *****
  • Posts: 649
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #37 on: November 14, 2023, 02:39:00 pm »
Hm, for to go more sure about the logic, i tried some cases accomplished by debug prints.
Cases:
Test 1: “startNode” nested downwards within the hierarchy of “Selected” (a: one level b. two levels)
Test 2: startNode equals Selected
Test 3: startNode somewhat upwards above Selected (but being within the same hierarchy line/path)
Test 4: startNode being within a completely different tree path

All ok, little remark about Test 3: reports 'listviewRefreshNeeded true' although, strictly seen, startNode's content is not visible in the listview.
Sample
Path Selected:    D:\__Temp\tests\shelltreeview_refresh\shelltreeview_refresh--wp-3
Path startNode:  D:\__Temp\tests\shelltreeview_refresh
*But* i'd think it’s not worthy to pursue this as it is irrelevant from a practical point of view; I’d simply ignore. Really relevant imo are different tree paths.
It's very goood imo.
Lazarus 4.4  FPC 3.2.2 Win10 64bit

wp

  • Hero Member
  • *****
  • Posts: 13264
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #38 on: November 14, 2023, 04:08:38 pm »
This is twisting my brain...

What do you mean with "downwards" and "updwards"? Which one is "towards the root", which one is "away from it"?

Clearly, when StartDir is equal to the SelectedNode the Listview must be updated. When StartDir is in one of the children of the SelectedNode (away from root), nothing needs to be done, because these changes will not be seen in the listview. The other direction (back towards root) is more difficult: When one of the selected node's parents has been deleted and nothing is selected the Listview must be updated as well (it will become empty then). But if there is code which selects another node in such a case the listview will update anyway. And when new files/folders are added in one of the parents, this will not be seen by the listview. The same when one of the parents is renamed.

Therefore, I'd suggest to define listViewRefreshNeeded only as "StartDir = " or "StartNode <> SelectedNode". Or am I missing a case again?

d7_2_laz

  • Hero Member
  • *****
  • Posts: 649
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #39 on: November 14, 2023, 05:56:58 pm »
Oh sorry… with “upwards” I meant: iterating from deeper nested node level in direction to the top node / root, with “downwards”: iterating from the root in direction to the bottom. And sorry, I hadn’t really so much the case “one of the selected node's parents has been deleted” in focus.

Anyhow, to my opinion your proposal from this morning is fully good and could be kept; maybe I didn’t express it clearly enough due to the remark.
About the last mentioned suggestion I’d need to re-test and think about a bit; I’d come back to it later the day.
Lazarus 4.4  FPC 3.2.2 Win10 64bit

d7_2_laz

  • Hero Member
  • *****
  • Posts: 649
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #40 on: November 14, 2023, 08:02:16 pm »
Hm, must not it be "StartNode = SelectedNode" ?

Code: Pascal  [Select][+][-]
  1.     listViewRefreshNeeded := ((AStartDir = '') or (StartNode = Selected));
  2.     if Assigned(FShellListView) and listViewRefreshNeeded then

The Test 1 to Test 5 (as above) would behave ok so far.
But doesn't it sound quite too easy to be true, that only 'StartNode' must equal to 'Selected' to be sufficient for to force a listview refresh here? So, there should remain very few cases to expect when this refresh will happen when working with StartDir. Uh. But maybe it's simply the truth.

From the scenario: "delete or rename a parent of the select node, or add childs on the parent"
i yet could try only the "delete or rename a parent of Selected".
Here the listview would need an update, because Selected gots nil. But it wasn't updated (as listViewRefreshNeeded was false).

This needs to be integrated, eg. via:
Code: Pascal  [Select][+][-]
  1.     listViewRefreshNeeded :=  ( (AStartDir = '') or ((Selected = nil) Or (StartNode = Selected)));
but i'm not sure if this can be a general rule (within UpdateView, ever update the listview if no tree node is selected).
Edit: the sub-condition "Seleted = nil" would cause a listview refresh again for any subsquent call of tree.UpdateView using the startDir parameter, not nice.
« Last Edit: November 14, 2023, 08:21:48 pm by d7_2_laz »
Lazarus 4.4  FPC 3.2.2 Win10 64bit

d7_2_laz

  • Hero Member
  • *****
  • Posts: 649
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #41 on: November 14, 2023, 08:35:28 pm »
Maybe then as such:

Code: Pascal  [Select][+][-]
  1.     listViewRefreshNeeded :=  ( (AStartDir = '') or (StartNode = Selected) );
  2.     if ((Selected = nil) And Assigned(FShellListView)) then
  3.        listViewRefreshNeeded := (FShellListView.Items.Count> 0);
Lazarus 4.4  FPC 3.2.2 Win10 64bit

d7_2_laz

  • Hero Member
  • *****
  • Posts: 649
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #42 on: November 17, 2023, 02:14:26 pm »
Independent on which one of the different variations will be kept, during my tests i had noticed a crash when a user program does  subsequent (second, third ..) call of tree.UpdateView for a folder that had been removed/renamed meanwhile.
Sample:
  tree.UpdateView('D:\basepath\subdir\workarea1');
  tree.UpdateView('D:\basepath\subdir\workarea2');   // Crashes when "subdir" does no longer exist.

It crashes after - as from current version in 'main', line 1300 – in the while loop:
Code: Pascal  [Select][+][-]
  1.    node := Items.FindNodeWithTextPath(ChompPathDelim(AStartDir));
  2.    while not Exists(GetPathFromNode(node)) and (node <> firstNode) do
  3.          node := node.Parent;

Of course one could ask for "node<>nil" here before doing the while loop. But the further logic based on nil would behave somehow unintended.

Possibly it's more safe to consolidate the path string before retrieving the node, instead of iterating along the nodes.
Code: Pascal  [Select][+][-]
  1.    node := Items.FindNodeWithTextPath(FindExistingPathPartInPathString(AStartDir));
  2.    if node = nil then  // occurs now only if path contains an unplugged drive
  3.       node := firstNode
  4. ....
  5. // Small helper procedure:
  6. function FindExistingPathPartInPathString(APath: String): String;
  7. begin
  8.    Result := ChompPathDelim(APath);
  9.    while ((Not Exists(Result)) And (Length(Result) > 3)) do    // stop at drive identifier
  10.        Result := ChompPathDelim(ExtractFileDir(Result));
  11.   if Not Exists(Result) then Result := '';   // return '' for a not existing drive
  12. end;
  13.  
« Last Edit: November 17, 2023, 02:18:44 pm by d7_2_laz »
Lazarus 4.4  FPC 3.2.2 Win10 64bit

wp

  • Hero Member
  • *****
  • Posts: 13264
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #43 on: November 17, 2023, 05:09:33 pm »
Code: Pascal  [Select][+][-]
  1.    while ((Not Exists(Result)) And (Length(Result) > 3)) do    // stop at drive identifier
  2.     ...
  3.  
There are no drive identifiers in Linux and Mac.

How about this?
Code: Pascal  [Select][+][-]
  1.   function FindExistingSubPath(APath: String): String;
  2.   var
  3.     sa: TStringArray;
  4.     path: String;
  5.     i: Integer;
  6.   begin
  7.     sa := APath.Split(PathDelim);
  8.     Result := sa[0];
  9.     for i := 1 to High(sa) do
  10.     begin
  11.       path := AppendPathDelim(Result) + sa[i];
  12.       if not Exists(path) then
  13.         Break;
  14.       Result := path;
  15.     end;
  16.     Result := ChompPathDelim(Result);
  17.   end;
  18. ...
  19.   startNode := Items.FindNodeWithTextPath(FindExistingSubPath(AStartDir));
  20.  
« Last Edit: November 17, 2023, 06:20:14 pm by wp »

d7_2_laz

  • Hero Member
  • *****
  • Posts: 649
Re: [SOLVED] Update TShell(Tree|List)View
« Reply #44 on: November 17, 2023, 08:10:37 pm »
Oh, too late :-)  …i was just to post a response and code update, as I saw your Edit/proposal.

"No drive identifiers in Linux and Mac." ... right, wasn't aware.
Reason i did so (lenght > 3) was that i tried to avoid an endless loop on the drive letter, especially an invalid one (path with a not plugged drive).

Would that alternative work for Linux and Mac?
Code: Pascal  [Select][+][-]
  1.   // Small helper function:
  2.   function FindExistingPathPartInPathString(APath: String): String;
  3.   var S : String;
  4.   begin
  5.       Result := ChompPathDelim(APath);
  6.       while ((Not Exists(Result)) And (Result <> '')) do begin
  7.          S := ChompPathDelim(ExtractFileDir(Result));
  8.          if S = Result then   // Nothing chomped, limit reached. Case not existing drive (Windows)
  9.                Result := ''
  10.          else  Result := S;
  11.       end;
  12.   end;

Your variation looks better to me as working with the splitted elements is more intuitive than using 'ExtractFileDir' even on folders.
Functionally it differs from mine in so far as for not existing paths it returns eg "C:", "H:" (fully correct) whereas mine would return e.g. "C:\" for an existing drive and an empty string for a path based on an invalid drive. For the invalid drive then the using code could switch to a default node instead. No big effect though, and i cannot test on Mac and Linux.

==>  Let’s take you variation which is more straight and probably cross.-platform?
Lazarus 4.4  FPC 3.2.2 Win10 64bit

 

TinyPortal © 2005-2018