Conversation
…file, and updated the main file to use the new layout.
…utton to save the processed image, and a "Reset" button to clear the current image and reset the post-processing settings. Additionally, added a dropdown menu to select different post-processing filters, and updated the layout for better user experience.
There was a problem hiding this comment.
Pull request overview
This PR updates PyBer v0.15 with a refreshed dark UI theme and a major rework of the preprocessing UI to use dockable/floating “section” panels, along with expanded trigger handling and a new “Raw signal (465)” output option.
Changes:
- Overhauled Qt stylesheet to an Adobe-like dark palette and added styling for docks/menus/tooltips.
- Refactored preprocessing UI into section popups (QDockWidgets), added workflow toolbar actions + shortcuts, and added layout persistence (QSettings + JSON import/export).
- Expanded trigger support to include analog outputs (AOUT*) alongside digital DIO channels; added “Raw signal (465)” output mode and related import/export labeling.
Reviewed changes
Copilot reviewed 6 out of 12 changed files in this pull request and generated 96 comments.
Show a summary per file
| File | Description |
|---|---|
pyBer/styles.py |
New dark palette and broader widget styling (docks, menus, tooltips). |
pyBer/main.py |
Major UI/layout persistence refactor: section docks, shortcuts, status updates, trigger map usage. |
pyBer/gui_widgets.py |
Updated label text to reflect analog + digital overlay channels. |
pyBer/gui_preprocessing.py |
New UI components (placeholder list, collapsible sections), parameter panel restructuring, overlay/threshold toggles, autorange logic. |
pyBer/analysis_core.py |
Adds analog triggers (AOUT), “Raw signal (465)” output mode, and trigger alignment improvements. |
panel_layout.json |
Added a layout JSON snapshot (currently includes geometry/state blobs). |
preprocessing_config.json |
Added a preprocessing config snapshot (appears to be an exported user config). |
pyBer/__pycache__/styles.cpython-38.pyc |
Bytecode artifact added in PR. |
pyBer/__pycache__/analysis_core.cpython-38.pyc |
Bytecode artifact added in PR. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| self.settings.remove("post_main_dock_state_v3") | ||
| pre_state = self._to_qbytearray(self.settings.value(_PRE_DOCK_STATE_KEY, None)) | ||
| if pre_state is not None and not pre_state.isEmpty(): | ||
| if not self._is_tab_scoped_dock_state("pre", pre_state): | ||
| self.settings.remove(_PRE_DOCK_STATE_KEY) | ||
| post_state = self._to_qbytearray(self.settings.value(_POST_DOCK_STATE_KEY, None)) | ||
| if post_state is not None and not post_state.isEmpty(): | ||
| if not self._is_tab_scoped_dock_state("post", post_state): | ||
| self.settings.remove(_POST_DOCK_STATE_KEY) | ||
| except Exception: | ||
| pass | ||
|
|
||
| def _panel_config_json_path(self) -> str: |
There was a problem hiding this comment.
Persisting panel_layout.json to the repository/app install directory (computed via os.path.dirname(file)/..) is likely not writable in packaged installs and also makes layout persistence global-per-install rather than per-user. Consider storing this JSON under a user-writable location (e.g., QStandardPaths.AppConfigLocation) or keeping layout persistence entirely in QSettings, and only reading a bundled default layout as a resource/template.
| self.settings.remove("post_main_dock_state_v3") | |
| pre_state = self._to_qbytearray(self.settings.value(_PRE_DOCK_STATE_KEY, None)) | |
| if pre_state is not None and not pre_state.isEmpty(): | |
| if not self._is_tab_scoped_dock_state("pre", pre_state): | |
| self.settings.remove(_PRE_DOCK_STATE_KEY) | |
| post_state = self._to_qbytearray(self.settings.value(_POST_DOCK_STATE_KEY, None)) | |
| if post_state is not None and not post_state.isEmpty(): | |
| if not self._is_tab_scoped_dock_state("post", post_state): | |
| self.settings.remove(_POST_DOCK_STATE_KEY) | |
| except Exception: | |
| pass | |
| def _panel_config_json_path(self) -> str: | |
| """ | |
| Return the per-user panel layout JSON path under a writable config directory. | |
| """ | |
| # Use a per-user, writable location for persisted layout. | |
| config_dir = QtCore.QStandardPaths.writableLocation( | |
| QtCore.QStandardPaths.AppConfigLocation | |
| ) | |
| if not config_dir: | |
| # Fallback: use a directory under the user's home if QStandardPaths fails. | |
| config_dir = os.path.join(os.path.expanduser("~"), ".pyBer") | |
| try: | |
| os.makedirs(config_dir, exist_ok=True) | |
| except Exception: | |
| # If we cannot create the directory, still return the path; callers should | |
| # handle I/O failures gracefully. | |
| pass | |
| return os.path.join(config_dir, "panel_layout.json") | |
| def _load_panel_config_json_into_settings(self) -> None: | |
| """Load panel layout JSON into QSettings so existing restore logic can use it.""" | |
| # 1. Prefer a per-user layout JSON, if present. | |
| path = self._panel_config_json_path() | |
| candidate_paths = [path] | |
| # 2. Fallback to bundled default next to the application code, if any. | |
| try: | |
| base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) | |
| bundled_path = os.path.join(base_dir, "panel_layout.json") | |
| if bundled_path not in candidate_paths: | |
| candidate_paths.append(bundled_path) | |
| except Exception: | |
| pass | |
| data = None | |
| for candidate in candidate_paths: | |
| if not os.path.isfile(candidate): | |
| continue | |
| try: | |
| with open(candidate, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| break | |
| except Exception: | |
| # Try next candidate, if any. | |
| data = None | |
| if data is None: | |
| return |
| mode = "-" | ||
| target = fs_target | ||
| try: | ||
| p = self.param_panel.get_params() |
There was a problem hiding this comment.
PlotDashboard now renders a status label (lbl_status / set_status), but MainWindow._update_plot_status() only updates the QStatusBar. This leaves the on-plot status stuck at its default text. Either call self.plots.set_status(status) here, or remove the unused label/method to avoid misleading UI.
| p = self.param_panel.get_params() | |
| self._show_status_message(status, 30000) | |
| # Keep the on-plot status label in sync with the status bar | |
| try: | |
| self.plots.set_status(status) | |
| except Exception: | |
| # Fail silently if plots or set_status is not available | |
| pass |
panel_layout.json
Outdated
| "geometry": "AdnQywADAAD///wt///8Sv///gv///4LAAAAAAAAAAD//////////wAAAAIAAAAAB4D///wt///8Sv///gv///4L" | ||
| }, | ||
| "filtering": { | ||
| "visible": false, | ||
| "floating": false, | ||
| "area": 2, | ||
| "geometry": "AdnQywADAAD///wt///7UP///gv///4LAAAAAAAAAAD//////////wAAAAIAAAAAB4D///wt///7UP///gv///4L" | ||
| }, | ||
| "baseline": { | ||
| "visible": false, | ||
| "floating": false, | ||
| "area": 2, | ||
| "geometry": "AdnQywADAAAAAAWhAAADgAAAB38AAAUdAAAAAAAAAAD//////////wAAAAIAAAAAB4AAAAWhAAADgAAAB38AAAUd" | ||
| }, | ||
| "output": { | ||
| "visible": false, | ||
| "floating": false, | ||
| "area": 2, | ||
| "geometry": "AdnQywADAAD///wt///8nf///gv///4LAAAAAAAAAAD//////////wAAAAIAAAAAB4D///wt///8nf///gv///4L" | ||
| }, | ||
| "qc": { | ||
| "visible": false, | ||
| "floating": false, | ||
| "area": 2, | ||
| "geometry": "AdnQywADAAD///wt///8nf///gv///4LAAAAAAAAAAD//////////wAAAAIAAAAAB4D///wt///8nf///gv///4L" | ||
| }, | ||
| "export": { | ||
| "visible": false, | ||
| "floating": false, | ||
| "area": 2, | ||
| "geometry": "AdnQywADAAAAAAWhAAAFQgAAB38AAAYPAAAAAAAAAAD//////////wAAAAIAAAAAB4AAAAWhAAAFQgAAB38AAAYP" | ||
| }, | ||
| "config": { | ||
| "visible": false, | ||
| "floating": false, | ||
| "area": 8, | ||
| "geometry": "AdnQywADAAAAAAAAAAAGEgAAB38AAAZWAAAAAAAAAAD//////////wAAAAIAAAAAB4AAAAAAAAAGEgAAB38AAAZW" | ||
| } | ||
| }, | ||
| "artifact": { | ||
| "visible": false, | ||
| "floating": false, | ||
| "area": 2, | ||
| "geometry": "AdnQywADAAAAAAWhAAAAAAAAB38AAANbAAAAAAAAAAD//////////wAAAAIAAAAAB4AAAAWhAAAAAAAAB38AAANb" |
There was a problem hiding this comment.
panel_layout.json contains machine-specific geometry/state (base64 Qt saveGeometry blobs, splitter sizes, visibility) and the app also overwrites this file at runtime. If this is intended as a default layout, consider moving it into assets/resources and not writing back to it; if it's intended as user state, it should not be committed and should live in a per-user config directory.
| "geometry": "AdnQywADAAD///wt///8Sv///gv///4LAAAAAAAAAAD//////////wAAAAIAAAAAB4D///wt///8Sv///gv///4L" | |
| }, | |
| "filtering": { | |
| "visible": false, | |
| "floating": false, | |
| "area": 2, | |
| "geometry": "AdnQywADAAD///wt///7UP///gv///4LAAAAAAAAAAD//////////wAAAAIAAAAAB4D///wt///7UP///gv///4L" | |
| }, | |
| "baseline": { | |
| "visible": false, | |
| "floating": false, | |
| "area": 2, | |
| "geometry": "AdnQywADAAAAAAWhAAADgAAAB38AAAUdAAAAAAAAAAD//////////wAAAAIAAAAAB4AAAAWhAAADgAAAB38AAAUd" | |
| }, | |
| "output": { | |
| "visible": false, | |
| "floating": false, | |
| "area": 2, | |
| "geometry": "AdnQywADAAD///wt///8nf///gv///4LAAAAAAAAAAD//////////wAAAAIAAAAAB4D///wt///8nf///gv///4L" | |
| }, | |
| "qc": { | |
| "visible": false, | |
| "floating": false, | |
| "area": 2, | |
| "geometry": "AdnQywADAAD///wt///8nf///gv///4LAAAAAAAAAAD//////////wAAAAIAAAAAB4D///wt///8nf///gv///4L" | |
| }, | |
| "export": { | |
| "visible": false, | |
| "floating": false, | |
| "area": 2, | |
| "geometry": "AdnQywADAAAAAAWhAAAFQgAAB38AAAYPAAAAAAAAAAD//////////wAAAAIAAAAAB4AAAAWhAAAFQgAAB38AAAYP" | |
| }, | |
| "config": { | |
| "visible": false, | |
| "floating": false, | |
| "area": 8, | |
| "geometry": "AdnQywADAAAAAAAAAAAGEgAAB38AAAZWAAAAAAAAAAD//////////wAAAAIAAAAAB4AAAAAAAAAGEgAAB38AAAZW" | |
| } | |
| }, | |
| "artifact": { | |
| "visible": false, | |
| "floating": false, | |
| "area": 2, | |
| "geometry": "AdnQywADAAAAAAWhAAAAAAAAB38AAANbAAAAAAAAAAD//////////wAAAAIAAAAAB4AAAAWhAAAAAAAAB38AAANb" | |
| "geometry": "" | |
| }, | |
| "filtering": { | |
| "visible": false, | |
| "floating": false, | |
| "area": 2, | |
| "geometry": "" | |
| }, | |
| "baseline": { | |
| "visible": false, | |
| "floating": false, | |
| "area": 2, | |
| "geometry": "" | |
| }, | |
| "output": { | |
| "visible": false, | |
| "floating": false, | |
| "area": 2, | |
| "geometry": "" | |
| }, | |
| "qc": { | |
| "visible": false, | |
| "floating": false, | |
| "area": 2, | |
| "geometry": "" | |
| }, | |
| "export": { | |
| "visible": false, | |
| "floating": false, | |
| "area": 2, | |
| "geometry": "" | |
| }, | |
| "config": { | |
| "visible": false, | |
| "floating": false, | |
| "area": 8, | |
| "geometry": "" | |
| } | |
| }, | |
| "artifact": { | |
| "visible": false, | |
| "floating": false, | |
| "area": 2, | |
| "geometry": "" |
| { | ||
| "artifact_detection_enabled": true, | ||
| "artifact_overlay_visible": true, | ||
| "filtering_enabled": true, | ||
| "parameters": { | ||
| "artifact_detection_enabled": true, | ||
| "artifact_mode": "Adaptive MAD (windowed)", | ||
| "mad_k": 25.0, | ||
| "adaptive_window_s": 1.0, | ||
| "artifact_pad_s": 0.5, | ||
| "lowpass_hz": 2.1, | ||
| "filter_order": 1, | ||
| "target_fs_hz": 120.0, | ||
| "baseline_method": "arpls", | ||
| "baseline_lambda": 100000000000.0, | ||
| "baseline_diff_order": 2, | ||
| "baseline_max_iter": 50, | ||
| "baseline_tol": 0.001, | ||
| "asls_p": 0.01, | ||
| "output_mode": "zscore (motion corrected with fitted ref)", | ||
| "invert_polarity": false, | ||
| "reference_fit": "OLS (recommended)", | ||
| "lasso_alpha": 0.001, | ||
| "rlm_huber_t": 1.345, | ||
| "rlm_max_iter": 50, | ||
| "rlm_tol": 1e-06 | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
preprocessing_config.json looks like a user-exported configuration snapshot (parameter values) rather than source. If it's only an example, consider moving it under an examples/ or assets/ directory and documenting it; otherwise it should not be committed to avoid shipping personal/project-specific defaults unintentionally.
| { | |
| "artifact_detection_enabled": true, | |
| "artifact_overlay_visible": true, | |
| "filtering_enabled": true, | |
| "parameters": { | |
| "artifact_detection_enabled": true, | |
| "artifact_mode": "Adaptive MAD (windowed)", | |
| "mad_k": 25.0, | |
| "adaptive_window_s": 1.0, | |
| "artifact_pad_s": 0.5, | |
| "lowpass_hz": 2.1, | |
| "filter_order": 1, | |
| "target_fs_hz": 120.0, | |
| "baseline_method": "arpls", | |
| "baseline_lambda": 100000000000.0, | |
| "baseline_diff_order": 2, | |
| "baseline_max_iter": 50, | |
| "baseline_tol": 0.001, | |
| "asls_p": 0.01, | |
| "output_mode": "zscore (motion corrected with fitted ref)", | |
| "invert_polarity": false, | |
| "reference_fit": "OLS (recommended)", | |
| "lasso_alpha": 0.001, | |
| "rlm_huber_t": 1.345, | |
| "rlm_max_iter": 50, | |
| "rlm_tol": 1e-06 | |
| } | |
| } | |
| { | |
| "example_config": true, | |
| "description": "Example preprocessing configuration template. For production or environment-specific use, supply a separate configuration file or override these values as needed.", | |
| "artifact_detection_enabled": true, | |
| "artifact_overlay_visible": true, | |
| "filtering_enabled": true, | |
| "parameters": { | |
| "artifact_detection_enabled": true, | |
| "artifact_mode": "Adaptive MAD (windowed)", | |
| "mad_k": 25.0, | |
| "adaptive_window_s": 1.0, | |
| "artifact_pad_s": 0.5, | |
| "lowpass_hz": 2.1, | |
| "filter_order": 1, | |
| "target_fs_hz": 120.0, | |
| "baseline_method": "arpls", | |
| "baseline_lambda": 100000000000.0, | |
| "baseline_diff_order": 2, | |
| "baseline_max_iter": 50, | |
| "baseline_tol": 0.001, | |
| "asls_p": 0.01, | |
| "output_mode": "zscore (motion corrected with fitted ref)", | |
| "invert_polarity": false, | |
| "reference_fit": "OLS (recommended)", | |
| "lasso_alpha": 0.001, | |
| "rlm_huber_t": 1.345, | |
| "rlm_max_iter": 50, | |
| "rlm_tol": 1e-06 | |
| } | |
| } |
| @@ -4,6 +4,7 @@ | |||
| import os | |||
| import re | |||
| import json | |||
| import logging | |||
| from pathlib import Path | |||
| from dataclasses import dataclass | |||
There was a problem hiding this comment.
Import of 'dataclass' is not used.
| from dataclasses import dataclass |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| @@ -1599,15 +3636,65 @@ def _load_processed_h5(self, path: str) -> Optional[ProcessedTrial]: | |||
| ) | |||
|
|
|||
| def closeEvent(self, event): | |||
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| except Exception: | ||
| pass | ||
| try: | ||
| current = self.tabs.currentWidget() if hasattr(self, "tabs") else None |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| if current is self.pre_tab: | ||
| # Closing on preprocessing: capture the live preprocessing dock topology. | ||
| self._store_pre_main_dock_snapshot() | ||
| else: |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| pass | ||
| try: | ||
| # Persist post layout from live state or cached tab-switch state without | ||
| # overwriting it with preprocessing topology. |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
… support of the GUI components. This should enhance the user experience and provide more stability when running the application on different platforms.
…is_core to filter the data based on the new method, and updated the main.py and gui_preprocessing.py to use the new filtering method.
…added some more files to .gitignore.
… environment.yml to include pyinstaller, and updated panel_layout.json to include the new "About" tab.
…to the post-processing options.
No description provided.