Skip to content

Open folders in place in the list view - #113

Open
rustytensor wants to merge 1 commit into
omacom:mainfrom
rustytensor:upstream-tree
Open

rustytensor wants to merge 1 commit into
omacom:mainfrom
rustytensor:upstream-tree

Conversation

@rustytensor

Copy link
Copy Markdown

The list view can now show a tree: a folder opens right where it is, its items below it, one step further in. It's off by default, and until you turn it on the list is exactly what it is today.

Why

To see what's in a few folders side by side, the list makes you go into each one and back out, and the columns only show one path at a time. A tree keeps the folder you're in on screen while you look inside the folders under it.

It's off by default because Folio's list is simple on purpose, and plenty of people won't want arrows and indents in it. With the tree off, there's no arrow, no indent and no line in the rows, the name sits where it always has, and → and ← still go into a folder and back out. Nothing changes for anyone who doesn't press T.

What you'd notice

With the tree on (T in the list, or the new tree button beside the view button, which only shows in the list and lights while the tree is on):

  • Each folder row has a small arrow. Clicking it, or →, opens the folder in place, and the arrow points down. → on an open folder steps to its first item.
  • ← closes the folder under the cursor, or steps out to the folder it's in. At the top level, ← goes up a folder as it always has. ⏎ still goes into a folder.
  • Rows in open folders are joined by thin lines, the way tree draws them. Right to left, the arrows and the lines turn round with the row.
  • The items inside an open folder are ordinary rows. Selecting, copying, deleting, renaming and dragging them works as on any other row.
  • Open folders are watched. A file added or removed in one shows up in place, without the list jumping back to the top or the cursor leaving its item. Changing the sort order re-sorts the open folders too.
  • Closing a folder lets go of anything selected inside it, so a copy or a delete never takes something you can no longer see. If the cursor was inside, it goes to the folder.
  • Searches, Recents and plugins' views stay flat. Switching to the grid or the columns closes what was open, and the cursor goes to the top-level folder its item was in.
  • T says "Tree on" or "Tree off" in the banner. The choice is remembered with the view, like the zoom.

Keys

Key Does
T Tree in the list, on or off
→ / ← in the list, with the tree on Open a folder in place / close it or step out

Both are in the README and the ? overlay. The new strings are in en.json.

