From 3f6e5f197fc6a3112ebe555a4093507e811dc5eb Mon Sep 17 00:00:00 2001 From: NoahMarkowitz Date: Wed, 28 Aug 2024 17:11:19 -0400 Subject: [PATCH 1/8] integrate event system --- mne_qt_browser/_pg_figure.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/mne_qt_browser/_pg_figure.py b/mne_qt_browser/_pg_figure.py index bc9b77c1..3eb76833 100644 --- a/mne_qt_browser/_pg_figure.py +++ b/mne_qt_browser/_pg_figure.py @@ -42,6 +42,7 @@ from mne.viz import plot_sensors from mne.viz._figure import BrowserBase from mne.viz.backends._utils import _init_mne_qtapp, _qt_raise_window +from mne.viz.ui_events import TimeChange, publish, subscribe from mne.viz.utils import _figure_agg, _merge_annotations, _simplify_float from pyqtgraph import ( AxisItem, @@ -3972,6 +3973,32 @@ def __init__(self, **kwargs): # disable histogram of epoch PTP amplitude del self.mne.keyboard_shortcuts["h"] + # Connect to the event system + self.mne.plt.sigXRangeChanged.connect(self._notify_event_system_on_x_change) + + # Now subscribe to the event system + subscribe(self, "time_change", self._on_time_change_event_system) + + def _notify_event_system_on_x_change(self): + """Notify the event system about a change in the x-range.""" + publish(self, TimeChange(time=self.mne.times[0])) + + def _on_time_change_event_system(self, event): + """Response to an event from the event-ui system.""" + tolerance = 0.005 # 5 ms + if np.abs(event.time - self.mne.times[0]) > tolerance: + xmin = event.time + xmax = event.time + self.mne.duration + + if xmin < 0: + xmin = 0 + xmax = xmin + self.mne.duration + elif xmax > self.mne.xmax: + xmax = self.mne.xmax + xmin = xmax - self.mne.duration + + self.mne.plt.setXRange(xmin, xmax, padding=0) + def _hidpi_mkPen(self, *args, **kwargs): kwargs["width"] = self._pixel_ratio * kwargs.get("width", 1.0) return mkPen(*args, **kwargs) From 492ed7b8df8f352823be4c3768e7cd67b2c23d91 Mon Sep 17 00:00:00 2001 From: NoahMarkowitz Date: Thu, 29 Aug 2024 18:50:33 -0400 Subject: [PATCH 2/8] TimeChange for vline --- mne_qt_browser/_pg_figure.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/mne_qt_browser/_pg_figure.py b/mne_qt_browser/_pg_figure.py index 3eb76833..b2cbc9f9 100644 --- a/mne_qt_browser/_pg_figure.py +++ b/mne_qt_browser/_pg_figure.py @@ -3324,6 +3324,7 @@ class MNEQtBrowser(BrowserBase, QMainWindow, metaclass=_PGMetaClass): """A PyQtGraph-backend for 2D data browsing.""" gotClosed = Signal() + sigVLineMoved = Signal(float) @_safe_splash def __init__(self, **kwargs): @@ -3974,30 +3975,24 @@ def __init__(self, **kwargs): del self.mne.keyboard_shortcuts["h"] # Connect to the event system - self.mne.plt.sigXRangeChanged.connect(self._notify_event_system_on_x_change) + self.sigVLineMoved.connect(self._notify_event_system_on_vline_change) # Now subscribe to the event system - subscribe(self, "time_change", self._on_time_change_event_system) + subscribe(self, "time_change", self._on_time_change_vline) - def _notify_event_system_on_x_change(self): - """Notify the event system about a change in the x-range.""" - publish(self, TimeChange(time=self.mne.times[0])) + def _notify_event_system_on_vline_change(self, t): + publish(self, TimeChange(time=t)) - def _on_time_change_event_system(self, event): + def _on_time_change_vline(self, event): """Response to an event from the event-ui system.""" - tolerance = 0.005 # 5 ms - if np.abs(event.time - self.mne.times[0]) > tolerance: - xmin = event.time - xmax = event.time + self.mne.duration + # At what point to not worry about accuracy + tolerance = 0.005 - if xmin < 0: - xmin = 0 - xmax = xmin + self.mne.duration - elif xmax > self.mne.xmax: - xmax = self.mne.xmax - xmin = xmax - self.mne.duration - - self.mne.plt.setXRange(xmin, xmax, padding=0) + if self.mne.vline is not None: + if np.abs(self.mne.vline.pos()[0] - event.time) > tolerance: + self._add_vline(event.time) + else: + self._add_vline(event.time) def _hidpi_mkPen(self, *args, **kwargs): kwargs["width"] = self._pixel_ratio * kwargs.get("width", 1.0) @@ -4276,11 +4271,13 @@ def _add_vline(self, t): self.mne.vline = VLine(self.mne, t, bounds=(0, self.mne.xmax)) self.mne.vline.sigPositionChangeFinished.connect(self._vline_slot) self.mne.plt.addItem(self.mne.vline) + else: self.mne.vline.setPos(t) self.mne.vline_visible = True self.mne.overview_bar.update_vline() + self.sigVLineMoved.emit(t) def _mouse_moved(self, pos): """Show Crosshair if enabled at mouse move.""" From c80bb48b9f24d46bcbde1c1bda04c3520b1a5cfd Mon Sep 17 00:00:00 2001 From: NoahMarkowitz Date: Thu, 5 Sep 2024 18:42:43 -0400 Subject: [PATCH 3/8] add time and channel browse --- mne_qt_browser/_pg_figure.py | 66 ++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/mne_qt_browser/_pg_figure.py b/mne_qt_browser/_pg_figure.py index b2cbc9f9..e1789da1 100644 --- a/mne_qt_browser/_pg_figure.py +++ b/mne_qt_browser/_pg_figure.py @@ -42,7 +42,14 @@ from mne.viz import plot_sensors from mne.viz._figure import BrowserBase from mne.viz.backends._utils import _init_mne_qtapp, _qt_raise_window -from mne.viz.ui_events import TimeChange, publish, subscribe +from mne.viz.ui_events import ( + ChannelBrowse, + TimeBrowse, + TimeChange, + disable_ui_events, + publish, + subscribe, +) from mne.viz.utils import _figure_agg, _merge_annotations, _simplify_float from pyqtgraph import ( AxisItem, @@ -3324,7 +3331,6 @@ class MNEQtBrowser(BrowserBase, QMainWindow, metaclass=_PGMetaClass): """A PyQtGraph-backend for 2D data browsing.""" gotClosed = Signal() - sigVLineMoved = Signal(float) @_safe_splash def __init__(self, **kwargs): @@ -3974,26 +3980,38 @@ def __init__(self, **kwargs): # disable histogram of epoch PTP amplitude del self.mne.keyboard_shortcuts["h"] - # Connect to the event system - self.sigVLineMoved.connect(self._notify_event_system_on_vline_change) - - # Now subscribe to the event system - subscribe(self, "time_change", self._on_time_change_vline) + # Subscribe to vertical line change + subscribe(self, "time_change", self._on_time_change_event) - def _notify_event_system_on_vline_change(self, t): - publish(self, TimeChange(time=t)) + # Subscribe to time browse + subscribe(self, "time_browse", self._on_time_browse_event) - def _on_time_change_vline(self, event): - """Response to an event from the event-ui system.""" - # At what point to not worry about accuracy - tolerance = 0.005 + # Subscribe to channel browse + # self.mne.plt.sigYRangeChanged.connect(self._on_channel_browse_event) + subscribe(self, "channel_browse", self._on_channel_browse_event) - if self.mne.vline is not None: - if np.abs(self.mne.vline.pos()[0] - event.time) > tolerance: - self._add_vline(event.time) - else: + def _on_time_change_event(self, event): + """Response to TimeChange event from the event-ui system.""" + with disable_ui_events(self): self._add_vline(event.time) + def _on_time_browse_event(self, event): + """Response to TimeBrowse event from the event-ui system.""" + with disable_ui_events(self): + self.mne.plt.setXRange(event.time_start, event.time_end, padding=0) + + def _on_channel_browse_event(self, event): + """Response to ChannelBrowse event from the event-ui system.""" + # Get the indices of the subset in the full set of channels + all_channels = self.mne.ch_names[self.mne.ch_order] + ch_indices = [np.where(all_channels == ch)[0][0] for ch in event.channels] + + # Take the start index and set range + with disable_ui_events(self): + start_idx = ch_indices[0] + n_chans = len(ch_indices) + self.mne.plt.setYRange(start_idx, start_idx + n_chans + 1, padding=0) + def _hidpi_mkPen(self, *args, **kwargs): kwargs["width"] = self._pixel_ratio * kwargs.get("width", 1.0) return mkPen(*args, **kwargs) @@ -4277,7 +4295,7 @@ def _add_vline(self, t): self.mne.vline_visible = True self.mne.overview_bar.update_vline() - self.sigVLineMoved.emit(t) + publish(self, TimeChange(time=t)) def _mouse_moved(self, pos): """Show Crosshair if enabled at mouse move.""" @@ -4372,6 +4390,15 @@ def _xrange_changed(self, _, xrange): # Update annotations self._update_regions_visible() + # Publish event + publish( + self, + TimeBrowse( + time_start=self.mne.t_start, + time_end=self.mne.t_start + self.mne.duration, + ), + ) + def _yrange_changed(self, _, yrange): if not self.mne.butterfly: if not self.mne.fig_selection: @@ -4425,6 +4452,9 @@ def _yrange_changed(self, _, yrange): trace.update_color() trace.update_data() + # Publish to event system + publish(self, ChannelBrowse(channels=self.mne.ch_names[self.mne.picks])) + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # DATA HANDLING # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # From 85b6b42b7511129bccea6aad3f43cef0116b06a4 Mon Sep 17 00:00:00 2001 From: NoahMarkowitz Date: Wed, 11 Jun 2025 13:01:16 -0400 Subject: [PATCH 4/8] drag vline works for continuous time --- mne_qt_browser/_pg_figure.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mne_qt_browser/_pg_figure.py b/mne_qt_browser/_pg_figure.py index 78df2d94..d4390cc6 100644 --- a/mne_qt_browser/_pg_figure.py +++ b/mne_qt_browser/_pg_figure.py @@ -4533,6 +4533,9 @@ def _vline_slot(self, orig_vline): vl.setPos(xt) self.mne.overview_bar.update_vline() + def _vline_drag_slot(self, vline): + publish(self, TimeChange(time=vline.value())) + def _add_vline(self, t): if self.mne.is_epochs: ts = self._get_vline_times(t) @@ -4550,8 +4553,9 @@ def _add_vline(self, t): # Avoid off-by-one-error at bmax for VlineLabel bmax -= 1 / self.mne.info["sfreq"] vl = VLine(self.mne, xt, bounds=(bmin, bmax)) - # Should only be emitted when dragged + # Connect signals for both drag and position change vl.sigPositionChangeFinished.connect(self._vline_slot) + vl.sigDragged.connect(self._vline_drag_slot) self.mne.vline.append(vl) self.mne.plt.addItem(vl) else: @@ -4561,6 +4565,7 @@ def _add_vline(self, t): if self.mne.vline is None: self.mne.vline = VLine(self.mne, t, bounds=(0, self.mne.xmax)) self.mne.vline.sigPositionChangeFinished.connect(self._vline_slot) + self.mne.vline.sigDragged.connect(self._vline_drag_slot) self.mne.plt.addItem(self.mne.vline) else: From 3a4063dca14a486cc7e6328fb232aa660cc453a4 Mon Sep 17 00:00:00 2001 From: NoahMarkowitz <34498671+nmarkowitz@users.noreply.github.com> Date: Mon, 16 Jun 2025 10:44:43 -0400 Subject: [PATCH 5/8] Update mne_qt_browser/_pg_figure.py Co-authored-by: Marijn van Vliet --- mne_qt_browser/_pg_figure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne_qt_browser/_pg_figure.py b/mne_qt_browser/_pg_figure.py index d4390cc6..6206be32 100644 --- a/mne_qt_browser/_pg_figure.py +++ b/mne_qt_browser/_pg_figure.py @@ -4275,7 +4275,7 @@ def _on_channel_browse_event(self, event): """Response to ChannelBrowse event from the event-ui system.""" # Get the indices of the subset in the full set of channels all_channels = self.mne.ch_names[self.mne.ch_order] - ch_indices = [np.where(all_channels == ch)[0][0] for ch in event.channels] + ch_indices = np.where(np.isin(event.channels, all_channels))[0][0] # Take the start index and set range with disable_ui_events(self): From 484d8f4e4ef817a0ae146b717666a03efa4f6cc5 Mon Sep 17 00:00:00 2001 From: NoahMarkowitz Date: Mon, 16 Jun 2025 12:19:33 -0400 Subject: [PATCH 6/8] change method of channel browse --- mne_qt_browser/_pg_figure.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/mne_qt_browser/_pg_figure.py b/mne_qt_browser/_pg_figure.py index 6206be32..a36c66d8 100644 --- a/mne_qt_browser/_pg_figure.py +++ b/mne_qt_browser/_pg_figure.py @@ -4275,13 +4275,18 @@ def _on_channel_browse_event(self, event): """Response to ChannelBrowse event from the event-ui system.""" # Get the indices of the subset in the full set of channels all_channels = self.mne.ch_names[self.mne.ch_order] - ch_indices = np.where(np.isin(event.channels, all_channels))[0][0] + # KRUFT + # ch_indices = [np.where(all_channels == ch)[0][0] for ch in event.channels] + ch_indices = np.where(np.isin(all_channels, event.channels))[0] # Take the start index and set range with disable_ui_events(self): - start_idx = ch_indices[0] - n_chans = len(ch_indices) - self.mne.plt.setYRange(start_idx, start_idx + n_chans + 1, padding=0) + start_idx, end_idx = ch_indices.min(), ch_indices.max() + 2 + # KRUFT + # start_idx = ch_indices[0] + # n_chans = len(ch_indices) + # end_idx = start_idx+n_chans+1 + self.mne.plt.setYRange(start_idx, end_idx, padding=0) def _hidpi_mkPen(self, *args, **kwargs): kwargs["width"] = self._pixel_ratio * kwargs.get("width", 1.0) From 0da3100566791643637ef30427447fc79b2e0060 Mon Sep 17 00:00:00 2001 From: NoahMarkowitz Date: Mon, 16 Jun 2025 13:33:31 -0400 Subject: [PATCH 7/8] update for channels_select --- mne_qt_browser/_pg_figure.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mne_qt_browser/_pg_figure.py b/mne_qt_browser/_pg_figure.py index a36c66d8..b2155cb9 100644 --- a/mne_qt_browser/_pg_figure.py +++ b/mne_qt_browser/_pg_figure.py @@ -44,7 +44,7 @@ from mne.viz._figure import BrowserBase from mne.viz.backends._utils import _init_mne_qtapp, _qt_raise_window from mne.viz.ui_events import ( - ChannelBrowse, + ChannelsSelect, TimeBrowse, TimeChange, disable_ui_events, @@ -4259,7 +4259,7 @@ def __init__(self, **kwargs): # Subscribe to channel browse # self.mne.plt.sigYRangeChanged.connect(self._on_channel_browse_event) - subscribe(self, "channel_browse", self._on_channel_browse_event) + subscribe(self, "channels_select", self._on_channel_browse_event) def _on_time_change_event(self, event): """Response to TimeChange event from the event-ui system.""" @@ -4272,12 +4272,12 @@ def _on_time_browse_event(self, event): self.mne.plt.setXRange(event.time_start, event.time_end, padding=0) def _on_channel_browse_event(self, event): - """Response to ChannelBrowse event from the event-ui system.""" + """Response to ChannelsSelect event from the event-ui system.""" # Get the indices of the subset in the full set of channels all_channels = self.mne.ch_names[self.mne.ch_order] # KRUFT # ch_indices = [np.where(all_channels == ch)[0][0] for ch in event.channels] - ch_indices = np.where(np.isin(all_channels, event.channels))[0] + ch_indices = np.where(np.isin(all_channels, event.ch_names))[0] # Take the start index and set range with disable_ui_events(self): @@ -4736,7 +4736,7 @@ def _yrange_changed(self, _, yrange): trace.update_data() # Publish to event system - publish(self, ChannelBrowse(channels=self.mne.ch_names[self.mne.picks])) + publish(self, ChannelsSelect(ch_names=self.mne.ch_names[self.mne.picks])) # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # DATA HANDLING From cf21573e7578e90a7ebbf3500654220b8ce9a2e7 Mon Sep 17 00:00:00 2001 From: NoahMarkowitz Date: Mon, 16 Jun 2025 14:16:30 -0400 Subject: [PATCH 8/8] unsubscribe from events on close --- mne_qt_browser/_pg_figure.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mne_qt_browser/_pg_figure.py b/mne_qt_browser/_pg_figure.py index b2155cb9..f387f193 100644 --- a/mne_qt_browser/_pg_figure.py +++ b/mne_qt_browser/_pg_figure.py @@ -50,6 +50,7 @@ disable_ui_events, publish, subscribe, + unsubscribe, ) from mne.viz.utils import _figure_agg, _merge_annotations, _simplify_float from pyqtgraph import ( @@ -5668,6 +5669,9 @@ def closeEvent(self, event): self.load_thread.clean() self.load_thread = None + # Ensure all event handlers are unsubscribed + unsubscribe(self, ["time_change", "time_browse", "channels_select"]) + # Remove self from browser_instances in globals if self in _browser_instances: _browser_instances.remove(self)