Skip to content

Refactor feature table tab to add filters - #6

Merged
robertsamples merged 7 commits into
mainfrom
feature-tree-filters
Jun 29, 2026
Merged

Refactor feature table tab to add filters#6
robertsamples merged 7 commits into
mainfrom
feature-tree-filters

Conversation

@robertsamples

Copy link
Copy Markdown
Owner

Table now filterable based on names and ranges for numerical values

robertsamples and others added 7 commits June 28, 2026 21:45
First layer of the per-column-filter feature tree request: TextFilter
(substring), RangeFilter (inclusive numeric range, either bound
optional), and CategoryFilter (multi-select over space-joined tokens,
e.g. the 'groups' column's ' Blanks 0um_Ce' format) plus row_passes()
to combine them and distinct_category_tokens() to populate a checkbox
filter's option list from real data.

Kept deliberately separate from the Qt model/proxy/widget layer (next)
so the actual matching rules are unit-tested without a GUI -- the Qt
layer will just be plumbing that calls into this.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… tests

Big finding while building this: PyQt5 runs headlessly via the
"offscreen" QPA platform plugin in this environment. That doesn't make
main.py importable standalone (the documented main<->ui_functions
circular import is a separate, unrelated constraint and still applies)
-- but it does mean a standalone Qt-coupled module that doesn't import
main.py can have its actual widget/model/signal behaviour covered by
real automated tests instead of relying solely on manual testing.
conftest.py now sets QT_QPA_PLATFORM=offscreen and provides a
session-scoped `qapp` fixture (one shared QApplication; constructing a
second one in the same process crashes).

searchtree.py replaces ui_main.py's Designer-created QTreeWidget with a
QTreeView + QAbstractTableModel (IonTableModel) + QSortFilterProxyModel
(IonFilterProxyModel) + a per-column filter bar (SearchTreePanel) --
text search for the Compound column, numeric min/max range for m/z/TR/
Max/FC/Hits, and multi-select checkboxes for the Sets/Groups columns
(space-joined category tokens). Filter-matching rules themselves live
in treefilters.py (already merged, Qt-free); this module is just the
Qt plumbing wired to it.

Does not edit any generated UI file: SearchTreePanel removes the
Designer QTreeWidget from its layout and inserts a container (filter
bar + QTreeView) in the same slot at runtime, the same pattern
plotting.py already uses for matplotlib canvases substituted into
Designer-created QFrame placeholders. Re-opening the .ui file in Qt
Designer would still show the old QTreeWidget -- only the running app
uses this.

16 new tests cover the model's raw-vs-display value distinction
(numeric sort/filter against real numbers, not formatted display
strings), every filter type individually and combined, and -- the part
that actually matters most here -- the real runtime widget swap itself
end to end: constructing a real QTreeWidget inside a real layout,
replacing it, populating/filtering/selecting rows, and confirming
selection correctly maps a (filtered, reordered) proxy row back to the
right source row.

Not yet wired into main.py/ui_functions.py -- next commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…reeWidget

self.searchtree = SearchTreePanel(self.ui.treeWidget) constructed once in
MainWindow.__init__, right where the old itemSelectionChanged connection
used to be (before uiDefinitions() runs, which is where the column-hide
call also needed updating to self.searchtree.view.hideColumn(5)).

fillfttree() now builds plain row tuples (Compound, m/z, TR, Max, Sets,
Groups, FC, Hits -- raw values, not pre-formatted strings) and calls
self.searchtree.set_rows() instead of clear()/addTopLevelItem() in a
loop. on_tree_item_selection_changed() reads self.searchtree.selected_
compound() instead of treeWidget.selectedItems(). Removed the now-fully-
unused NumericalTreeWidgetItem class (the new model/proxy sort
numerically via raw typed values instead of parsing display strings, so
the old __lt__-override hack isn't needed) and a dead local `itemdict`
variable in fillfttree() that was built but never read.

The OTHER tree widget (self.ftrdialog.ui.treeWidget, the per-feature
NPAtlas "Hits" results list in the compound-details dialog, populated by
runsearch()) is untouched -- different widget, different dialog, out of
scope for this request.

127 tests passing (includes the 16 added for searchtree.py/treefilters.py
last commit). This is the part that needs your hands-on testing: open
the feature search tab, confirm the per-column filter bar appears and
works (text search on Compound, min/max range on the numeric columns,
checkbox dropdown on Sets/Groups), and that selecting a row still
highlights the feature everywhere else as before.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The new QTreeView is a different widget instance than the Designer-
created QTreeWidget it replaces, so it never inherited the dark-theme
stylesheet setupUi() had already applied to the original (rgb(70,70,70)
borders/backgrounds, rgb(200,200,200) light-grey text) -- it rendered
with default (light) Qt widget styling instead, clashing badly with the
rest of the app.

Now copies old_tree_widget.styleSheet() onto the new view before
discarding the old widget, rather than guessing/hardcoding colours that
could drift from whatever Designer actually has set. The new filter-bar
widgets (text boxes, range inputs, category dropdown buttons/menus) get
a matching dark stylesheet of their own, since there's no pre-existing
widget to copy from for those.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8 columns' worth of filter widgets (1 text box, 5 numeric min/max
pairs, 2 category dropdowns -- 13 widgets) doesn't fit comfortably in
one horizontal strip above a tree that already lives in a narrow side
panel. Restructured into a single-line "Filters ▸" toggle button that
shows/hides a vertical panel (QFormLayout: one filter per row, each
with the column name as its label and room to breathe) instead of
fighting for horizontal space. Collapsed by default, so nothing changes
for anyone who doesn't open it.

Removed the now-redundant per-widget labels (the text filter's
placeholder no longer repeats the column name, the range filter drops
its inline QLabel, the category button just says "Select...") since
the form row itself already shows the column name.

Caught a real Qt subtlety while testing the toggle: QWidget.isVisible()
reflects the whole ancestor chain, not just a widget's own explicit
visibility flag -- the toggle test failed until the test's host widget
was actually shown, which is a good reminder of exactly why testing
this for real (offscreen Qt) is worth more than reading the code and
assuming it works.

130 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added setAlternatingRowColors(True) myself, unrequested, when building
this. The original QTreeWidget never set it (confirmed: zero references
anywhere in the codebase), and the app's QPalette was never themed dark
for the AlternateBase role -- only the QSS stylesheets carry the dark
theme -- so Qt's default light-grey alternate colour showed through
starkly against everything else, looking far more intense than intended.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test_searchtree.py imports PyQt5 directly -- the CI install step never
needed it before (everything tested was Qt-free), so collection failed
outright on every OS/Python combination with ModuleNotFoundError:
No module named 'PyQt5'.

PyQt5 wheels bundle their own Qt libs including the "offscreen" platform
plugin conftest.py already selects, so no Xvfb/real display is needed --
but on a minimal Ubuntu CI image they still dynamically link against a
few system libraries (libgl1, libxkbcommon-x11-0, libxcb-cursor0) the
base image doesn't ship, so added those via apt on Linux specifically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@robertsamples
robertsamples merged commit d784f5f into main Jun 29, 2026
7 checks passed
robertsamples added a commit that referenced this pull request Jun 30, 2026
Refactor feature table tab to add filters
@robertsamples
robertsamples deleted the feature-tree-filters branch June 30, 2026 21:27
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