Repository: napari/napari

Issue:
Play button sticks if all layers are deleted while playing
## 🐛 Bug

Play/Stop stops working if layer is deleted while playing

## To Reproduce

Steps to reproduce the behavior:

1. add stack of images as an image layer
2. hit play, then while playing delete the layer
3. add stack of images again

## Expected behavior

Play/Stop button should function normally after removing an image layer while playing

## Environment
napari: 0.4.13
Platform: Linux-3.10.0-1160.45.1.el7.x86_64-x86_64-with-glibc2.17
Python: 3.9.6 | packaged by conda-forge | (default, Jul 11 2021, 03:39:48) [GCC 9.3.0]
Qt: 5.12.9
PyQt5: 5.12.3
NumPy: 1.20.3
SciPy: 1.7.1
Dask: 2021.08.1
VisPy: 0.9.4

OpenGL:

GL version: 2.1 Mesa 18.3.4
MAX_TEXTURE_SIZE: 8192
Screens:

screen 1: resolution 1440x847, scale 1.0
Plugins:

animation: 0.0.2
cellpose-napari: 0.1.3
console: 0.0.4
napari-nikon-nd2: 0.1.3
scikit-image: 0.4.13
svg: 0.1.5


Discussion:

Resolving patch:
diff --git a/napari/_qt/widgets/qt_dims.py b/napari/_qt/widgets/qt_dims.py
--- a/napari/_qt/widgets/qt_dims.py
+++ b/napari/_qt/widgets/qt_dims.py
@@ -97,12 +97,8 @@ def _update_range(self):
         self._resize_slice_labels()
 
     def _update_display(self):
-        """
-        Updates display for all sliders.
-
-        The event parameter is there just to allow easy connection to signals,
-        without using `lambda event:`
-        """
+        """Updates display for all sliders."""
+        self.stop()
         widgets = reversed(list(enumerate(self.slider_widgets)))
         nsteps = self.dims.nsteps
         for (axis, widget) in widgets:
@@ -121,12 +117,8 @@ def _update_display(self):
         self._resize_slice_labels()
 
     def _update_nsliders(self):
-        """
-        Updates the number of sliders based on the number of dimensions.
-
-        The event parameter is there just to allow easy connection to signals,
-        without using `lambda event:`
-        """
+        """Updates the number of sliders based on the number of dimensions."""
+        self.stop()
         self._trim_sliders(0)
         self._create_sliders(self.dims.ndim)
         self._update_display()
diff --git a/napari/_qt/widgets/qt_dims_slider.py b/napari/_qt/widgets/qt_dims_slider.py
--- a/napari/_qt/widgets/qt_dims_slider.py
+++ b/napari/_qt/widgets/qt_dims_slider.py
@@ -413,7 +413,7 @@ def _play(
             _connect={'frame_requested': self.qt_dims._set_frame},
         )
         worker.finished.connect(self.qt_dims.stop)
-        thread.finished.connect(self.play_stopped.emit)
+        thread.finished.connect(self.play_stopped)
         self.play_started.emit()
         self.thread = thread
         return worker, thread


Test patch:
diff --git a/napari/_qt/widgets/_tests/test_qt_dims.py b/napari/_qt/widgets/_tests/test_qt_dims.py
--- a/napari/_qt/widgets/_tests/test_qt_dims.py
+++ b/napari/_qt/widgets/_tests/test_qt_dims.py
@@ -314,6 +314,34 @@ def test_slice_labels(qtbot):
     assert dims.point[0] == 8
 
 
+def test_not_playing_after_ndim_changes(qtbot):
+    """See https://github.com/napari/napari/issues/3998"""
+    dims = Dims(ndim=3, ndisplay=2, range=((0, 10, 1), (0, 20, 1), (0, 30, 1)))
+    view = QtDims(dims)
+    qtbot.addWidget(view)
+    # Loop to prevent finishing before the assertions in this test.
+    view.play(loop_mode='loop')
+    qtbot.waitUntil(lambda: view.is_playing)
+
+    dims.ndim = 2
+
+    qtbot.waitUntil(lambda: not view.is_playing)
+
+
+def test_not_playing_after_ndisplay_changes(qtbot):
+    """See https://github.com/napari/napari/issues/3998"""
+    dims = Dims(ndim=3, ndisplay=2, range=((0, 10, 1), (0, 20, 1), (0, 30, 1)))
+    view = QtDims(dims)
+    qtbot.addWidget(view)
+    # Loop to prevent finishing before the assertions in this test.
+    view.play(loop_mode='loop')
+    qtbot.waitUntil(lambda: view.is_playing)
+
+    dims.ndisplay = 3
+
+    qtbot.waitUntil(lambda: not view.is_playing)
+
+
 def test_set_axis_labels_after_ndim_changes(qtbot):
     """See https://github.com/napari/napari/issues/3753"""
     dims = Dims(ndim=3, ndisplay=2)