Skip to content

fix(search): restore the column show/hide menu and shift+page selection - #805

Merged
got3nks merged 9 commits into
amule-org:masterfrom
got3nks:fix/search-column-menu
Aug 5, 2026
Merged

fix(search): restore the column show/hide menu and shift+page selection#805
got3nks merged 9 commits into
amule-org:masterfrom
got3nks:fix/search-column-menu

Conversation

@got3nks

@got3nks got3nks commented Aug 5, 2026

Copy link
Copy Markdown

Two more behaviours the wxDataViewCtrl port left behind, both of which every list still on wxListCtrl inherits for free from CMuleListCtrl. Same class as the three restored in #796 (multi-selection, type-to-select, select-all) and the reason #801 argues for a shared base before the next list is ported.

Column show/hide menu

Right-clicking a column header opened a check-menu of columns (CMuleListCtrl::OnColumnRClick/OnMenuSelected). The ported search list bound no header right-click handler at all, so there was no way to hide a column.

Restored on CSearchListCtrl with the original semantics: hiding sets the column to COL_SIZE_MIN and caches the previous width, so re-showing restores what the user had, and the state persists through CListColumnStore's existing width handling instead of needing a second mechanism.

COL_SIZE_MIN moves from a file-local constant in MuleListCtrl.cpp to ListColumnStore.h, beside the column-width interface, so both lists read one threshold rather than keeping copies. It is 10 on wxGTK — which won't shrink a column past the header grip — and 0 elsewhere, so this touches the GTK build in particular.

Shift+page-up/down on macOS

Shift+page-up/down extends the selection on GTK and MSW, whose backends implement it. On macOS it does nothing: NSOutlineView pages the view without touching the selection. Handled by hand under __WXOSX__ so the three ports agree; repeated presses keep extending, since it grows the existing selection between the cursor's old and new position.

Plain page-up/down is deliberately left to the platform — macOS scrolls without moving the selection by convention (Finder, Mail) — and that behaviour is unchanged.

Shared ordering

Both the existing type-ahead search and the new page handler need the top-level rows in displayed order, so that is factored into BuildDisplayOrder() and shared rather than written twice. It runs the rows through this list's own comparator because GetItemByRow()/GetRowByItem() exist only in wx's generic implementation, not on GTK or macOS.

Testing

Built clean on macOS, Ubuntu 26.04 arm64 (wxGTK 3.2.9) and Windows 11 arm64; clang-format and both clang-tidy tiers clean on the diff.

Draft until the interactive pass on all three is done — specifically that a hidden column stays hidden across a restart, and that the macOS page size feels right.

got3nks added 3 commits August 5, 2026 16:27
Two more behaviours the wxDataViewCtrl port left behind, both inherited
for free from CMuleListCtrl by every list that is still on wxListCtrl.

Right-clicking the header opened a check-menu of columns
(CMuleListCtrl::OnColumnRClick/OnMenuSelected); the ported search list
had no header right-click handler at all, so there was no way to hide a
column. Restored on CSearchListCtrl with the same semantics: hiding sets
the column to COL_SIZE_MIN and caches the previous width, so re-showing
restores what the user had and the state persists through
CListColumnStore's existing width handling rather than needing a second
mechanism. COL_SIZE_MIN moves from a file-local constant in
MuleListCtrl.cpp to ListColumnStore.h, next to the column-width
interface, so both lists read the same threshold instead of keeping
copies (it is 10 on wxGTK, which won't shrink a column past the header
grip, and 0 elsewhere).

Shift+page-up/down extends the selection on GTK and MSW, whose backends
implement it, but does nothing on macOS: NSOutlineView pages the view
without touching the selection. Handled by hand there so the three ports
agree. Plain page-up/down is deliberately left to the platform -- macOS
scrolls without moving the selection by convention, and that stays.

The row ordering the type-ahead search already needed is factored into
BuildDisplayOrder() and shared with the page handler rather than
duplicated; both need the top-level rows in displayed order, which means
running them through this list's own comparator since
GetItemByRow()/GetRowByItem() exist only in wx's generic implementation.
Four problems from testing the first cut.

