Repository: napari/napari

Issue:
Deleting layer throws error and viewing area is empty
## 🐛 Bug

<!-- A clear and concise description of what the bug is. -->

## To Reproduce

Steps to reproduce the behavior:

1. Load images or create other layers in `napari`
2. Select layer and click trash button, or drag to trash
3. Viewing area goes black and following trace is printed:

```
Traceback (most recent call last):
  File "XXXX\lib\site-packages\vispy\app\backends\_qt.py", line 825, in paintGL
    self._vispy_canvas.events.draw(region=None)
  File "XXXX\lib\site-packages\vispy\util\event.py", line 455, in __call__
    self._invoke_callback(cb, event)
  File "XXXX\lib\site-packages\vispy\util\event.py", line 473, in _invoke_callback
    _handle_exception(self.ignore_callback_errors,
  File "XXXX\lib\site-packages\vispy\util\event.py", line 471, in _invoke_callback
    cb(event)
  File "YYY\napari\napari\_vispy\vispy_base_layer.py", line 234, in on_draw
    self.layer.scale_factor = self.scale_factor
  File "YYY\napari\napari\_vispy\vispy_base_layer.py", line 131, in scale_factor
    transform = self.node.canvas.scene.node_transform(self.node)
AttributeError: 'NoneType' object has no attribute 'scene'
```

I checked out various commits, and narrowed it down to `f9ca768` (#1053 Fix pyramid clipping). 

Perhaps @sofroniewn might have the best idea.

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

## Environment

 - Please copy and paste the information at napari info option in help menubar here:

```
napari: 0.2.12+61.g5c30662
Platform: Windows-10-10.0.19041-SP0
Python: 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
Qt: 5.14.1
PySide2: 5.14.1
NumPy: 1.18.1
SciPy: 1.4.1
Dask: 2.12.0
VisPy: 0.6.4

GL version: 4.6.0 NVIDIA 441.45
MAX_TEXTURE_SIZE: 32768

Plugins:
- mat-images: 0.1.0
```



Discussion:
Yup, that's a bug, will work on a fix + test right now

Resolving patch:
diff --git a/napari/_qt/qt_viewer.py b/napari/_qt/qt_viewer.py
--- a/napari/_qt/qt_viewer.py
+++ b/napari/_qt/qt_viewer.py
@@ -242,9 +242,10 @@ def _remove_layer(self, event):
         """
         layer = event.item
         vispy_layer = self.layer_to_visual[layer]
+        self.canvas.events.draw.disconnect(vispy_layer.on_draw)
         vispy_layer.node.transforms = ChainTransform()
         vispy_layer.node.parent = None
-        del self.layer_to_visual[layer]
+        del vispy_layer
 
     def _reorder_layers(self, event):
         """When the list is reordered, propagate changes to draw order.
diff --git a/napari/_vispy/vispy_base_layer.py b/napari/_vispy/vispy_base_layer.py
--- a/napari/_vispy/vispy_base_layer.py
+++ b/napari/_vispy/vispy_base_layer.py
@@ -125,12 +125,13 @@ def translate(self, translate):
 
     @property
     def scale_factor(self):
-        """float: Conversion factor from canvas coordinates to image
-        coordinates, which depends on the current zoom level.
+        """float: Conversion factor from canvas pixels to data coordinates.
         """
-        transform = self.node.canvas.scene.node_transform(self.node)
-        scale_factor = transform.map([1, 1])[0] - transform.map([0, 0])[0]
-        return scale_factor
+        if self.node.canvas is not None:
+            transform = self.node.canvas.scene.node_transform(self.node)
+            return transform.map([1, 1])[0] - transform.map([0, 0])[0]
+        else:
+            return 1
 
     @abstractmethod
     def _on_data_change(self, event=None):
@@ -182,10 +183,9 @@ def _transform_position(self, position):
             transform = self.node.canvas.scene.node_transform(self.node)
             # Map and offset position so that pixel center is at 0
             mapped_position = transform.map(list(position))[:nd] - 0.5
-            coords = tuple(mapped_position[::-1])
+            return tuple(mapped_position[::-1])
         else:
-            coords = (0,) * nd
-        return coords
+            return (0,) * nd
 
     def _reset_base(self):
         self._on_visible_change()


Test patch:
diff --git a/napari/_tests/test_draw.py b/napari/_tests/test_draw.py
new file mode 100644
--- /dev/null
+++ b/napari/_tests/test_draw.py
@@ -0,0 +1,27 @@
+import numpy as np
+import pytest
+import sys
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith('win') or sys.platform.startswith('linux'),
+    reason='Currently fails on certain CI due to error on canvas draw.',
+)
+def test_canvas_drawing(viewer_factory):
+    """Test drawing before and after adding and then deleting a layer."""
+    view, viewer = viewer_factory()
+    assert len(viewer.layers) == 0
+    # Check canvas context is not none before drawing, as currently on
+    # some of our CI a proper canvas context is not made
+    view.canvas.events.draw()
+
+    # Add layer
+    data = np.random.random((15, 10, 5))
+    layer = viewer.add_image(data)
+    assert len(viewer.layers) == 1
+    view.canvas.events.draw()
+
+    # Remove layer
+    viewer.layers.remove(layer)
+    assert len(viewer.layers) == 0
+    view.canvas.events.draw()