Refactor feature table tab to add filters - #6
Merged
Conversation
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
added a commit
that referenced
this pull request
Jun 30, 2026
Refactor feature table tab to add filters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Table now filterable based on names and ranges for numerical values