diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-20 12:35:36 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-20 12:35:36 +0200 |
| commit | 77ac3ea8c1f365cf7d4691c7580d64d0e7c7c720 (patch) | |
| tree | bbc601911aa009a1331ff74183e891527ca9df5c | |
| parent | f2bc23df84907a361e7d9fa20bf1bc8f7a916037 (diff) | |
| download | sb-builder-77ac3ea8c1f365cf7d4691c7580d64d0e7c7c720.tar.gz sb-builder-77ac3ea8c1f365cf7d4691c7580d64d0e7c7c720.zip | |
Packages: fix segfault on Clear / Select visible behind a filter
Both emitted layoutChanged without layoutAboutToBeChanged; with a filter
on and a current row, the proxy kept stale persistent indexes and Qt
crashed (Ctrl+Shift+A with orphaned + cat:games closed the app). Only
check states change, so the model now emits dataChanged on the box
column. The new test segfaults on the old code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | sb_builder/packages.py | 16 | ||||
| -rw-r--r-- | tests/test_gui.py | 22 |
2 files changed, 34 insertions, 4 deletions
diff --git a/sb_builder/packages.py b/sb_builder/packages.py index ab6f3ce..68de7c2 100644 --- a/sb_builder/packages.py +++ b/sb_builder/packages.py @@ -112,6 +112,16 @@ class Model(QAbstractTableModel): self.checked &= names self.endResetModel() + def boxes_changed(self): + """Only check states changed: dataChanged on the box column (count follows). + + Not layoutChanged: emitted without layoutAboutToBeChanged it leaves the + filtering proxy with stale persistent indexes, and Qt segfaults. + """ + if self.rows: + self.dataChanged.emit(self.index(0, 0), self.index(len(self.rows) - 1, 0), + [Qt.ItemDataRole.CheckStateRole]) + def rowCount(self, parent=QModelIndex()): return 0 if parent.isValid() else len(self.rows) @@ -337,13 +347,11 @@ class PackagesTab(QWidget): def select_visible(self): for i in range(self.proxy.rowCount()): self.model.checked.add(self.proxy.index(i, 0).data(Qt.ItemDataRole.UserRole)["name"]) - self.model.layoutChanged.emit() - self.update_count() + self.model.boxes_changed() def clear(self): self.model.checked.clear() - self.model.layoutChanged.emit() - self.update_count() + self.model.boxes_changed() def targets(self): """Ticked packages, sorted; the highlighted row alone is not a target.""" diff --git a/tests/test_gui.py b/tests/test_gui.py index bd08e7f..ca51c3c 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -651,6 +651,28 @@ class SmokeTest(unittest.TestCase): w.hint_dialogs[0].build() self.assertIn(("job-start", "slackrepo", "update", "--preview", "nuclei"), remote.calls) + def test_clear_with_filter_and_current_row(self): + # regression: Clear emitted layoutChanged with no layoutAboutToBeChanged; + # behind a filter, with a current row, the proxy's stale indexes segfaulted Qt + rows = [inv_row(f"games/g{i}", maintainer="orphaned - no maintainer" if i % 2 else "A") + for i in range(40)] + [inv_row(f"misc/m{i}") for i in range(40)] + w, _ = self.window(inventory=({"columns": COLS, "rows": rows}, None)) + w.show() + tab = w.open_packages() + tab.chips["orphaned"].setChecked(True) + tab.query.setText("cat:games") + for i in range(3): + tab.table.clicked.emit(tab.proxy.index(i, 1)) + tab.table.setCurrentIndex(tab.proxy.index(2, 1)) + app.processEvents() + select, clear = tab.keys + clear.activated.emit() + app.processEvents() + self.assertEqual(tab.count.text(), "20 shown · 0 selected") + select.activated.emit() + app.processEvents() + self.assertEqual(tab.count.text(), "20 shown · 20 selected") + def test_packages_new_hint(self): w, remote = self.window(**{"job-start": ({"id": "20260101-000001-hint-new"}, None)}) w.poll_jobs() |