wxDataViewCtrl won't shrink a column to nothing, so setting the width to
COL_SIZE_MIN left a header stub behind instead of hiding the column, and
GetWidth() never dropped to the threshold -- which meant the menu kept
showing a hidden column as visible and toggling it again just re-cached
the width, so it could not be brought back. Hiding now goes through
wxDataViewColumn::SetHidden() and the menu reads IsHidden(). Persistence
is unchanged: ColumnWidthAdapter reports a hidden column as zero-width
and treats a stored width at or below COL_SIZE_MIN as hidden, so
CListColumnStore still round-trips the state through the width it
already saves.

Hiding the expander column would have taken the group triangles with it
and left children unreachable, so the expander moves to the first
column still visible.

Shift+page-up/down moved a single row because the page size came from
GetItemRect(), which returns an empty rect for rows that aren't on
screen; the zero height collapsed a page to one row. GetCountPerPage()
is implemented by the native macOS backend and is used instead.

Shift+home/end weren't handled at all -- the view jumped but nothing was
selected -- so they now extend the selection to the start or end of the
list. The unshifted forms still belong to the platform.
Hiding the column that owned the expander moved it to the next visible
one, but re-showing a column to its left didn't take it back, so after
hiding and restoring the file name column the group triangles stayed
out in the middle of the row.

The rule is simply that the expander belongs on the leftmost visible
column, so both directions go through one helper, which also runs after
LoadColumnSettings(): a restored width can leave the default expander
column hidden, which would strand the triangles on a column nobody can
see.
@got3nks
got3nks marked this pull request as ready for review August 5, 2026 17:42
@got3nks

got3nks commented Aug 5, 2026

Copy link
Copy Markdown
Author

Interactive pass done on macOS, Ubuntu 26.04 arm64 (wxGTK 3.2.9) and Windows 11 arm64. Four things the first cut got wrong, all fixed in the commits above:

  • wxDataViewCtrl won't shrink a column to nothing, so setting the width to COL_SIZE_MIN left a header stub and GetWidth() never reached the threshold — the menu kept showing a hidden column as visible, and toggling it again just re-cached the width, so the file name column could not be brought back. Hiding goes through SetHidden() now, with the adapter mapping hidden to zero width so CListColumnStore still persists the state through the width it already saves.
  • Shift+page-up/down moved a single row: the page size came from GetItemRect(), which returns an empty rect for rows that aren't on screen, and the zero height collapsed a page to one row. GetCountPerPage() is implemented by the native macOS backend and is used instead.
  • Shift+home/end weren't handled at all — the view jumped but nothing was selected.
  • The group expander stayed where it was pushed when the column owning it was hidden, so restoring the file name column left the triangles mid-row. It now always sits on the leftmost visible column, including after LoadColumnSettings(), since a restored width can otherwise leave it on a hidden column at startup.

Verified after the fixes: hiding and restoring any column including the file name one, hidden columns surviving a restart, the expander following the leftmost visible column both ways, shift+page/home/end extending the selection on macOS, and the unshifted keys plus GTK/MSW selection behaviour unchanged.

got3nks added 4 commits August 5, 2026 19:54
SyncLists() mirrors column state between open search tabs, but only
copied widths. A hidden column reports a width of zero, so hiding one in
any tab pushed that zero to the others without hiding them -- leaving a
column that is invisible in the list yet still ticked in the header menu,
and so impossible to restore from there. Toggling it there just cached
the zero as its "previous" width.

Hidden state now travels with the width, and only a visible column's
width is copied, so an empty width never reaches another tab.
Hide/show also syncs immediately rather than waiting for the idle
width-comparison to notice, and the receiving tab re-derives its
expander column, since the sync can hide the column that was carrying
it.
Observed on macOS: after hiding Codec and Directory the columns were
gone from the list, but the header menu still ticked both and toggling
them did nothing useful. wxDataViewColumn::SetHidden() had taken effect
while IsHidden() went on reporting the column as shown, so every
decision made by reading the state back was wrong -- the menu's check
marks, the expander's "leftmost visible" search, the width the adapter
reported to CListColumnStore, and what SyncLists() copied to the other
tabs.