What changed

  • FolderModel keeps the folder's own items apart from the rows it shows. expand(), collapse(), collapseAll() and parentRow() open and close folders, and three new roles (depth, open, lines) tell the row how far in it is and which lines to draw. Open folders are listed in the background and watched on their own, so a change in one re-reads only that folder.
  • Rows come and go by removing and inserting only the stretch that changed, in one go. Opening a folder of 20,000 items above 20,000 more doesn't hold the window up: the longest gap between 10 ms timer ticks during that is checked to stay under 250 ms.
  • indexOf(name) only looks at the folder's own items, so a pasted or renamed file is found in the folder shown, not under the same name in an open folder.
  • FolderList gets a tree property, the new arrow-key behaviour, and puts the cursor back on its item when rows shift around it. FileRow draws the arrow and the lines only when tree is set. FolderPane adds T and the tree button, and closes open folders when you leave the list. Main.qml keeps treeView in its Settings, off by default.
  • Windows takes the folder Main.qml is read from (the app's resources by default), so a test can open Folio's own window from the source folder.

Tested

On Fedora 45 in a clean build root, built with the project's -Wall -Wextra: no warnings.

folio_tests: 197 passed, 12 skipped, 2 failed. Main passes 182 on the same root. The 15 new tests are below. The two failures are the same ones main has there (kindsFromMimeTypes, previewerPicksAViewerForEachType, Fedora's MIME type for shell scripts).

  • treeIsOffUntilTurnedOn: with the tree off, no arrows, lines or indent, the name at the same 46 px as on main, and → goes into the folder. Turned on, → opens in place; turned off again, everything closes and the cursor lands on the folder.
  • treeOpensFoldersInPlace, treeKeepsOpenFoldersThroughChanges, treeLetsGoOfHiddenSelection, treeStaysFlatWhereItCannotNest, treeFindsNamesInTheFolderShown: the rows, depths and open folders for a small fixed folder, through edits on disk, a re-read, a search, Recents and moving to another folder.
  • treeListKeys, treeArrowKeepsTheCursorOnTheFolder, treeKeepsTheSelectionAsRowsArrive, treeKeepsTheCursorThroughANewOrder: where the cursor and the selection end up after each key, click and change, mirrored too.
  • treeDrawsItsLines, treeLinesTurnRoundRightToLeft: the exact lines for each row, drawn in the list only (never in the columns), and their position left to right and right to left.
  • treeOpensALargeFolderWithoutFreezing: the 20,000-item case above.
  • treeKeyAndButton, viewButtonCyclesViews: T and the button in the list, nothing in the grid or the columns, and the view button still just cycles grid, list, columns.

This doesn't depend on the pane widths PR, and either can go in first. They edit neighbouring lines in the same files, and both add the same test helper for opening Folio's real window, so whichever lands second will need a rebase. I'll do that rebase.

The list can now show a tree: with it on, → opens the folder under the cursor right below it and ← closes it again (or steps out to the folder it's in, and at the top goes up as before). Each open folder's items are ordinary rows, one step further in and joined by thin lines like the tree command draws them, so selection, copy, delete and rename work on them as on any other row. Open folders are listed in the background and watched, so what changes in them shows without the rest of the list jumping, and closing one lets go of anything selected inside it.

The tree is off until T is pressed in the list, or the tree button beside the view button is clicked, and the choice is remembered with the view. Off, the list is exactly what it was. Searches, Recents and plugins' views stay flat, and switching to the grid or the columns closes what was open.
@greptile-apps

greptile-apps Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The change affects folder browsing in the list, while other views remain flat.

Fix context-menu actions on nested selections before merging.

Findings

  1. P1 Menus skip selected files ▶
  2. P2 Rename loses the selected file ▶
  3. P2 Folder arrows do nothing ▶
  4. P2 Open folders repeat count work ▶

Summary

Adds an optional, remembered tree to the list view:

  • Folio’s list can open folders in place as an optional tree.
  • Tests can open Folio’s window using QML from the source folder.

The nested-row integration needs fixes for context-menu selection. Other findings cover rename cursor recovery, inactive arrows in flat views and repeated folder counting.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Toggle["T or tree button"] --> Choice["Remember tree choice"]
  Choice --> List["FolderList"]
  List --> Expand["Expand folder"]
  Expand --> Read["Read child folder in background"]
  Read --> Rows["Insert nested rows"]
  Rows --> Watch["Watch open folders"]
  Watch --> Read
  List --> Close["Collapse folder or leave list"]
  Close --> Prune["Remove nested rows and hidden selection"]
Loading

Reviews (1) · Last reviewed commit: "Open folders in place in the list view" · Reviewed by Greptile

Comment thread src/foldermodel.cpp
int FolderModel::indexOf(const QString &name) const {
for (int row = 0; row < m_entries.size(); ++row)
if (m_entries[row].name == name)
if (m_entries[row].depth == 0 && m_entries[row].name == name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Menus skip selected files

indexOf() now skips nested rows, but FileMenu.forSelection still uses it to check whether the clicked item belongs to the shared selection. Select two nested files whose names do not appear at the top level, then right-click either one: Copy, Cut and Delete receive only that file. Keep the top-level lookup for pasted names, but change the menu's membership check to use indexOfPath(target.path).

Knowledge Base Used:

Comment thread src/FolderList.qml
Comment on lines +52 to +54
for (let up = list.shiftingPath; row < 0 && up.startsWith(shown);) {
up = up.substring(0, up.lastIndexOf("/"))
row = folder.indexOfPath(up)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Rename loses the selected file

Renaming a nested file moves the cursor to its parent instead of keeping the renamed file selected. FileOps::rename() reports the actual parent folder, but FolderPane.onDone ignores that result because it differs from backend.path. When the child listing arrives, this fallback treats the old name as a removed file and selects its parent. Follow completed changes inside open folders by their full destination path before using the parent fallback.

Knowledge Base Used: Folder view layouts

Comment thread src/FolderList.qml
delegate: FileRow {
width: list.width
detailed: true
tree: list.tree

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Folder arrows do nothing

Folder arrows remain visible in search results and Recents when the tree is on, but clicking them does nothing. The delegate receives list.tree without checking whether the current model can nest, while expand() rejects both views. Hide the tree controls there, or give the arrow the same navigation fallback as the right-arrow key.

Comment thread src/foldermodel.cpp
// Its items show first and its folders are counted after, as for
// the folder's own: thousands of folders on a slow drive would
// otherwise keep it open and empty for as long as counting takes.
countFolders(m_generation);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Open folders repeat count work

Each child listing starts another count pass over every visible folder whose count is still unknown. Counts are written back only when a whole pass finishes, so opening several folders or refreshing a tree on a slow mount queues repeated scans of the same folders. These jobs share the same generation and do not cancel each other. Track counts already being read, or use one count worker that picks up newly arrived folders.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant