feat(search): port CSearchListCtrl to wxDataViewCtrl - #796
Conversation
Replaces the wxListCtrl-based (CMuleListCtrl-derived) search-results control with a wxDataViewCtrl-backed one, for screen-reader accessibility (amule-org#180 phase 2, part of amule-org#675). CMuleListCtrl wraps MuleExtern::wxGenericListCtrl, a vendored copy of wx's *generic* (owner-drawn) list control with no native platform widget behind it, so VoiceOver/Orca have nothing to attach to; wxDataViewCtrl is natively backed on GTK and macOS (wxHAS_NATIVE_DATAVIEWCTRL). New src/SearchListModel.{h,cpp}: a wxDataViewModel presenting the existing CSearchFile parent/children forest as a native tree. CSearchFile::GetParent()/GetChildren() drive it directly; expand/ collapse state is now owned by the control itself (IsExpanded()), replacing the old hand-drawn tree in CSearchListCtrl::OnDrawItem (manual DrawLine/DrawCircle connectors) and the CSearchFile::ShowChildren()/SetShowChildren() bookkeeping that faked expand state via row insertion/removal. CSearchListCtrl (src/SearchListCtrl.{h,cpp}) is rewritten to inherit wxDataViewCtrl directly rather than CMuleListCtrl, per the amule-org#180 discussion: generalizing CMuleListCtrl itself was explicitly rejected to avoid pulling Downloads/Servers/Shared Files into scope. Column persistence is reused unchanged via CListColumnStore/ IColumnWidthProvider (src/ListColumnStore.h, from the prior extraction PR) through a small ColumnWidthAdapter, since wxDataViewCtrl::GetColumnCount() is itself virtual with an incompatible signature -- unlike CMuleListCtrl's forwarding-override trick, that requires a separate adapter object rather than multiple inheritance. The rating column (smiley icon + text) uses the built-in wxDataViewIconTextRenderer via AppendIconTextColumn(), already precedented in-tree (PrefsUnifiedDlg's sidebar), instead of a custom renderer. FindItem()-based O(n) pointer-to-row lookups are gone: a wxDataViewItem carries the CSearchFile* directly as its ID. Behaviour notes (flagged for review): - Regex/known-file filtering excludes rows at the model level (IsContainer()/GetChildren()) rather than removing/reinserting wxListCtrl rows. A parent that fails its own filter but has a filter-passing child is now always shown as a container regardless of current expand state, whereas the old code only kept it visible while its children were already expanded -- a deliberate simplification now that expand/collapse is a pure display concern owned by the control rather than something filtering needs to consult. - Alt-sort (the Sources column's total/complete-count tie-break swap) is preserved by driving sorting entirely through CSearchListCtrl's own chain (m_sort_orders) rather than wxDataViewCtrl's native single-criterion sort state; header clicks are intercepted (EVT_DATAVIEW_COLUMN_HEADER_CLICK) and replicate the exact ascending/descending/alt cycle CMuleListCtrl:: OnColumnLClick used. - Cross-tab column-width sync (previously EVT_LIST_COL_END_DRAG) has no portable wxDataViewCtrl equivalent event, so it's detected via idle-time width polling instead (CSearchListCtrl::OnIdle). Verified so far: full `amule` target builds clean (macOS); clang-format v18 (pinned) applied. Ran the local clang-tidy CI replica (Tier-1 whole-tree + Tier-2 changed-lines); Tier-1 reported no hits in the new/changed files, Tier-2 flagged only modernize-use-emplace / modernize-loop-convert suggestions on the new code, all fixed and re-verified clean. Launched the built app against an isolated test config (own ports, not the real app) and confirmed it starts without crashing. Interactive verification of the tree/filter/sort/rating behaviour and the got3nks-required before/after screenshots on macOS/Linux/Windows are still outstanding before this is ready to open as a PR. Interactive verification (automated via cliclick + macOS Accessibility API against the isolated test instance, which has a live ed2k connection, so real search results with real grouped variants were available): tree expand/collapse on grouped results, regex filtering (93 -> 29/164 results for a substring match), and the sort cycle (4 clicks on the Sources column, direction visibly alternating each time, never getting stuck) all work correctly against real data. That testing surfaced two real bugs in the cross-tab column-width sync, both fixed here: - The live idle-driven resize sync (CSearchListCtrl::OnIdle) never fired: by default a wxWindow doesn't receive idle events unless it opts in, and this control didn't. Fixed with SetExtraStyle(GetExtraStyle() | wxWS_EX_PROCESS_IDLE) in the constructor -- confirmed a resize in one tab now immediately mirrors to a second, already-open tab. - Resizing a column in one tab and then closing tabs in a different order than the resize happened in dropped the resize entirely: the destructor only saved *its own* current width, and relied on the idle sync having already propagated it there, which is not guaranteed by the time tabs are closed. Fixed by calling SyncOtherLists(this) unconditionally at the top of the destructor, before removing this list from s_lists -- whichever tab happens to be closed last now always reflects the most recently touched state regardless of which tab the user actually resized. Confirmed the resized width survives a full clean app exit (persistence requires a clean shutdown for wxConfig to flush to disk -- pre- existing behaviour, not introduced by this port). clang-format v18 and the clang-tidy CI replica (Tier-1 + Tier-2) are clean after these fixes. Not yet verified: row coloring and the rating column's icon+text render (no rated results turned up in the ad-hoc searches used for testing). Cross-platform (Linux/Windows) behaviour and the got3nks-required before/after screenshots are still outstanding.
… entry
The wxDataViewCtrl port's rewritten CSearchListCtrl::OnRightClick
dropped the #if 0-guarded "Mark as known file" menu entry that was
never actually compiled/shown (OnMarkAsKnown itself is untouched,
just permanently unreachable, exactly as before). xgettext scans
_("...") calls textually regardless of #if 0, so removing that dead
line dropped the string from the source scan and put the checked-in
catalogs out of sync with scripts/update-po.sh's output -- caught by
CI's "App catalogs in sync with source" gate on PR amule-org#796.
Verified: only change across every po/*.po and po/amule.pot is the
removal of the single "Mark as known file" msgid (and its already-
untranslated-or-obsoleted msgstr entries) -- symmetric, matches the
one line removed from source, no POT-Creation-Date-only churn.
|
Reviewed and built this on macOS (arm64, wx 3.3.3) and Ubuntu 26.04 arm64 (wxGTK 3.2.6); Windows ARM64 is building. Row colouring is confirmed working on macOS, so that box can be ticked. 1. Grouped rows show only the filename until expanded. Returning true is right for this model specifically: a grouped parent is a real result, and its children are alternative sources of the same file — not a section header over unrelated rows. Patch--- a/src/SearchListModel.h
+++ b/src/SearchListModel.h
bool IsContainer(const wxDataViewItem &item) const wxOVERRIDE;
+ //! A grouped result is a row in its own right, not a section header: the
+ //! parent carries the same name/size/sources/rating as any other result
+ //! and its children are alternative sources for the same file. Without
+ //! this, wxDataViewModel::HasValue() draws only column 0 for a container
+ //! -- the group shows its filename and nothing else until expanded.
+ bool HasContainerColumns(const wxDataViewItem &item) const wxOVERRIDE;
--- a/src/SearchListModel.cpp
+++ b/src/SearchListModel.cpp
+bool CSearchListModel::HasContainerColumns(const wxDataViewItem &WXUNUSED(item)) const
+{
+ return true;
+}2. A new child of an existing group is never announced to the control. When a duplicate result arrives, Two consequences: a variant arriving for an already-expanded group won't appear until something forces a rebuild, and the leaf→container transition (first child of what was a plain row) relies on the port re-querying 3. 4. A comment now names members this PR deletes. |
|
Built this on Ubuntu 26.04 arm64 (wxGTK 3.2.6) and Windows 11 arm64 from
item->AddChild(toadd);
Notify_Search_Update_Sources(item); // -> ItemChanged(parent)
Confirmed on Linux: toggling the filter checkbox makes expanders appear on the groups that already exist. That routes through Fix: send |
…und in review Addresses four issues got3nks found reviewing PR amule-org#796 (the CSearchListCtrl wxDataViewCtrl port): 1. Grouped results only showed the filename column until expanded. wxDataViewModel::HasValue() defaults to drawing just column 0 for a container (HasContainerColumns() defaults to false) -- a wxListCtrl had no notion of container rows, so this only surfaced with the port. A grouped parent is a real result carrying the same size/sources/rating as any other row, not a section header, so CSearchListModel::HasContainerColumns() now returns true. 2. Grouped results never got an expander triangle on GTK or MSW (confirmed by got3nks building this branch on Ubuntu 26.04 arm64 and Windows 11 arm64 -- child rows were completely unreachable on both). CSearchList::AddToList's duplicate-result path calls item->AddChild(toadd) then only Notify_Search_Update_Sources(item), which reaches the model as ItemChanged(parent) -- a *value* notification. Leaf-to-container is a *structural* change, which the wxDataViewModel notifier API signals via ItemAdded/ItemDeleted instead. macOS masked this: its NSOutlineView backend re-queries IsContainer() on every draw, so the triangle appeared as soon as HasChildren() flipped regardless of notification. GTK's GtkTreeView and the MSW generic implementation both maintain their own node tree and only learn container-ness from structural notifications, which this path never sent. Fixed by also calling Notify_Search_Add_Result(toadd) -- routes through the existing CSearchDlg::AddResult -> CSearchListCtrl::AddResult -> CSearchListModel::NotifyFileAdded path, which already handles a child correctly (ItemAdded(parent, child)) once actually called. 3. NotifyFileRemoved() didn't gate on the same ShouldShow() check NotifyFileAdded() does, so a result that was filtered out (and therefore never added to the control) could still get an ItemDeleted call for a row the control never knew about. Now symmetric. 4. A comment in CSearchList::RemoveResults still named SetItemPtrData and m_filteredOut, both gone with the port; updated to describe the current mechanism (wxDataViewItem IDs are the raw CSearchFile pointers). Verified: full `amule` target builds clean (macOS); clang-format v18 (pinned) applied; clang-tidy CI replica (Tier-1 whole-tree + Tier-2 changed-lines) both clean.
|
Reviewed 1. Gating if (result && m_filterKnown) {
result = file->GetDownloadStatus() == CSearchFile::NEW;
}With "filter known files" on, a result shown while 2. Both are the same mistake: deciding what to notify by re-evaluating a predicate, when the question is what the control was actually told. Those diverge as soon as the predicate depends on mutable state (download status) or on data that arrives later (children). Fix: track announcement rather than recomputing it — a Both need an active filter, so they're edge cases — but the first is a crash, and "filter known files" plus starting a download is an ordinary sequence. 3. The child path now recomputes the tab label on every duplicate. |
|
Built this on Ubuntu 26.04 arm64 (wxGTK 3.2.9) and worked through the failures with a debug build. Five distinct problems, all reproduced and all fixed in the diff below — the first is a crash, and two of them aren't GTK-specific. 1. Use-after-free on every grouped search (crash). 2. Groups never gain an expander until something forces a rebuild. A result is announced when it arrives, before it has children, so the control records it as a leaf. Nothing afterwards makes wxGTK re-derive container-ness: 3. Dead expanders. 4. Sort order is never restored (not GTK-specific). 5. No sort indicator in the header (not GTK-specific). The columns are appended Patch (applies to 30b83ac)diff --git a/src/SearchList.cpp b/src/SearchList.cpp
index 1ed9f9205..d4177bffa 100644
--- a/src/SearchList.cpp
+++ b/src/SearchList.cpp
@@ -816,7 +816,12 @@ bool CSearchList::AddToList(CSearchFile *toadd, bool clientResponse)
CFormat("Received duplicate results for '%s' : %s") % item->GetFileName() %
item->GetFileHash().Encode());
// Add the child, possibly updating the parents filename.
+ const size_t childrenBefore = item->GetChildren().size();
item->AddChild(toadd);
+ // AddChild MERGES a duplicate filename into an existing result
+ // and `delete`s the file it was handed -- notifying with `toadd`
+ // after that reads freed memory.
+ const bool survived = item->GetChildren().size() > childrenBefore;
// Structural change (leaf-or-nothing -> container, or a new row
// under an existing container) needs its own notification --
// Search_Update_Sources only signals that the parent's values
@@ -826,7 +831,9 @@ bool CSearchList::AddToList(CSearchFile *toadd, bool clientResponse)
// becomes reachable there. Native NSOutlineView survives the
// omission by re-querying IsContainer() on every draw, which
// masked this on macOS.
- Notify_Search_Add_Result(toadd);
+ if (survived) {
+ Notify_Search_Add_Result(toadd);
+ }
Notify_Search_Update_Sources(item);
return true;
}
diff --git a/src/SearchListCtrl.cpp b/src/SearchListCtrl.cpp
index f27d017ee..12348d839 100644
--- a/src/SearchListCtrl.cpp
+++ b/src/SearchListCtrl.cpp
@@ -105,44 +105,44 @@ CSearchListCtrl::CSearchListCtrl(
wxDATAVIEW_CELL_INERT,
500,
wxALIGN_LEFT,
- wxDATAVIEW_COL_RESIZABLE);
+ wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE);
AppendTextColumn(_("Size"),
CSearchListModel::COL_SIZE,
wxDATAVIEW_CELL_INERT,
100,
wxALIGN_LEFT,
- wxDATAVIEW_COL_RESIZABLE);
+ wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE);
AppendTextColumn(_("Sources"),
CSearchListModel::COL_SOURCES,
wxDATAVIEW_CELL_INERT,
50,
wxALIGN_LEFT,
- wxDATAVIEW_COL_RESIZABLE);
+ wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE);
AppendTextColumn(_("Type"),
CSearchListModel::COL_TYPE,
wxDATAVIEW_CELL_INERT,
65,
wxALIGN_LEFT,
- wxDATAVIEW_COL_RESIZABLE);
+ wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE);
// Rating: smiley icon + text label in one cell.
AppendIconTextColumn(_("Rating"),
CSearchListModel::COL_RATING,
wxDATAVIEW_CELL_INERT,
120,
wxALIGN_LEFT,
- wxDATAVIEW_COL_RESIZABLE);
+ wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE);
AppendTextColumn(_("FileID"),
CSearchListModel::COL_FILEID,
wxDATAVIEW_CELL_INERT,
280,
wxALIGN_LEFT,
- wxDATAVIEW_COL_RESIZABLE);
+ wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE);
AppendTextColumn(_("Status"),
CSearchListModel::COL_STATUS,
wxDATAVIEW_CELL_INERT,
100,
wxALIGN_LEFT,
- wxDATAVIEW_COL_RESIZABLE);
+ wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE);
// Media tag columns: ed2k/Kad publishers (eMule, eMule AI, aMule) can
// advertise per-file media metadata in FT_MEDIA_LENGTH / _BITRATE /
// _CODEC. Cells stay empty for non-media results.
@@ -151,19 +151,19 @@ CSearchListCtrl::CSearchListCtrl(
wxDATAVIEW_CELL_INERT,
80,
wxALIGN_LEFT,
- wxDATAVIEW_COL_RESIZABLE);
+ wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE);
AppendTextColumn(_("Bitrate"),
CSearchListModel::COL_BITRATE,
wxDATAVIEW_CELL_INERT,
80,
wxALIGN_LEFT,
- wxDATAVIEW_COL_RESIZABLE);
+ wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE);
AppendTextColumn(_("Codec"),
CSearchListModel::COL_CODEC,
wxDATAVIEW_CELL_INERT,
80,
wxALIGN_LEFT,
- wxDATAVIEW_COL_RESIZABLE);
+ wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE);
// Directories is almost always empty (only populated when the result
// came from a "view shared files" request, rare in practice), so put
// it at the end with the other usually-empty columns.
@@ -173,7 +173,7 @@ CSearchListCtrl::CSearchListCtrl(
wxDATAVIEW_CELL_INERT,
280,
wxALIGN_LEFT,
- wxDATAVIEW_COL_RESIZABLE);
+ wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE);
m_columnStore.RegisterColumn(CSearchListModel::COL_NAME, 500, "N");
m_columnStore.RegisterColumn(CSearchListModel::COL_SIZE, 100, "Z");
@@ -237,17 +237,22 @@ void CSearchListCtrl::LoadColumnSettings()
CListColumnStore::CSortingList decoded;
m_columnStore.LoadSettings(m_widthAdapter, "N,Z,u,Y,I,S", decoded);
+ // LoadSettings() returns the orders primary-LAST: CMuleListCtrl applied
+ // them by calling SetSorting() on each in turn, and each call pushes to
+ // the front, so the last one processed ends up primary. ApplySorting()
+ // has the same push-to-front semantics, so replaying them in order
+ // reproduces that -- taking front() as the primary instead picks the
+ // least significant entry.
m_sort_orders.clear();
for (const CListColumnStore::CColPair &pair : decoded) {
- m_sort_orders.emplace_back(pair.first, pair.second);
+ ApplySorting(pair.first, pair.second);
}
if (m_sort_orders.empty()) {
- m_sort_orders.emplace_back(CSearchListModel::COL_NAME, 0);
+ ApplySorting(CSearchListModel::COL_NAME, 0);
}
+ return;
+
- const CColPair &primary = m_sort_orders.front();
- GetColumn(primary.first)->SetSortOrder(!(primary.second & SORT_DES));
- GetModel()->Resort();
}
void CSearchListCtrl::SaveColumnSettings()
@@ -626,6 +631,46 @@ void CSearchListCtrl::OnIdle(wxIdleEvent &event)
{
event.Skip();
+ // One coalesced rebuild per idle for everything that arrived since the
+ // last one. Cleared() throws away the control's own view state,
+ // so selection and expansion are captured and re-applied around it --
+ // otherwise a result landing mid-search deselects whatever the user had
+ // picked. Items are CSearchFile*, still valid across the rebuild; the ones
+ // that went away are dropped by re-checking membership after it.
+ if (m_model->HasPending()) {
+ wxDataViewItemArray selected;
+ GetSelections(selected);
+ wxDataViewItemArray expanded;
+ {
+ wxDataViewItemArray roots;
+ m_model->GetChildren(wxDataViewItem(), roots);
+ for (size_t i = 0; i < roots.GetCount(); ++i) {
+ if (IsExpanded(roots[i])) {
+ expanded.Add(roots[i]);
+ }
+ }
+ }
+
+ m_model->FlushPending();
+
+ wxDataViewItemArray live;
+ m_model->GetChildren(wxDataViewItem(), live);
+ for (size_t i = 0; i < expanded.GetCount(); ++i) {
+ if (live.Index(expanded[i]) != wxNOT_FOUND) {
+ Expand(expanded[i]);
+ }
+ }
+ wxDataViewItemArray restore;
+ for (size_t i = 0; i < selected.GetCount(); ++i) {
+ if (live.Index(selected[i]) != wxNOT_FOUND) {
+ restore.Add(selected[i]);
+ }
+ }
+ if (!restore.IsEmpty()) {
+ SetSelections(restore);
+ }
+ }
+
// No portable wxDataViewCtrl "column resized" event exists to hook
// directly (unlike wxListCtrl's EVT_LIST_COL_END_DRAG), so a drag-resize
// is detected here by simply comparing against the last-seen widths.
diff --git a/src/SearchListModel.cpp b/src/SearchListModel.cpp
index 615968696..75e796eee 100644
--- a/src/SearchListModel.cpp
+++ b/src/SearchListModel.cpp
@@ -43,42 +43,36 @@ CSearchListModel::CSearchListModel(CSearchListCtrl *owner)
{
}
-void CSearchListModel::NotifyFileAdded(CSearchFile *file)
+void CSearchListModel::NotifyFileAdded(CSearchFile *)
{
- if (!m_owner->ShouldShow(file)) {
- return;
- }
- CSearchFile *parent = file->GetParent();
- ItemAdded(parent ? ToItem(parent) : wxDataViewItem(), ToItem(file));
+ MarkDirty();
}
-void CSearchListModel::NotifyFileRemoved(CSearchFile *file)
+void CSearchListModel::NotifyFileRemoved(CSearchFile *)
{
- // Mirror NotifyFileAdded's gate: a file the control was never told about
- // (filtered out at add time) must not be deleted from it either -- that
- // gate is exactly ShouldShow(), the same test GetChildren() itself uses
- // to decide what's visible (root items via ShouldShow(); children via
- // PassesFilter(), which ShouldShow() reduces to for a childless file).
- if (!m_owner->ShouldShow(file)) {
- return;
- }
- CSearchFile *parent = file->GetParent();
- ItemDeleted(parent ? ToItem(parent) : wxDataViewItem(), ToItem(file));
+ MarkDirty();
}
-void CSearchListModel::NotifyFileUpdated(CSearchFile *file)
+void CSearchListModel::NotifyFileUpdated(CSearchFile *)
{
- ItemChanged(ToItem(file));
+ MarkDirty();
}
void CSearchListModel::NotifyFilterChanged()
{
- // The set of visible root items (and which parents count as containers)
- // may have changed arbitrarily; a full reset is the only thing that's
- // guaranteed correct here, at the cost of resetting expand state -- the
- // same cost EnableFiltering()/SetFilter() already paid via
- // DeleteAllItems()-equivalent behaviour before this port.
+ // User action: reset now rather than waiting for idle.
+ m_pendingReset = false;
+ Cleared();
+}
+
+bool CSearchListModel::FlushPending()
+{
+ if (!m_pendingReset) {
+ return false;
+ }
+ m_pendingReset = false;
Cleared();
+ return true;
}
unsigned int CSearchListModel::GetColumnCount() const
@@ -233,7 +227,17 @@ bool CSearchListModel::IsContainer(const wxDataViewItem &item) const
if (!item.IsOk()) {
return true; // invisible root
}
- return ToFile(item)->HasChildren();
+ // Must agree with GetChildren(), which only yields children that pass the
+ // filter: answering "has children" here for a group whose variants are all
+ // filtered out draws an expander that opens onto nothing.
+ const CSearchFile *file = ToFile(item);
+ const CSearchResultList &kids = file->GetChildren();
+ for (const CSearchFile *kid : kids) {
+ if (m_owner->PassesFilter(kid)) {
+ return true;
+ }
+ }
+ return false;
}
bool CSearchListModel::HasContainerColumns(const wxDataViewItem &WXUNUSED(item)) const
diff --git a/src/SearchListModel.h b/src/SearchListModel.h
index 21a0bd8e4..478758b72 100644
--- a/src/SearchListModel.h
+++ b/src/SearchListModel.h
@@ -70,6 +70,14 @@ public:
//! tree should be re-evaluated (some rows may appear/disappear).
void NotifyFilterChanged();
+ //! Results arrive in bursts, and mixing incremental Item*
+ //! notifications with the full Cleared() that a group formation requires
+ //! leaves the control's tree inconsistent. Every arrival now just marks
+ //! the model dirty; the control flushes one reset per idle.
+ void MarkDirty() { m_pendingReset = true; }
+ bool FlushPending();
+ bool HasPending() const { return m_pendingReset; }
+
static CSearchFile *ToFile(const wxDataViewItem &item)
{
return static_cast<CSearchFile *>(item.GetID());
@@ -120,6 +128,7 @@ public:
private:
CSearchListCtrl *m_owner;
+ bool m_pendingReset = false;
};
#endif // SEARCHLISTMODEL_HTesting. All five reproduced and verified fixed on Linux/GTK against live searches. Windows was built with the first three but not visually checked; macOS never exhibited 1–3, because Two caveats on the patch. The use-after-free guard infers deletion from a child-count delta; cleaner would be for |
|
Two of my comments above review the same commit ( Withdrawing point 1 of the earlier comment (the Concrete follow-up instead: Point 2 of that comment (child announced under a parent the control never heard about) was real against Still open, and not addressed by that patch: point 3, Testing correction. The later comment said Windows was built with the first three fixes but not visually checked. Windows ARM64 has since been rebuilt with all five: points 4 and 5 (sort restore and the header caret) are confirmed fixed there. 1–3 are in that build, but the visual pass concentrated on sorting. Unrelated to this PR: fast touchpad scrolling bounces the view back toward the top on Windows. It reproduces on the Shared Files list, which this PR doesn't touch — both it and |
Addresses five issues got3nks found reviewing 30b83ac on real Ubuntu 26.04 arm64 (wxGTK 3.2.9) and Windows 11 arm64 builds, worked through with a debug build -- the first is a crash. 1. Use-after-free on every grouped search. CSearchFile::AddChild() merges a duplicate filename into an existing result and deletes the object it was handed, on two of its four paths. Notify_Search_Add_Result(toadd) afterwards passed that freed pointer onward -- CSearchDlg::AddResult() dereferences it to pick the tab, then the model hands it to ItemAdded() as an item ID. Duplicate filenames are the common case in search results, so this faulted within seconds of a search on GTK. Fixed by checking whether the child count actually grew before notifying (AddChild() itself doesn't report what it did). 2. Groups never gained an expander until something forced a rebuild. A result is announced when it arrives, before it has children, so the control records it as a leaf. Nothing afterwards made wxGTK re-derive container-ness: ItemChanged() is a value notification, and deleting and re-adding the row doesn't do it either -- both were tried. Only Cleared() works, which is why toggling the filter used to "fix" the display. A full reset can't be interleaved with incremental Item* calls in the same burst without leaving the tree inconsistent, so every notification now just marks the model dirty (CSearchListModel::MarkDirty()) and CSearchListCtrl::OnIdle() flushes one coalesced Cleared() rebuild, with selection and expansion captured and restored around it so a result landing mid-search doesn't reset what the user had picked. 3. Dead expanders: IsContainer() asked HasChildren() while GetChildren() yields only children passing the filter, so a group whose variants were all filtered out drew an expander that opened onto nothing. Both now ask the same question. 4. Sort order was never restored correctly. CListColumnStore:: LoadSettings() returns the decoded orders primary-LAST (its own doc comment says so); CMuleListCtrl consumed them by calling SetSorting() on each in turn, each push-to-front, so the last one processed ends up primary. CSearchListCtrl::LoadColumnSettings() instead built the chain directly and took front() as primary -- the *least* significant entry. Fixed by replaying the decoded list through ApplySorting() in order, matching the original semantics. 5. No sort indicator in the column headers: columns were appended with wxDATAVIEW_COL_RESIZABLE only: wx won't render a sort caret on a column that isn't also wxDATAVIEW_COL_SORTABLE. Added the flag to all 11 columns; the custom click cycle (ascending/descending/alt) still drives the actual sort, this only restores the visual cue. Also fixes an unrelated narrowing-conversion warning clang-tidy flagged nearby (OnRightClick's per-category menu item IDs). Verified: full `amule` target builds clean (macOS); clang-format v18 (pinned) applied; clang-tidy CI replica (Tier-1 whole-tree + Tier-2 changed-lines) both clean. Interactive testing (cliclick + macOS Accessibility API against the isolated test instance's live ed2k connection): no crash across repeated live searches producing duplicate/grouped results (the exact use-after-free trigger); a group forming live during a search shows its expander immediately, with no need to toggle the filter; sorting by a non-default column (Sources) persists and is correctly restored as primary after a full app restart (previously silently reverted to Name).
Two follow-ups from the PR amule-org#796 review that the previous commit didn't cover. CSearchDlg::UpdateHitCount() walks the search's whole result list twice -- GetItemCount() and GetHiddenItemCount(), each looping every result's children through ShouldShow()/IsFiltered() -- and it ran once per arriving result, so a search was quadratic in its own result count. The child path made it worse: since the notification rework, a duplicate result also reaches CSearchDlg::AddResult, so a heavily grouped search paid the double walk per duplicate as well. Results arrive in bursts and only a burst's final label is ever seen, so AddResult/UpdateResult now just mark the tab pending and one recompute per idle flushes them -- the same coalescing CSearchListCtrl::OnIdle already does for the tree rebuild. The flush iterates the notebook's live pages and updates those present in the pending set, so a tab closed between mark and flush is never matched and its pointer never followed. The other UpdateHitCount() call sites are user actions (tab change, filter toggle, browse status) and stay immediate. CSearchDlg needs wxWS_EX_PROCESS_IDLE for any of that to run: amuleDlg sets wxIdleEvent::SetMode(wxIDLE_PROCESS_SPECIFIED), so idle events only reach windows that opt in (CSearchListCtrl already does for its own OnIdle). Without the style the flush never fires and the tab label stops updating entirely -- it builds and lints clean either way. CSearchListCtrl::RemoveResult() and CSearchListModel::NotifyFileRemoved() are dead after this port: their only caller was ShowChildren(), the flat -list expand/collapse emulation wxDataViewCtrl makes unnecessary, and CSearchList::RemoveResults() never took that path -- it fires Notify_Search_Removed and closes the tab before freeing anything. Removed both. (This also retires the ShouldShow() gate an earlier review comment of mine flagged as a use-after-free; the gate was latent-wrong rather than reachable, since nothing called into it.)
|
Pushed Dead code removed. Hit-count recompute coalesced. One trap worth knowing if you touch idle handling here: Verified on macOS and Ubuntu 26.04 arm64 (wxGTK 3.2.9); Windows ARM64 is still building. clang-format and both clang-tidy tiers clean on the diff. |
Catalog conflict only: master picked up a Weblate translation round while this branch had regenerated the catalogs after dropping the dead "Mark as known file" menu entry, so all 41 po/ files collided. No source conflicts. Resolved by taking master's catalogs -- keeping the new Weblate translations -- and re-running scripts/update-po.sh against the merged source, which re-applies this branch's string removal. "Mark as known file" moves back to an obsolete (#~) entry with its translation retained, and the only other change is POT-Creation-Date.
wxDataViewCtrl defaults to single selection; the wxListCtrl it replaces was multi-select unless given wxLC_SINGLE_SEL, and the pre-port construction site passed wxLC_REPORT | wxNO_BORDER, so it was on. Without wxDV_MULTIPLE none of the list's multi-selection behaviour works: downloading, downloading into a category, copying eD2k links, and the related-search/comments entries all iterate GetSelections(), and OnRightClick still enables the single-selection-only items via GetSelectedItemCount() == 1 -- a test that can never be false.
The menu entry that reached OnMarkAsKnown was removed with the port -- it had been sitting behind #if 0 since long before, with a comment warning it might break known.met -- but the handler, its event-table binding and the MP_MARK_AS_KNOWN id survived, so nothing can invoke it. Removed all three, plus the KnownFileList.h include that was only there for it. MP_ASSIGNCAT is anchored at MP_LISTCOL_15 + 1, so dropping an earlier enumerator doesn't shift the category id range. Verified both variants build: the removed body was inside #ifndef CLIENT_GUI, so amule and amulegui were checked separately.
CMuleListCtrl::OnChar() gave every list type-to-select: typing jumps the selection to the first row whose name starts with what was typed, accumulating keystrokes until a 1.5s pause resets them. The port lost it along with the rest of that handler, and the wxDataViewCtrl backends disagree about what to do instead -- GTK pops up its own interactive search box, macOS and MSW do nothing at all -- so the three ports no longer behaved alike. Reimplemented on CSearchListCtrl so all three match, and so GTK's search popup no longer surfaces (the handler consumes the keystroke rather than skipping it). Matching walks the top-level rows through this list's own comparator, the one CSearchListModel::Compare() already uses, so the jump order follows what is actually on screen under the current sort. GetItemByRow()/GetRowByItem() would be the direct route but exist only in wx's generic implementation, not on GTK or macOS. Navigation and shortcut keys are skipped to the backend: page up/down keeps its per-platform behaviour (macOS scrolls without moving the selection, by convention), and select-all stays native.
|
Pulled Nothing further from me right now; CI looks green apart from mingw-w64 and clang-tidy still running. |
Only wxGTK's backend implements select-all for wxDataViewCtrl; macOS and MSW leave the shortcut unhandled, so the result list lost it on two of three ports. CMuleListCtrl::OnChar() implemented it explicitly for the same reason, so do it here too rather than relying on the backend. A control-modified 'a' arrives as SOH on most ports -- the test the old handler used -- but the letter is accepted as well so the shortcut can't go missing on a port that reports it differently. Every other modified key still goes to the backend, which keeps shift+page-up/down extending the selection where the platform supports it.
|
Pushed four more commits after testing the port on all three platforms. Three of them are regressions the port introduced by inheriting
Page Up/Down is deliberately left alone: it moves the selection on GTK and MSW but scrolls without moving it on macOS, which is the platform convention (Finder, Mail). Shift+PgUp/Dn extends the selection on Linux and Windows. Navigation and other shortcut keys are skipped to the backend so this stays true. Also merged master in ( Verified interactively on macOS, Ubuntu 26.04 arm64 (wxGTK 3.2.9) and Windows 11 arm64: multi-select, type-to-select, select-all, sorting and grouping all behave the same on the three ports. clang-format and both clang-tidy tiers clean throughout; |
#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.
…) (#830) * feat(gui): port CFriendListCtrl to CMuleVirtualDataViewCtrl (#180, #801) Continues the wxListCtrl -> wxDataViewCtrl migration (#180/#801) onto the shared base PR #811 extracted after the Servers port (#807, now absorbed into #811). CFriendListCtrl was picked next after reading all the remaining candidates directly rather than going by line count alone: it has no CBarShader rendering (unlike Downloads/SharedFiles/ Sources-Peers, which additionally need a still-nonexistent wxDataViewCustomRenderer), it's already pointer-identity addressed (UpdateFriend(CFriend*)/RemoveFriend(CFriend*) map directly onto AddItemData/RefreshItemData/RemoveItemData), and its blast radius is tiny (ChatWnd.cpp, muuli_wdr.cpp construction only). CFileDetailListCtrl looked smaller by line count but its caller (FileDetailDialog.cpp) drives it with raw position-indexed CRUD (FindItem by name, SetItem by column index, DeleteItem) plus a pre-existing duplicate-type quirk (two unrelated SourcenameItem structs relying on compatible layout) -- porting it cleanly means refactoring the caller too, which is a separate, more invasive piece of work than this one. CServerListCtrl (current, post-#811) is the template mirrored here: same AppendTextColumn/AppendSpacerColumn/AssociateVirtualModel/ LoadColumnSettings/InitColumnState ctor sequence, same GetItemColumnText/ GetItemAttr/CompareItemData/OnListKey hook shape. Public API (UpdateFriend, RemoveFriend) and the constructor signature are unchanged, so ChatWnd.cpp and muuli_wdr.cpp needed no edits. Notable deltas from the pre-port behaviour: added a real CompareItemData so header-click sort now works (the old list never called SetSortFunc at all); GetItemAttr replaces the old SetItemTextColour call with the same visible result (blue for linked friends, default text colour otherwise). Verified: builds clean; clang-format v18 (pinned Docker image) applied; clang-tidy Tier-1 (whole-tree) and Tier-2 (changed-lines, .clang-tidy-new-code) both clean via the ~/aMuleTest/ci-local replica -- Tier-2 caught one real modernize-use-nullptr hit, fixed. Grepped ChatWnd.cpp/.h and muuli_wdr.cpp to confirm no other call sites exist. Interactive verification (sort, right-click menu states, Delete-key removal, chat-session activation, VoiceOver) left for manual testing per project convention, same as PR #796/#807. * fix(gui): resolve activated friend through selection, not row-as-pointer got3nks's review on #830 found a real crash: OnItemActivated() cast event.GetItem()'s ID directly to CFriend* on the assumption it was the item's data pointer, but CMuleVirtualDataViewCtrl's row-addressed model returns the row index (+1) as that ID -- the "item identity is not row identity" case MuleVirtualDataViewCtrl.h itself documents. Every double-click/Enter on a friend dereferenced a bogus pointer. Fixed by selecting the activated row and resolving it through GetSelectedItemData(), matching CServerListCtrl::OnItemActivated. Also from the same review: IsLiveSortColumn() now returns true, since the name (the only sortable column) can change after a friend is already listed and the UpdateFriend() comment claimed a re-sort that the base's default-false hook never actually triggered; and dropped a no-op static_cast<int>() around a call that already returns int. Verified: builds clean, clang-format v18 applied, clang-tidy Tier-2 (changed lines, .clang-tidy-new-code) clean via the local CI replica. * chore: retrigger CI The previous run hit a GitHub Actions infrastructure outage (job not acquired by any runner, "Failed to resolve action download info" / "Service Unavailable" on clang-format, Translation checks, mingw-w64 Debug and clang-tidy Tier-1) unrelated to this branch's code -- every job that did run passed. No admin rights to rerun the failed jobs directly, so retriggering with an empty commit instead.
…#801) (#839) * feat(gui): port CFileDetailListCtrl to CMuleVirtualDataViewCtrl (#180, #801) Continues the wxListCtrl -> wxDataViewCtrl migration (#180/#801) onto the shared base from #811, after Search (#796), Servers (#807/#811) and Friends (#830). CFileDetailListCtrl was next because it has no CBarShader rendering (unlike Downloads/SharedFiles/Sources-Peers, which additionally need a still-nonexistent wxDataViewCustomRenderer). Its caller, CFileDetailDialog::FillSourcenameList(), drove the old list with raw position-indexed CRUD (FindItem by name, SetItemPtrData, SetItem by column index, DeleteItem) rather than the pointer-identity style CServerListCtrl/CFriendListCtrl already use, so porting the control cleanly meant refactoring the caller too: - CFileDetailDialog gained a std::map<wxString, SourcenameItem *> m_sourcenames member, replacing "search the list widget by name" with a map lookup. FillSourcenameList()'s reset/update/prune shape is otherwise unchanged, just re-keyed to the map and driven through the control's new AddSource/RefreshSource/RemoveSource API. - OnBnClickedTakeOver()/OnListClickedTakeOver() resolve the selected row through GetSelectedItemData() (inherited from CMuleVirtualDataViewCtrl) instead of GetNextItem+GetItemText. - The dialog's EVT_LIST_ITEM_ACTIVATED binding on IDC_LISTCTRLFILENAMES becomes EVT_DATAVIEW_ITEM_ACTIVATED; wxDataViewEvent is a wxNotifyEvent/wxCommandEvent descendant so it still propagates from the child control to the dialog-level handler the same way. - Fixed a latent leak: nothing in ~CFileDetailDialog() freed the SourcenameItem objects still referenced by open rows -- only the "not a partfile" and "pruned to zero" paths in FillSourcenameList() ever deleted them. Now freed in the destructor via m_sourcenames. CMuleDataViewCtrl always ORs in wxDV_MULTIPLE (no single-selection mode exists on the shared base), but this list's "take over filename" actions assume exactly one selection, same as the old list did (it never actually requested wxLC_SINGLE_SEL either -- checked muuli_wdr.cpp -- it enforced single-selection itself via OnSelect() deselecting every other row). CFileDetailListCtrl::OnSelectionChanged() does the equivalent: collapses to the just-clicked row whenever more than one ends up selected. Also deleted a vestigial nested SourcenameItem struct in FileDetailListCtrl.h that duplicated (by accident of matching layout) the real one in PartFile.h, and dropped a per-row background-colour set that reproduced the default and carried its own "do we still need this?" comment. Verified: builds clean; clang-format v18 (pinned Docker image) applied; clang-tidy Tier-1 (whole-tree) and Tier-2 (changed-lines, .clang-tidy-new-code) both clean via the ~/aMuleTest/ci-local replica. Grepped for other CFileDetailListCtrl/IDC_LISTCTRLFILENAMES references to confirm muuli_wdr.cpp's construction call needed no changes. Interactive verification (source list populate/re-tally, take-over via button and double-click, single-selection enforcement, sort, VoiceOver) left for manual testing per project convention. * fix(gui): keep the source-name list sorted, and drop rows before freeing them Review follow-up to the port. FillSourcenameList() zeroes every count and then rewrites them in place, so while it runs the list is not ordered by the column it is sorted on -- and AddSource() places a new row with a binary search, which needs that ordering to hold. An insertion therefore leaves rows in arbitrary positions, and the repair has to happen here, exactly as the pre-port code did. Only on insertion, though. A count that merely changed is what the live-sort preference governs: RefreshSource() re-sorts when it is on, and when it is off the row is meant to stay put rather than move under the user. Sorting unconditionally would quietly override that setting for this list. The destructor and the not-a-partfile path also freed the SourcenameItem objects while the list control still held pointers to them. Nothing can paint or sort in either window today -- both dialog call sites are stack temporaries, so destruction is synchronous -- but that rests on wx's teardown order rather than on anything guaranteed, and the base states the rule plainly: it has to be told before the caller frees the item data. Both now clear the rows first, as the prune loop already did. * fix(gui): decide the source list's live re-sort per column IsLiveSortColumn() answered true for every column, so a refresh tick scheduled a re-sort even when the list was sorted by File Name -- a value that never changes, since the name is the key each row was created under. Answer per column instead, the shape CServerListCtrl already uses, so only a sources-sorted list re-sorts on its own. The header comment already described it this way; only the implementation did not. * chore: drop a duplicated paragraph from the file header FileDetailDialog.cpp carried the "Any parts of this program derived from the xMule, lMule or eMule project" paragraph twice. It is the only file in src/ that does, and the file is already being touched here. --------- Co-authored-by: got3nks <[email protected]>
Three ways the ported lists threw away the row the user was reading. SortList() ended in wxDataViewIndexListModel::Reset(), which says the model was replaced; every backend rebuilds and lands at row 0, both axes. A sort reorders rows without replacing them and the count is the same on both sides of the std::sort, so a repaint is all it needs. The bulk path is the exception: AppendItemData() adds rows without telling the control -- that is what makes a bulk load cheap -- so Reset() was doubling as the only notification that the rows existed, and repainting there drew a list the control still believed was empty. The sort now splits into SortItems(), which is silent, SortList(), which repaints, and FinishBulkLoad(), which keeps its Reset(). Affects servers, shared files, clients, downloads, file details and friends. The unshifted page and home/end keys on macOS were left to NSOutlineView, which scrolls without moving the cursor, so the next arrow key snapped the view straight back and the page key looked inert. They now run through the handler the shifted keys already used: unshifted replaces the selection with the row landed on, shifted keeps extending, and both move the cursor. GTK and MSW get this from their backends. The search list answered every arriving result with Cleared(), which during a search is continuous. Arrivals now batch per idle into ItemsAdded()/ItemsChanged() on macOS. GTK cannot take them at all -- feeding this model's arrivals in one notification at a time aborts inside GtkTreeView's red-black tree, first from ItemAdded() under a newly formed group and again from the batch of top-level additions after that case was routed away. PR #796 had already found that neither ItemChanged() nor a delete-and-re-add made GTK or MSW re-derive container-ness; two aborts from two different notifications say the constraint is broader than that, so those platforms keep one Cleared() per idle and put the view back afterwards instead. The idle branch already restored the selection and the expanded rows, so the top row joins them -- which works here and not in the virtual lists because a CSearchFile pointer still names the same result after a rebuild, where a row-addressed item names whatever has since moved into it. Measured against wx 3.3.3 rather than argued: Refresh() and SetSelections() move no viewport on either backend; SetCurrentItem() moves it only when the target is off screen, and then onto the cursor; EnsureVisible() after Cleared() restores exactly on GTK and lands at the bottom on macOS. So the cursor is restored only while its row is on screen -- with no row ever clicked GTK's cursor sits at row 0, and restoring it unconditionally clamped the view to the top. GetScrollPos() is unusable on this control: wx asserts that the window is not scrollable and answers 0. Four things review turned up on the way. The synthetic first child of a new group is now announced -- AddChild() creates a copy of the parent as the first child and notified nobody, so a freshly formed group reported one child where the model had two. Both batches de-duplicate, since SetDownloadStatus() notifies every child of the parent it updated and a single arrival into a 50-variant group queued 51 entries. Pending pointers are dropped through MuleNotify::SearchFileBeingDestroyed, which CCommentDialogLst already consumes for this purpose, replacing a guard that checked membership in a list nothing ever unlinks from. And SortList() restores the cursor by identity alongside the selection, which Reset() used to invalidate for it. Builds clean on macOS ARM64, Ubuntu ARM64 and Windows ARM64, with clang-format and both clang-tidy tiers clean over the diff. Confirmed interactively on amulegui on all three: sorting holds position in shared files and clients, the search list holds position while results stream in, group expansion is intact, and the macOS page keys move the cursor with the view.
Summary
Ports
CSearchListCtrl(the search-results list) fromwxListCtrl(viaCMuleListCtrl) towxDataViewCtrl, for screen-reader accessibility (#180 phase 2, part of #675). This is PR 2 of the two-PR plan agreed in #180 — PR 1 (#787, merged) extracted column persistence intoCListColumnStore/IColumnWidthProviderso this port could reuse it unchanged.CMuleListCtrlwrapsMuleExtern::wxGenericListCtrl, a vendored copy of wx's generic (owner-drawn) list control with no native platform widget behind it — VoiceOver/Orca have nothing to attach to.wxDataViewCtrlis natively backed on GTK and macOS (wxHAS_NATIVE_DATAVIEWCTRL).What changed
src/SearchListModel.{h,cpp}: awxDataViewModelpresenting the existingCSearchFileparent/children forest as a native tree.CSearchFile::GetParent()/GetChildren()drive it directly; expand/collapse state is now owned by the control itself (IsExpanded()), replacing the hand-drawn tree in the oldOnDrawItem(manualDrawLine/DrawCircleconnectors) and theCSearchFile::ShowChildren()/SetShowChildren()bookkeeping that faked expand state via row insertion/removal.CSearchListCtrlnow inheritswxDataViewCtrldirectly rather thanCMuleListCtrl, per the Accessibility bug report: Search results list is invisible with VoiceOver on macOS #180 discussion (generalizingCMuleListCtrlitself was explicitly rejected, to avoid pulling Downloads/Servers/Shared Files into scope). Column persistence is reused unchanged viaCListColumnStore/IColumnWidthProviderthrough a smallColumnWidthAdapterobject (not multiple inheritance likeCMuleListCtrl, sincewxDataViewCtrl::GetColumnCount()is itself virtual with an incompatible signature).wxDataViewIconTextRendererviaAppendIconTextColumn(), already precedented in-tree (PrefsUnifiedDlg's sidebar), instead of a custom renderer.FindItem()-based O(n) pointer-to-row lookups are gone: awxDataViewItemcarries theCSearchFile*directly as its ID.Behaviour notes (flagged for review)
IsContainer()/GetChildren()) rather than removing/reinsertingwxListCtrlrows. A parent that fails its own filter but has a filter-passing child is now always shown as a container regardless of current expand state, whereas the old code only kept it visible while its children were already expanded — a deliberate simplification now that expand/collapse is a pure display concern owned by the control.CSearchListCtrl's own chain (m_sort_orders) rather thanwxDataViewCtrl's native single-criterion sort state; header clicks are intercepted and replicate the exact ascending/descending/alt cycleCMuleListCtrl::OnColumnLClickused.EVT_LIST_COL_END_DRAG) has no portablewxDataViewCtrlequivalent event, so it's detected via idle-time width polling instead (CSearchListCtrl::OnIdle, gated onwxWS_EX_PROCESS_IDLE).Test plan
amuletarget builds clean (macOS)clang-formatv18 (pinned) applied;clang-tidyTier-1 (whole-tree) + Tier-2 (changed-lines) both cleanwxWS_EX_PROCESS_IDLE; (2) closing tabs in a different order than a resize happened in could drop the resize -- fixed by syncing outward from a tab's destructor before it's removed from the tab list. Confirmed the resize survives a full clean app exit.UpdateItemColorpalette logic).