diff --git a/.github/workflows/qt_viz_tests.yml b/.github/workflows/qt_viz_tests.yml
index a5710299..54f92e98 100644
--- a/.github/workflows/qt_viz_tests.yml
+++ b/.github/workflows/qt_viz_tests.yml
@@ -77,7 +77,7 @@ jobs:
run: | # use MNE main to ensure test files are available
set -e
python -m pip install --upgrade pip
- git clone -b ${MNE_BRANCH} --single-branch --depth=1 https://github.com/mne-tools/mne-python.git ../mne-python
+ git clone -b ${MNE_BRANCH} --single-branch --depth=1 --branch main https://github.com/mne-tools/mne-python.git ../mne-python
python -m pip install -qe ../mne-python
python -m pip install -ve .${PIP_OPTION} -r requirements.txt -r requirements_testing.txt PyQt5
- name: Downgrade pytest for mne==0.24
@@ -115,7 +115,7 @@ jobs:
if: runner.os == 'Linux' && contains(matrix.opengl, 'opengl')
- name: Show system information
run: mne sys_info
- - run: pytest -m pgtest --cov=mne_qt_browser --cov-report=xml ../mne-python/mne/viz
+ - run: pytest -m pgtest --cov=mne_qt_browser --cov-report=xml ../mne-python/mne/viz ../mne-python/mne/report
name: Run MNE-Tests
- run: pytest --error-for-skips mne_qt_browser/tests/test_pg_specific.py
name: Run pyqtgraph-specific tests
@@ -171,7 +171,7 @@ jobs:
run: | # use MNE main to ensure test files are available
set -e
python -m pip install --upgrade pip
- git clone -b ${MNE_BRANCH} --single-branch --depth=1 https://github.com/mne-tools/mne-python.git ../mne-python
+ git clone -b ${MNE_BRANCH} --single-branch --depth=1 --branch main https://github.com/mne-tools/mne-python.git ../mne-python
python -m pip install -qe ../mne-python
python -m pip install -ve .${PIP_OPTION} -r requirements.txt -r requirements_testing.txt $QT_LIB
- shell: bash -el {0}
@@ -197,7 +197,7 @@ jobs:
mne sys_info -pd
echo ${QT_LIB}:
mne sys_info -pd | grep "^qtpy: .*{${QT_LIB}=.*}$"
- - run: pytest -m pgtest --cov=mne_qt_browser --cov-report=xml ../mne-python/mne/viz
+ - run: pytest -m pgtest --cov=mne_qt_browser --cov-report=xml ../mne-python/mne/viz ../mne-python/mne/report
name: Run MNE-Tests
- run: pytest --error-for-skips mne_qt_browser/tests/test_pg_specific.py
name: Run pyqtgraph-specific tests
diff --git a/mne_qt_browser/_pg_figure.py b/mne_qt_browser/_pg_figure.py
index c681aed8..749d8764 100644
--- a/mne_qt_browser/_pg_figure.py
+++ b/mne_qt_browser/_pg_figure.py
@@ -11,6 +11,7 @@
import math
import platform
import sys
+import weakref
from pathlib import Path
from ast import literal_eval
from collections import OrderedDict
@@ -169,8 +170,9 @@ class DataTrace(PlotCurveItem):
def __init__(self, main, ch_idx, child_idx=None, parent_trace=None):
super().__init__()
- self.main = main
+ self.weakmain = weakref.ref(main)
self.mne = main.mne
+ del main
# Set clickable with small area around trace to make clicking easier.
self.setClickable(True, 12)
@@ -255,7 +257,7 @@ def update_color(self):
# Add child traces if necessary
if trace_diff > 0:
for cix in range(n_childs, n_childs + trace_diff):
- child = DataTrace(self.main, self.ch_idx,
+ child = DataTrace(self.weakmain(), self.ch_idx,
child_idx=cix, parent_trace=self)
self.child_traces.append(child)
elif trace_diff < 0:
@@ -370,7 +372,7 @@ def toggle_bad(self, x=None):
"""Toggle bad status."""
# Toggle bad epoch
if self.mne.is_epochs and x is not None:
- epoch_idx, color = self.main._toggle_bad_epoch(x)
+ epoch_idx, color = self.weakmain()._toggle_bad_epoch(x)
# Update epoch color
if color != 'none':
@@ -402,8 +404,8 @@ def toggle_bad(self, x=None):
# Toggle bad channel
else:
- bad_color, pick, marked_bad = self.main._toggle_bad_channel(
- self.range_idx)
+ bad_color, pick, marked_bad = self.weakmain()._toggle_bad_channel(
+ self.range_idx)
# Update line color status
self.isbad = not self.isbad
@@ -429,7 +431,7 @@ def toggle_bad(self, x=None):
self.update_data()
# Update channel-axis
- self.main._update_yaxis_labels()
+ self.weakmain()._update_yaxis_labels()
# Update overview-bar
self.mne.overview_bar.update_bad_channels()
@@ -526,8 +528,9 @@ class ChannelAxis(AxisItem):
"""The Y-Axis displaying the channel-names."""
def __init__(self, main):
- self.main = main
+ self.weakmain = weakref.ref(main)
self.mne = main.mne
+ del main
self.ch_texts = OrderedDict()
super().__init__(orientation='left')
self.style['autoReduceTextSpace'] = False
@@ -543,7 +546,8 @@ def tickStrings(self, values, scale, spacing):
"""Customize strings of axis values."""
# Get channel-names
if self.mne.butterfly and self.mne.fig_selection is not None:
- tick_strings = list(self.main._make_butterfly_selections_dict())
+ tick_strings = list(
+ self.weakmain()._make_butterfly_selections_dict())
elif self.mne.butterfly:
_, ixs, _ = np.intersect1d(DATA_CH_TYPES_ORDER,
self.mne.ch_types, return_indices=True)
@@ -599,7 +603,7 @@ def mouseClickEvent(self, event):
if event.button() == Qt.LeftButton:
trace.toggle_bad()
elif event.button() == Qt.RightButton:
- self.main._create_ch_context_fig(trace.range_idx)
+ self.weakmain()._create_ch_context_fig(trace.range_idx)
def get_labels(self):
"""Get labels for testing."""
@@ -785,8 +789,9 @@ def __init__(self, main):
self._scene = QGraphicsScene()
super().__init__(self._scene)
assert self.scene() is self._scene
- self.main = main
+ self.weakmain = weakref.ref(main)
self.mne = main.mne
+ del main
self.bg_img = None
self.bg_pxmp = None
self.bg_pxmp_item = None
@@ -1210,7 +1215,7 @@ def _mapToData(self, point):
return x, y
def keyPressEvent(self, event):
- self.main.keyPressEvent(event)
+ self.weakmain().keyPressEvent(event)
class RawViewBox(ViewBox):
@@ -1219,8 +1224,9 @@ class RawViewBox(ViewBox):
def __init__(self, main):
super().__init__(invertY=True)
self.enableAutoRange(enable=False, x=False, y=False)
- self.main = main
+ self.weakmain = weakref.ref(main)
self.mne = main.mne
+ del main
self._drag_start = None
self._drag_region = None
@@ -1269,10 +1275,11 @@ def mouseDragEvent(self, event, axis=None):
self._drag_region.setRegion((min(merge_values),
max(merge_values)))
for rm_region in rm_regions:
- self.main._remove_region(rm_region, from_annot=False)
- self.main._add_region(plot_onset, duration,
- self.mne.current_description,
- self._drag_region)
+ self.weakmain()._remove_region(
+ rm_region, from_annot=False)
+ self.weakmain()._add_region(
+ plot_onset, duration, self.mne.current_description,
+ self._drag_region)
self._drag_region.select(True)
# Update Overview-Bar
@@ -1282,10 +1289,10 @@ def mouseDragEvent(self, event, axis=None):
self._drag_region.setRegion((self._drag_start, x_to))
elif event.isFinish():
- self.main.message_box(text='No description!',
- info_text='No description is given, '
- 'add one!',
- icon=QMessageBox.Warning)
+ self.weakmain().message_box(
+ text='No description!',
+ info_text='No description is given, add one!',
+ icon=QMessageBox.Warning)
def mouseClickEvent(self, event):
"""Customize mouse click events."""
@@ -1293,22 +1300,22 @@ def mouseClickEvent(self, event):
# super().mouseClickEvent(event)
if not self.mne.annotation_mode:
if event.button() == Qt.LeftButton:
- self.main._add_vline(self.mapSceneToView(
+ self.weakmain()._add_vline(self.mapSceneToView(
event.scenePos()).x())
elif event.button() == Qt.RightButton:
- self.main._remove_vline()
+ self.weakmain()._remove_vline()
def wheelEvent(self, ev, axis=None):
"""Customize mouse wheel/trackpad-scroll events."""
ev.accept()
scroll = -1 * ev.delta() / 120
if ev.orientation() == Qt.Horizontal:
- self.main.hscroll(scroll * 10)
+ self.weakmain().hscroll(scroll * 10)
elif ev.orientation() == Qt.Vertical:
- self.main.vscroll(scroll)
+ self.weakmain().vscroll(scroll)
def keyPressEvent(self, event):
- self.main.keyPressEvent(event)
+ self.weakmain().keyPressEvent(event)
class VLineLabel(InfLineLabel):
@@ -1500,9 +1507,10 @@ def __init__(self, main, widget=None,
modal=False, name=None, title=None,
flags=Qt.Window | Qt.Tool):
super().__init__(main, flags)
- self.main = main
+ self.weakmain = weakref.ref(main)
self.widget = widget
self.mne = main.mne
+ del main
self.name = name
self.modal = modal
@@ -1553,7 +1561,7 @@ def closeEvent(self, event):
# the main window should be raised as well
def event(self, event):
if event.type() == QEvent.WindowActivate:
- self.main.raise_()
+ self.weakmain().raise_()
return super().event(event)
@@ -1574,7 +1582,7 @@ def __init__(self, main, title='Settings', **kwargs):
' Default is 1.')
self.downsampling_box.setMinimum(0)
self.downsampling_box.setSpecialValueText('Auto')
- self.downsampling_box.valueChanged.connect(partial(
+ self.downsampling_box.valueChanged.connect(_methpartial(
self._value_changed, value_name='downsampling'))
self.downsampling_box.setValue(0 if self.mne.downsampling == 'auto'
else self.mne.downsampling)
@@ -1596,10 +1604,9 @@ def __init__(self, main, title='Settings', **kwargs):
'pyqtgraph)
'
'Default is "peak".')
self.ds_method_cmbx.addItems(['subsample', 'mean', 'peak'])
- self.ds_method_cmbx.currentTextChanged.connect(partial(
- self._value_changed, value_name='ds_method'))
- self.ds_method_cmbx.setCurrentText(
- self.mne.ds_method)
+ self.ds_method_cmbx.currentTextChanged.connect(
+ _methpartial(self._value_changed, value_name='ds_method'))
+ self.ds_method_cmbx.setCurrentText(self.mne.ds_method)
layout.addRow('ds_method', self.ds_method_cmbx)
self.scroll_sensitivity_slider = QSlider(Qt.Horizontal)
@@ -1608,8 +1615,8 @@ def __init__(self, main, title='Settings', **kwargs):
self.scroll_sensitivity_slider.setToolTip('Set the sensitivity of '
'the scrolling in '
'horizontal direction.')
- self.scroll_sensitivity_slider.valueChanged.connect(partial(
- self._value_changed, value_name='scroll_sensitivity'))
+ self.scroll_sensitivity_slider.valueChanged.connect(
+ _methpartial(self._value_changed, value_name='scroll_sensitivity'))
# Set default
self.scroll_sensitivity_slider.setValue(self.mne.scroll_sensitivity)
layout.addRow('horizontal scroll sensitivity',
@@ -1631,7 +1638,7 @@ def _value_changed(self, new_value, value_name):
if value_name == 'scroll_sensitivity':
self.mne.ax_hscroll._update_scroll_sensitivity()
else:
- self.main._redraw()
+ self.weakmain()._redraw()
class HelpDialog(_BaseDialog):
@@ -1669,7 +1676,7 @@ def __init__(self, main, **kwargs):
layout.addWidget(scroll_area)
# Additional help for mouse interaction
- inst = self.main.mne.instance_type
+ inst = self.mne.instance_type
is_raw = inst == 'raw'
is_epo = inst == 'epochs'
is_ica = inst == 'ica'
@@ -1734,7 +1741,7 @@ def __init__(self, main, *, name):
for idx, label in enumerate(labels):
chkbx = QCheckBox(label)
chkbx.setChecked(bool(self.mne.projs_on[idx]))
- chkbx.clicked.connect(partial(self._proj_changed, idx=idx))
+ chkbx.clicked.connect(_methpartial(self._proj_changed, idx=idx))
if self.mne.projs_active[idx]:
chkbx.setEnabled(False)
self.checkboxes.append(chkbx)
@@ -1750,11 +1757,11 @@ def _proj_changed(self, state, idx):
# Only change if proj wasn't already applied.
if not self.mne.projs_active[idx]:
self.mne.projs_on[idx] = state
- self.main._apply_update_projectors()
+ self.weakmain()._apply_update_projectors()
def toggle_all(self):
"""Toggle all projectors."""
- self.main._apply_update_projectors(toggle_all=True)
+ self.weakmain()._apply_update_projectors(toggle_all=True)
# Update all checkboxes
for idx, chkbx in enumerate(self.checkboxes):
@@ -1837,7 +1844,8 @@ def __init__(self, main):
self.chkbxs = OrderedDict()
for label in selections_dict:
chkbx = QCheckBox(label)
- chkbx.clicked.connect(partial(self._chkbx_changed, label))
+ chkbx.clicked.connect(
+ _methpartial(self._chkbx_changed, label=label))
self.chkbxs[label] = chkbx
layout.addWidget(chkbx)
@@ -1863,7 +1871,7 @@ def __init__(self, main):
def _chkbx_changed(self, label):
# Disable butterfly if checkbox is clicked
if self.mne.butterfly:
- self.main._set_butterfly(False)
+ self.weakmain()._set_butterfly(False)
# Disable other checkboxes
for chkbx in self.chkbxs.values():
chkbx.setChecked(False)
@@ -1968,9 +1976,10 @@ def closeEvent(self, event):
# MNE >= 1.0
self.channel_fig.lasso.callbacks.clear()
for chkbx in self.chkbxs.values():
- _disconnect(chkbx.clicked)
- if hasattr(self, 'main'):
- self.main.close()
+ _disconnect(chkbx.clicked, allow_error=True)
+ main = self.weakmain()
+ if main is not None:
+ main.close()
class AnnotRegion(LinearRegionItem):
@@ -2127,13 +2136,24 @@ def _edit(self):
self.close()
+def _select_all(chkbxs):
+ for chkbx in chkbxs:
+ chkbx.setChecked(True)
+
+
+def _clear_all(chkbxs):
+ for chkbx in chkbxs:
+ chkbx.setChecked(False)
+
+
class AnnotationDock(QDockWidget):
"""Dock-Window for Management of annotations."""
def __init__(self, main):
super().__init__('Annotations')
- self.main = main
+ self.weakmain = weakref.ref(main)
self.mne = main.mne
+ del main
self._init_ui()
self.setFeatures(QDockWidget.DockWidgetMovable |
@@ -2206,7 +2226,7 @@ def _add_description_to_cmbx(self, description):
def _add_description(self, new_description):
self.mne.new_annotation_labels.append(new_description)
self.mne.visible_annotations[new_description] = True
- self.main._setup_annotation_colors()
+ self.weakmain()._setup_annotation_colors()
self._add_description_to_cmbx(new_description)
self.mne.current_description = new_description
self.description_cmbx.setCurrentText(new_description)
@@ -2226,19 +2246,20 @@ def _edit_description_all(self, new_des):
if r.description == old_des]
# Update regions & annotations
for ed_region in edit_regions:
- idx = self.main._get_onset_idx(ed_region.getRegion()[0])
+ idx = self.weakmain()._get_onset_idx(ed_region.getRegion()[0])
self.mne.inst.annotations.description[idx] = new_des
ed_region.update_description(new_des)
# Update containers with annotation-attributes
self.mne.new_annotation_labels.remove(old_des)
- self.mne.new_annotation_labels = self.main._get_annotation_labels()
+ self.mne.new_annotation_labels = \
+ self.weakmain()._get_annotation_labels()
self.mne.visible_annotations[new_des] = \
self.mne.visible_annotations.pop(old_des)
self.mne.annotation_segment_colors[new_des] = \
self.mne.annotation_segment_colors.pop(old_des)
# Update related widgets
- self.main._setup_annotation_colors()
+ self.weakmain()._setup_annotation_colors()
self._update_regions_colors()
self._update_description_cmbx()
self.mne.overview_bar.update_annotations()
@@ -2246,7 +2267,8 @@ def _edit_description_all(self, new_des):
def _edit_description_selected(self, new_des):
"""Update description only of selected region."""
old_des = self.mne.selected_region.description
- idx = self.main._get_onset_idx(self.mne.selected_region.getRegion()[0])
+ idx = self.weakmain()._get_onset_idx(
+ self.mne.selected_region.getRegion()[0])
# Update regions & annotations
self.mne.inst.annotations.description[idx] = new_des
self.mne.selected_region.update_description(new_des)
@@ -2262,7 +2284,7 @@ def _edit_description_selected(self, new_des):
self.mne.annotation_segment_colors.pop(old_des)
# Update related widgets
- self.main._setup_annotation_colors()
+ self.weakmain()._setup_annotation_colors()
self._update_regions_colors()
self._update_description_cmbx()
self.mne.overview_bar.update_annotations()
@@ -2271,10 +2293,10 @@ def _edit_description_dlg(self):
if len(self.mne.inst.annotations.description) > 0:
_AnnotEditDialog(self)
else:
- self.main.message_box(text='No Annotations!',
- info_text='There are no annotations '
- 'yet to edit!',
- icon=QMessageBox.Information)
+ self.weakmain().message_box(
+ text='No Annotations!',
+ info_text='There are no annotations yet to edit!',
+ icon=QMessageBox.Information)
def _remove_description(self, rm_description):
# Remove regions
@@ -2309,28 +2331,19 @@ def _remove_description_dlg(self):
f'"{rm_description}".\n' \
f'Do you really want to remove them?'
buttons = QMessageBox.Yes | QMessageBox.No
- ans = self.main.message_box(text=text, info_text=info_text,
- buttons=buttons,
- default_button=QMessageBox.Yes,
- icon=QMessageBox.Question)
+ ans = self.weakmain().message_box(
+ text=text, info_text=info_text, buttons=buttons,
+ default_button=QMessageBox.Yes, icon=QMessageBox.Question)
else:
ans = QMessageBox.Yes
if ans == QMessageBox.Yes:
self._remove_description(rm_description)
- def _select_annotations(self):
- def _set_visible_region(state, description):
- self.mne.visible_annotations[description] = bool(state)
-
- def _select_all():
- for chkbx in chkbxs:
- chkbx.setChecked(True)
-
- def _clear_all():
- for chkbx in chkbxs:
- chkbx.setChecked(False)
+ def _set_visible_region(self, state, *, description):
+ self.mne.visible_annotations[description] = bool(state)
+ def _select_annotations(self):
select_dlg = QDialog(self)
chkbxs = list()
layout = QVBoxLayout()
@@ -2344,8 +2357,8 @@ def _clear_all():
for des in self.mne.visible_annotations:
chkbx = QCheckBox(des)
chkbx.setChecked(self.mne.visible_annotations[des])
- chkbx.stateChanged.connect(partial(_set_visible_region,
- description=des))
+ chkbx.stateChanged.connect(
+ _methpartial(self._set_visible_region, description=des))
chkbxs.append(chkbx)
scroll_layout.addWidget(chkbx)
@@ -2356,11 +2369,11 @@ def _clear_all():
bt_layout = QGridLayout()
all_bt = QPushButton('All')
- all_bt.clicked.connect(_select_all)
+ all_bt.clicked.connect(partial(_select_all, chkbxs=chkbxs))
bt_layout.addWidget(all_bt, 0, 0)
clear_bt = QPushButton('Clear')
- clear_bt.clicked.connect(_clear_all)
+ clear_bt.clicked.connect(partial(_clear_all, chkbxs=chkbxs))
bt_layout.addWidget(clear_bt, 0, 1)
ok_bt = QPushButton('Ok')
@@ -2371,8 +2384,10 @@ def _clear_all():
select_dlg.setLayout(layout)
select_dlg.exec()
+ all_bt.clicked.disconnect()
+ clear_bt.clicked.disconnect()
- self.main._update_regions_visible()
+ self.weakmain()._update_regions_visible()
def _description_changed(self, descr_idx):
new_descr = self.description_cmbx.itemText(descr_idx)
@@ -2386,11 +2401,10 @@ def _start_changed(self):
if start < stop:
sel_region.setRegion((start, stop))
else:
- self.main.message_box(text='Invalid value!',
- info_text='Start can\'t be bigger or '
- 'equal to Stop!',
- icon=QMessageBox.Critical,
- modal=False)
+ self.weakmain().message_box(
+ text='Invalid value!',
+ info_text='Start can\'t be bigger or equal to Stop!',
+ icon=QMessageBox.Critical, modal=False)
self.start_bx.setValue(sel_region.getRegion()[0])
def _stop_changed(self):
@@ -2401,10 +2415,10 @@ def _stop_changed(self):
if start < stop:
sel_region.setRegion((start, stop))
else:
- self.main.message_box(text='Invalid value!',
- info_text='Stop can\'t be smaller or '
- 'equal to Start!',
- icon=QMessageBox.Critical)
+ self.weakmain().message_box(
+ text='Invalid value!',
+ info_text='Stop can\'t be smaller or equal to Start!',
+ icon=QMessageBox.Critical)
self.stop_bx.setValue(sel_region.getRegion()[1])
def _set_color(self):
@@ -2433,7 +2447,7 @@ def update_values(self, region):
def _update_description_cmbx(self):
self.description_cmbx.clear()
- descriptions = self.main._get_annotation_labels()
+ descriptions = self.weakmain()._get_annotation_labels()
for description in descriptions:
self._add_description_to_cmbx(description)
self.description_cmbx.setCurrentText(self.mne.current_description)
@@ -2477,9 +2491,9 @@ def _show_help(self):
'