CSearchListCtrl now owns the hidden state and only ever writes to the
control, so nothing depends on how faithfully a port reports it. The
persisted form is unchanged: a hidden column is still saved as a
zero width, which CListColumnStore already stores as a negative entry
carrying the width to restore.
The previous commit justified tracking the state by claiming macOS
disagreed with itself -- that SetHidden() took effect while IsHidden()
still reported the column as shown. That was wrong. A probe against
wx 3.3.3 on macOS shows SetHidden(true) is followed by IsHidden()
returning true, as expected.

What is true, and what the code actually has to account for, is that a
hidden column keeps reporting its previous width rather than zero. The
width is what CListColumnStore persists, so the adapter has to answer
"hidden" from somewhere other than the width -- which is what the
tracked state is for. Same code, honest reason.
macOS sizes the trailing column to whatever space is left over, so once
the columns are collectively wider than the control it reports a width
of zero -- while remaining a perfectly visible column the user can
scroll to. Confirmed with a standalone wxDataViewCtrl carrying this
list's eleven columns: in a window too narrow for them the last column
reported zero while every other column, including ones entirely off
screen, reported its real width; widening the window until they all fit
made it report a real width again.

CListColumnStore::SaveSettings() treats a width <= 0 as hidden and
writes it as a negative entry, so each shutdown marked the last column
hidden; the next launch hid it and then did the same to whichever
column had become last, losing one per restart. This predates the
header menu -- it is why configs from the port already carry entries
like "C:-80,D:-258" -- but it only became visible once a negative width
started hiding the column outright instead of leaving a zero-width
stub.

The width adapter no longer lets a zero reach the store for a column
that isn't hidden: hidden state is tracked explicitly now, so a
reported zero falls back to the column's remembered or default width.
@got3nks
got3nks force-pushed the fix/search-column-menu branch from 95ba314 to a2914e5 Compare August 5, 2026 18:17
@got3nks
got3nks marked this pull request as draft August 5, 2026 18:20
macOS hands the leftover space to the last *resizable* column, so with
eleven columns wider than the control the trailing one is sized to
nothing: invisible in the list, and reported by GetWidth() as zero.
CListColumnStore reads a width <= 0 as hidden and persists it as a
negative entry, so each shutdown marked that column hidden and the next
launch did the same to whichever column had become last -- one lost per
restart, which is why configs from the port already carry entries like
"C:-80,D:-258". This predates the header menu; it only became obvious
once a negative width started hiding the column outright instead of
leaving a stub.

An empty spacer column now takes that position, so no column the user
cares about is ever the last resizable one. It has to be resizable
itself -- a fixed spacer cannot shrink and the collapse simply falls
through to the last real column. RealColumnCount() keeps it out of the
header menu, the persisted widths, the hidden-state tracking and the
cross-tab sync, and the width adapter refuses to pass a zero for a
column that isn't hidden, so a stray zero can no longer eat a column
even if the spacer is ever not last.

SetMinWidth() was the cheaper alternative but only stops the width
reaching zero -- the column still shrinks to its minimum rather than
keeping its configured width.

Separately, BuildDisplayOrder() now includes the children of expanded
groups. It is measured against what is on screen -- a page is
GetCountPerPage() rows, children included -- so leaving them out made
shift+page overshoot by however many children were showing and skip
every child inside the range. Type-to-select shares the helper and can
now land on a child row too.
@got3nks
got3nks marked this pull request as ready for review August 5, 2026 19:13
@got3nks

got3nks commented Aug 5, 2026

Copy link
Copy Markdown
Author

Pushed the fixes from testing on all three platforms. Two things worth calling out for review.

The trailing column collapsing is a pre-existing bug from #796, not something this PR introduced, and it is specific to the wxDataViewCtrl port. macOS gives the leftover space to the last resizable column, so with eleven columns wider than the control the trailing one is sized to nothing — invisible, and GetWidth() returns zero. CListColumnStore reads a width <= 0 as hidden and persists it negative, so every shutdown marked that column hidden and the next launch did the same to whichever column had become last, losing one per restart. That is why configs from the port already contain entries like C:-80,D:-258. It only became visible here because a negative width now hides the column outright instead of leaving a zero-width stub.

