Repository: mne-tools/mne-python

Issue:
BUG: PyVista segfault on macOS with a single time point
Plotting a STC with PyVista that only has a single time point with `time_viewer=True`, then clicking anywhere in the window leads to a segfault:
<details>
<summary>MWE</summary>

```
import os.path as op
import mne

data_path = mne.datasets.sample.data_path()
sample_dir = op.join(data_path, 'MEG', 'sample')
subjects_dir = op.join(data_path, 'subjects')
inv = mne.minimum_norm.read_inverse_operator(op.join(
    sample_dir, 'sample_audvis-meg-eeg-oct-6-meg-eeg-inv.fif'))
evoked = mne.read_evokeds(op.join(sample_dir, 'sample_audvis-ave.fif'))[0]
evoked.apply_baseline((None, 0)).crop(0.1, 0.1)
stc = mne.minimum_norm.apply_inverse(
    evoked, inv, method='dSPM', verbose='debug', pick_ori=None)  # 'vector')

with mne.viz.use_3d_backend('pyvista'):
    brain = stc.plot(subjects_dir=subjects_dir)
```

</details>

```
Thread 0 Crashed:
0   libsystem_kernel.dylib        	0x00007fff6e70c7fa __pthread_kill + 10
1   libsystem_c.dylib             	0x00007fff6e6233a2 raise + 26
2   libsystem_platform.dylib      	0x00007fff6e7be42d _sigtramp + 29
3   ???                           	000000000000000000 0 + 0
4   libvtkInteraction-8.1.1.dylib 	0x000000063fec1469 vtkSliderRepresentation2D::StartWidgetInteraction(double*) + 313
5   libvtkInteraction-8.1.1.dylib 	0x000000063fec5ac7 vtkSliderWidget::SelectAction(vtkAbstractWidget*) + 135
6   libvtkCommon-8.1.1.dylib      	0x000000062c0db281 vtkCallbackCommand::Execute(vtkObject*, unsigned long, void*) + 33
7   libvtkCommon-8.1.1.dylib      	0x000000062c245dfc vtkSubjectHelper::InvokeEvent(unsigned long, void*, vtkObject*) + 1004
8   libvtkRenderingKitPython37D-8.1.1.dylib	0x000000063ab149a8 PyvtkRenderWindowInteractor_LeftButtonPressEvent(_object*, _object*) + 136
9   python                        	0x000000010ed0c18e _PyMethodDef_RawFastCallKeywords + 350
10  python                        	0x000000010ee48e42 call_function + 306
11  python                        	0x000000010ee46aec _PyEval_EvalFrameDefault + 46092
12  python                        	0x000000010ed0b8d5 function_code_fastcall + 117
13  python                        	0x000000010ed0ece2 method_call + 130
14  python                        	0x000000010ed0c752 PyObject_Call + 130
15  sip.cpython-37m-darwin.so     	0x0000000121747d40 sip_api_call_procedure_method + 208
16  QtOpenGL.so                   	0x000000064103c1c7 sipQGLWidget::mousePressEvent(QMouseEvent*) + 103
17  QtWidgets                     	0x000000012265d058 QWidget::event(QEvent*) + 424
18  QtOpenGL.so                   	0x000000064103b9a0 sipQGLWidget::event(QEvent*) + 192
...
```

Not sure why this happens, maybe something about the time viewer bounds. I noticed on Linux before the hiding code that they ended up negative/backward somehow when there was a single time point. My guess is that we need to be more careful about instantiation in the single time point case.

cc @GuillaumeFavelier in case you can look at the code and immediately see the problem


Discussion:
Commenting out the playback speed slider and time slider instantiation entirely makes it go away. So I've at least narrowed it down...

Resolving patch:
diff --git a/mne/viz/_brain/_timeviewer.py b/mne/viz/_brain/_timeviewer.py
--- a/mne/viz/_brain/_timeviewer.py
+++ b/mne/viz/_brain/_timeviewer.py
@@ -390,18 +390,6 @@ def keyPressEvent(self, event):
         if callback is not None:
             callback()
 
-    def _set_time_slider_visibility(self):
-        # if we actually have time points, we will show the slider so
-        # hide the time actor
-        if self.brain._times is not None and len(self.brain._times) > 1:
-            if self.time_actor is not None:
-                self.time_actor.VisibilityOff()
-            return
-        # otherwise, hide the irrelevant sliders
-        for slider in self.plotter.slider_widgets:
-            if getattr(slider, "name", None) in {'playback_speed', 'time'}:
-                slider.GetRepresentation().VisibilityOff()
-
     def toggle_interface(self):
         self.visibility = not self.visibility
 
@@ -415,16 +403,16 @@ def toggle_interface(self):
 
         # manage time label
         time_label = self.brain._data['time_label']
+        # if we actually have time points, we will show the slider so
+        # hide the time actor
+        have_ts = self.brain._times is not None and len(self.brain._times) > 1
         if self.time_actor is not None:
