Repository: napari/napari

Issue:
singleton dimensions are a problem
## 🐛 Bug
Was working in napari with someone yesterday and one of their images revealed an issue with napari's handling of singleton dimensions:

```python
import napari, numpy

with napari.gui_qt():
    napari.view_image(numpy.random.rand(10,1,10,10))
```

![Screen Shot 2020-01-17 at 8 56 50 AM](https://user-images.githubusercontent.com/1609449/72617595-4f86a580-3907-11ea-86b9-ba38a2be1671.png)

(see dimensions sliders 🖕)
this would be solved visually easily enough by adding `np.squeeze` somewhere in `magic_imread`, but i'm not sure that's necessarily what we want to do here.  perhaps dealing with them in the dims model is more appropriate?  If anyone has a preference on how to tackle this, I'm happy to submit a PR


Discussion:
That must have crept back in there when adding labels / play buttons as we've had that issue a while back - see #409. I think there the approach was to not call squeeze, but to just change the display logic in the qt_dims, and I lean to still taking that approach here - i.e. if someone has 5D data with a singleton dim I shouldn't convert that to 4D data, I should just know not to show them a slider they don't need, and the sliders should still be numbered by default according to the original dims.

How does that sound @tlambert03? It would be great if you can fix / add better tests / see what the current tests we have (if any) for singletons are doing now 
sounds good.  will take a look

Resolving patch:
diff --git a/napari/_qt/qt_dims_slider.py b/napari/_qt/qt_dims_slider.py
--- a/napari/_qt/qt_dims_slider.py
+++ b/napari/_qt/qt_dims_slider.py
@@ -54,20 +54,10 @@ def __init__(self, parent: QWidget, axis: int):
         # editingFinished event (the user is expected to change the value)...
         # which is confusing to the user, so instead we use an IntValidator
         # that makes sure the user can only enter integers, but we do our own
-        # value validation in change_slice
+        # value validation in self.change_slice
         self.curslice_label.setValidator(QIntValidator(0, 999999))
 
-        def change_slice():
-            val = int(self.curslice_label.text())
-            max_allowed = self.dims.max_indices[self.axis]
-            if val > max_allowed:
-                val = max_allowed
-                self.curslice_label.setText(str(val))
-            self.curslice_label.clearFocus()
-            self.qt_dims.setFocus()
-            self.dims.set_point(self.axis, val)
-
-        self.curslice_label.editingFinished.connect(change_slice)
+        self.curslice_label.editingFinished.connect(self._set_slice_from_label)
         self.totslice_label = QLabel(self)
         self.totslice_label.setToolTip(f'Total slices for axis {axis}')
         self.curslice_label.setObjectName('slice_label')
@@ -97,6 +87,17 @@ def change_slice():
         self.setLayout(layout)
         self.dims.events.axis_labels.connect(self._pull_label)
 
+    def _set_slice_from_label(self):
+        """Update the dims point based on the curslice_label."""
+        val = int(self.curslice_label.text())
+        max_allowed = self.dims.max_indices[self.axis]
+        if val > max_allowed:
+            val = max_allowed
+            self.curslice_label.setText(str(val))
+        self.curslice_label.clearFocus()
+        self.qt_dims.setFocus()
+        self.dims.set_point(self.axis, val)
+
     def _create_axis_label_widget(self):
         """Create the axis label widget which accompanies its slider."""
         label = QLineEdit(self)
@@ -186,7 +187,7 @@ def _update_range(self):
             if _range[1] == 0:
                 displayed_sliders[self.axis] = False
                 self.qt_dims.last_used = None
-                self.slider.hide()
+                self.hide()
             else:
                 if (
                     not displayed_sliders[self.axis]
@@ -194,7 +195,7 @@ def _update_range(self):
                 ):
                     displayed_sliders[self.axis] = True
                     self.last_used = self.axis
-                    self.slider.show()
+                    self.show()
                 self.slider.setMinimum(_range[0])
                 self.slider.setMaximum(_range[1])
                 self.slider.setSingleStep(_range[2])
@@ -205,7 +206,7 @@ def _update_range(self):
                 self._update_slice_labels()
         else:
             displayed_sliders[self.axis] = False
-            self.slider.hide()
+            self.hide()
 
     def _update_slider(self):
         mode = self.dims.mode[self.axis]


Test patch:
diff --git a/napari/_qt/_tests/test_qt_dims.py b/napari/_qt/_tests/test_qt_dims.py
--- a/napari/_qt/_tests/test_qt_dims.py
+++ b/napari/_qt/_tests/test_qt_dims.py
@@ -19,6 +19,7 @@ def test_creating_view(qtbot):
     view = QtDims(dims)
 
     qtbot.addWidget(view)
+    view.show()
 
     # Check that the dims model has been appended to the dims view
     assert view.dims == dims
@@ -27,6 +28,12 @@ def test_creating_view(qtbot):
     # dimensions
     assert view.nsliders == view.dims.ndim
     assert np.sum(view._displayed_sliders) == view.dims.ndim - 2
+    assert np.all(
+        [
+            s.isVisible() == d
+            for s, d in zip(view.slider_widgets, view._displayed_sliders)
+        ]
+    )
 
 
 def test_changing_ndim(qtbot):
@@ -37,16 +44,29 @@ def test_changing_ndim(qtbot):
     view = QtDims(Dims(ndim))
 
     qtbot.addWidget(view)
+    view.show()
 
     # Check that adding dimensions adds sliders
     view.dims.ndim = 5
     assert view.nsliders == view.dims.ndim
     assert np.sum(view._displayed_sliders) == view.dims.ndim - 2
+    assert np.all(
+        [
+            s.isVisible() == d
+            for s, d in zip(view.slider_widgets, view._displayed_sliders)
+        ]
+    )
 
     # Check that removing dimensions removes sliders
     view.dims.ndim = 2
     assert view.nsliders == view.dims.ndim
     assert np.sum(view._displayed_sliders) == view.dims.ndim - 2
+    assert np.all(
+        [
+            s.isVisible() == d
+            for s, d in zip(view.slider_widgets, view._displayed_sliders)
+        ]
+    )
 
 
 def test_changing_focus(qtbot):
@@ -79,14 +99,27 @@ def test_changing_display(qtbot):
     view = QtDims(Dims(ndim))
 
     qtbot.addWidget(view)
+    view.show()
 
     assert view.nsliders == view.dims.ndim
     assert np.sum(view._displayed_sliders) == view.dims.ndim - 2
+    assert np.all(
+        [
+            s.isVisible() == d
+            for s, d in zip(view.slider_widgets, view._displayed_sliders)
+        ]
+    )
 
     # Check changing displayed removes a slider
     view.dims.ndisplay = 3
     assert view.nsliders == view.dims.ndim
     assert np.sum(view._displayed_sliders) == view.dims.ndim - 3
+    assert np.all(
+        [
+            s.isVisible() == d
+            for s, d in zip(view.slider_widgets, view._displayed_sliders)
+        ]
+    )
 
 
 def test_slider_values(qtbot):
@@ -142,6 +175,32 @@ def test_slider_range(qtbot):
     assert first_slider.singleStep() == view.dims.range[0][2]
 
 
+def test_singleton_dims(qtbot):
+    """
+    Test singleton dims causes no slider.
+    """
+    ndim = 4
+    dims = Dims(ndim)
+    dims.set_range(0, (0, 1, 1))
+    view = QtDims(dims)
+
+    qtbot.addWidget(view)
+    view.show()
+
+    # Check that the dims model has been appended to the dims view
+    assert view.dims == dims
+
+    # Check the number of displayed sliders is only one
+    assert view.nsliders == 4
+    assert np.sum(view._displayed_sliders) == 1
+    assert np.all(
+        [
+            s.isVisible() == d
+            for s, d in zip(view.slider_widgets, view._displayed_sliders)
+        ]
+    )
+
+
 def test_order_when_changing_ndim(qtbot):
     """
     Test order of the sliders when changing the number of dimensions.