The lists still on CMuleListCtrl don't do this, which is the clearest evidence it belongs to the port rather than to CListColumnStore: in the same config, TableWidthsShared carries fourteen columns ending in D:399 — a wider set than the search list, overflowing the window just as much — and every entry is positive, as are Downloads, Sources, Peers and Friends. The only non-positive entries anywhere else are two columns hidden deliberately from the old header menu.

The fix is an empty spacer column that occupies the position macOS insists on shrinking. It has to be resizable itself — a fixed spacer cannot shrink and the collapse simply falls through to the last real column. RealColumnCount() keeps it out of the header menu, the persisted widths, the hidden-state tracking and the cross-tab sync. SetMinWidth() was the cheaper alternative but only stops the width reaching zero; the column still shrinks to its minimum instead of keeping its configured width.

BuildDisplayOrder() now includes the children of expanded groups. It is measured against what is on screen — a page is GetCountPerPage() rows, children included — so leaving them out made shift+page overshoot by however many children were showing and skip every child in the range. Type-to-select shares the helper and can now land on a child row.

One caveat if you read this commit-by-commit: 56e4e6083's message claims macOS's SetHidden() and IsHidden() disagree. That was wrong — a probe against wx 3.3.3 shows they agree — and cd819b6a5 corrects it. The tracked hidden state is still right, just for a different reason: a hidden column keeps reporting its previous width, so the width cannot answer "is this hidden".

Verified interactively on macOS, Ubuntu 26.04 arm64 (wxGTK 3.2.9) and Windows 11 arm64: the column menu, hidden columns surviving restarts, the expander following the leftmost visible column, cross-tab sync, and shift+page/home/end on macOS. amule and amulegui built separately on each. clang-format and both clang-tidy tiers clean.

Roughly 110 of the ~340 added lines are __WXOSX__-only — the page-selection handler and the spacer. Every remaining list will need the same compensation when it is ported, which is the concrete cost estimate for the shared base discussed in #801.

RealColumnCount() returns unsigned while the hidden-state helpers take
an int column index, so the loop variable narrowed implicitly on three
lines -- caught by the Tier-2 clang-tidy gate.
@got3nks
got3nks merged commit 4b1de94 into amule-org:master Aug 5, 2026
14 checks passed
@got3nks
got3nks deleted the fix/search-column-menu branch August 5, 2026 19:34
got3nks added a commit that referenced this pull request Aug 5, 2026
#796 ported the search list to wxDataViewCtrl and #805 fixed what that
port left behind. #801 asked whether the remaining lists should each be
ported independently or share a base, and deferred the answer until the
shape was clear from more than one list. It now is.

CMuleDataViewCtrl is the wxDataViewCtrl counterpart of CMuleListCtrl:
column widths and their persistence, the header show/hide menu, hidden
state, the multi-column sort chain, type-to-select, Cmd/Ctrl+A, the
macOS shifted page/home/end keys, the trailing-column spacer, and
drag-resize detection. It owns no data; a list supplies its rows through
GetDisplayOrder(), their label through GetRowLabel(), and how two of
them compare through CompareByColumn().

CMuleVirtualDataViewCtrl is the counterpart of CMuleVirtualListCtrl,
and virtual in the same sense: rows are addressed by index through a
wxDataViewIndexListModel and nothing is materialised per row. It carries
the item-identity bookkeeping an identity-addressed port avoids -- a
wxDataViewItem from a row-addressed model encodes the row number, so a
deletion silently retargets any item held across it, which is why
everything here speaks in wxUIntPtr and re-resolves selection after each
mutation. Also the legacy filter API, live re-sort coalesced through one
CallAfter and deferred while the user is interacting, bulk append and
batch removal, and icon columns.

CSearchListCtrl moves onto the plain base, losing 499 lines.
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