-            if self.visibility and time_label is not None:
+            if self.visibility and time_label is not None and not have_ts:
                 self.time_actor.SetInput(time_label(self.brain._current_time))
                 self.time_actor.VisibilityOn()
             else:
                 self.time_actor.VisibilityOff()
 
-        # hide time labels that are not relevant
-        self._set_time_slider_visibility()
-
         self.plotter.update()
 
     def apply_auto_scaling(self):
@@ -571,23 +559,30 @@ def configure_sliders(self):
 
         # Time slider
         max_time = len(self.brain._data['time']) - 1
-        self.time_call = TimeSlider(
-            plotter=self.plotter,
-            brain=self.brain,
-            first_call=False,
-            callback=self.plot_time_line,
-        )
-        time_slider = self.plotter.add_slider_widget(
-            self.time_call,
-            value=self.brain._data['time_idx'],
-            rng=[0, max_time],
-            pointa=(0.23, 0.1),
-            pointb=(0.77, 0.1),
-            event_type='always'
-        )
-        time_slider.name = "time"
-        # configure properties of the time slider
-        time_slider.GetRepresentation().SetLabelFormat('idx=%0.1f')
+        # VTK on macOS bombs if we create these then hide them, so don't
+        # even create them
+        if max_time < 1:
+            self.time_call = None
+            time_slider = None
+        else:
+            self.time_call = TimeSlider(
+                plotter=self.plotter,
+                brain=self.brain,
+                first_call=False,
+                callback=self.plot_time_line,
+            )
+            time_slider = self.plotter.add_slider_widget(
+                self.time_call,
+                value=self.brain._data['time_idx'],
+                rng=[0, max_time],
+                pointa=(0.23, 0.1),
+                pointb=(0.77, 0.1),
+                event_type='always'
+            )
+            time_slider.name = "time"
+            # configure properties of the time slider
+            time_slider.GetRepresentation().SetLabelFormat('idx=%0.1f')
+
         current_time = self.brain._current_time
         assert current_time is not None  # should never be the case, float
         time_label = self.brain._data['time_label']
@@ -595,26 +590,30 @@ def configure_sliders(self):
             current_time = time_label(current_time)
         else:
             current_time = time_label
-        time_slider.GetRepresentation().SetTitleText(current_time)
+        if time_slider is not None:
+            time_slider.GetRepresentation().SetTitleText(current_time)
         if self.time_actor is not None:
             self.time_actor.SetInput(current_time)
         del current_time
 
         # Playback speed slider
-        self.playback_speed_call = SmartSlider(
-            plotter=self.plotter,
-            callback=self.set_playback_speed,
-            name="playback_speed"
-        )
-        playback_speed_slider = self.plotter.add_slider_widget(
-            self.playback_speed_call,
-            value=self.default_playback_speed_value,
-            rng=self.default_playback_speed_range, title="speed",
-            pointa=(0.02, 0.1),
-            pointb=(0.18, 0.1),
-            event_type='always'
-        )
-        playback_speed_slider.name = "playback_speed"
+        if time_slider is None:
+            self.playback_speed_call = None
+        else:
+            self.playback_speed_call = SmartSlider(
+                plotter=self.plotter,
+                callback=self.set_playback_speed,
+                name="playback_speed"
+            )
+            playback_speed_slider = self.plotter.add_slider_widget(
+                self.playback_speed_call,
+                value=self.default_playback_speed_value,
+                rng=self.default_playback_speed_range, title="speed",
+                pointa=(0.02, 0.1),
+                pointb=(0.18, 0.1),
+                event_type='always'
+            )
+            playback_speed_slider.name = "playback_speed"
 
         # Colormap slider
         pointa = np.array((0.82, 0.26))
@@ -687,8 +686,9 @@ def configure_sliders(self):
         self.set_slider_style(fmid_slider)
         self.set_slider_style(fmax_slider)
         self.set_slider_style(fscale_slider)
-        self.set_slider_style(playback_speed_slider)
-        self.set_slider_style(time_slider)
+        if time_slider is not None:
+            self.set_slider_style(playback_speed_slider)
+            self.set_slider_style(time_slider)
 
     def configure_playback(self):
         self.plotter.add_callback(self.play, self.refresh_rate_ms)
@@ -937,11 +937,12 @@ def clean(self):
         self.orientation_call = None
         self.smoothing_call.plotter = None
         self.smoothing_call = None
-        self.time_call.plotter = None
-        self.time_call.brain = None
-        self.time_call = None
-        self.playback_speed_call.plotter = None
-        self.playback_speed_call = None
+        if self.time_call is not None:
+            self.time_call.plotter = None
+            self.time_call.brain = None
+            self.time_call = None
+            self.playback_speed_call.plotter = None
+            self.playback_speed_call = None
         self.fmin_call.plotter = None
         self.fmin_call.brain = None
         self.fmin_call = None


Test patch: