Repository: napari/napari

Issue:
Enable shapes layer (right click) > convert to labels
## 🚀 Feature
<!-- A clear and concise description of the feature proposal -->
Enable shapes layer (right click) > convert to labels so that this function is accessible from GUI directly.

## Pitch

<!-- A clear and concise description of what you want to happen. -->
Currently it is accessible through API to generate labels layer from shapes layer through napari.layers.Shapes.to_labels, but the option to access from the shapes layer (right click) > convert to labels is grayed out. 
The additional input that might be required is the labels layer data shape. One possible implementation is after clicking on “convert to labels”, add a dialog box to select “match dimension to [available image layer]”, which might be the most common usage. 

## Alternatives

<!-- A clear and concise description of any alternative solutions or features you've considered, if any. -->
Continue to only support via API. Though it would be nice to have a tooltip when hovering on the grayed out option that shows the corresponding API. 

## Additional context
In general, the right click menu from different layers shows the same contents but grayed out with different parts. 
This could be confusing as the grayed out options may be due to:
(1) Non-applicable actions
(2) Not sufficient selection/condition (i.e. Link Layers is only available if more than one layer is selected)
Ideally (1) should be removed so that users do not have to guess if those actions could be activated at all.
 
<!-- Add any other context or screenshots about the feature request here. -->



Discussion:
This would be great!

Resolving patch:
diff --git a/napari/layers/_layer_actions.py b/napari/layers/_layer_actions.py
--- a/napari/layers/_layer_actions.py
+++ b/napari/layers/_layer_actions.py
@@ -107,12 +107,17 @@ def _convert_dtype(ll: LayerList, mode='int64'):
 
 
 def _convert(ll: LayerList, type_: str):
+    from ..layers import Shapes
 
     for lay in list(ll.selection):
         idx = ll.index(lay)
-        data = lay.data.astype(int) if type_ == 'labels' else lay.data
         ll.pop(idx)
-        ll.insert(idx, Layer.create(data, {'name': lay.name}, type_))
+        if isinstance(lay, Shapes) and type_ == 'labels':
+            data = lay.to_labels()
+        else:
+            data = lay.data.astype(int) if type_ == 'labels' else lay.data
+        new_layer = Layer.create(data, {'name': lay.name}, type_)
+        ll.insert(idx, new_layer)
 
 
 def _merge_stack(ll: LayerList, rgb=False):
@@ -224,7 +229,9 @@ def _labeltypedict(key) -> ContextAction:
         'napari:convert_to_labels': {
             'description': trans._('Convert to Labels'),
             'action': partial(_convert, type_='labels'),
-            'enable_when': LLCK.only_images_selected,
+            'enable_when': (
+                LLCK.only_images_selected | LLCK.only_shapes_selected
+            ),
             'show_when': True,
         },
         'napari:convert_to_image': {
diff --git a/napari/utils/context/_layerlist_context.py b/napari/utils/context/_layerlist_context.py
--- a/napari/utils/context/_layerlist_context.py
+++ b/napari/utils/context/_layerlist_context.py
@@ -43,6 +43,10 @@ def _only_labels(s: LayerSel) -> bool:
     return bool(s and all(x._type_string == "labels" for x in s))
 
 
+def _only_shapes(s: LayerSel) -> bool:
+    return bool(s and all(x._type_string == "shapes" for x in s))
+
+
 def _active_type(s: LayerSel) -> Optional[str]:
     return s.active._type_string if s.active else None
 
@@ -103,6 +107,9 @@ class LayerListContextKeys(ContextNamespace['LayerSel']):
         ),
         _active_type,
     )
+    # TODO: try to reduce these `only_x_selected` to a single set of strings
+    # or something... however, this would require that our context expressions
+    # support Sets, tuples, lists, etc...  which they currently do not.
     only_images_selected = ContextKey(
         False,
         trans._(
@@ -117,6 +124,13 @@ class LayerListContextKeys(ContextNamespace['LayerSel']):
         ),
         _only_labels,
     )
+    only_shapes_selected = ContextKey(
+        False,
+        trans._(
+            "True when there is at least one selected layer and all selected layers are shapes"
+        ),
+        _only_shapes,
+    )
     active_layer_ndim = ContextKey['LayerSel', Optional[int]](
         None,
         trans._(


Test patch:
diff --git a/napari/layers/_tests/test_layer_actions.py b/napari/layers/_tests/test_layer_actions.py
--- a/napari/layers/_tests/test_layer_actions.py
+++ b/napari/layers/_tests/test_layer_actions.py
@@ -4,11 +4,12 @@
 import pytest
 
 from napari.components.layerlist import LayerList
-from napari.layers import Image, Labels
+from napari.layers import Image, Labels, Shapes
 from napari.layers._layer_actions import (
     _LAYER_ACTIONS,
     ContextAction,
     SubMenu,
+    _convert,
     _convert_dtype,
     _project,
 )
@@ -86,3 +87,19 @@ def test_convert_dtype(mode):
 
     assert ll[-1].data[5, 5] == 1000
     assert ll[-1].data.flatten().sum() == 1000
+
+
+@pytest.mark.parametrize(
+    'input, type_',
+    [
+        (Image(np.random.rand(10, 10)), 'labels'),
+        (Labels(np.ones((10, 10), dtype=int)), 'image'),
+        (Shapes([np.array([[0, 0], [0, 10], [10, 0], [10, 10]])]), 'labels'),
+    ],
+)
+def test_convert_layer(input, type_):
+    ll = LayerList()
+    ll.append(input)
+    assert ll[0]._type_string != type_
+    _convert(ll, type_)
+    assert ll[0]._type_string == type_