Repository: napari/napari

Issue:
Minor bug: in the layer contextual menu, Split Stack is not disabled for single-channel 2D images.
## 🐛 Bug

In the layer contextual menu, Split Stack is not disabled for 2D images. Clicking it results in an error: `ValueError: The image needs more than 2 dimensions for splitting`
This error is instructive, but the other functions of the menu are disabled when not usable, so this should also be disabled for consistency.

## To Reproduce

Steps to reproduce the behavior:

1. Open Binary blobs or any 2D single channel image
2. Right-click, Split Stack is not grey

<img width="304" alt="image" src="https://user-images.githubusercontent.com/76622105/208298172-e0b92073-1e15-4d13-b5b2-a16727291b51.png">

Note: Interestingly, all of the projections are disabled in the sub-menu, but not the menu itself.

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

## Expected behavior

The Split Stack menu item should be disabled.

## Environment
```
napari: 0.4.17rc4.dev124+g5e0aebee
Platform: macOS-12.6-arm64-arm-64bit
System: MacOS 12.6
Python: 3.10.8 | packaged by conda-forge | (main, Nov 22 2022, 08:25:29) [Clang 14.0.6 ]
Qt: 5.15.6
PyQt5: 5.15.7
NumPy: 1.23.5
SciPy: 1.9.3
Dask: 2022.12.1
VisPy: 0.12.1
magicgui: 0.6.1
superqt: unknown
in-n-out: 0.1.6
app-model: 0.1.1
npe2: 0.6.1

OpenGL:
- GL version: 2.1 Metal - 76.3
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1680x1050, scale 2.0

Settings path:
- /Users/piotrsobolewski/Library/Application Support/napari/napari-dev_a1eb8b76ba95fa16ad06e26097b46b8455dfbf0b/settings.yaml

```

Note: not sure why superqt is not reporting properly, it's 0.4.1:
`superqt                   0.4.1              pyhd8ed1ab_0    conda-forge`

## Additional context

<!-- Add any other context about the problem here. -->



Discussion:
Hi, I think I can help with this! Checking seems that what is missing is to use `_IMAGE_IS_3D` as the `enablement` kwarg for the `Split stack` (`CommandId.LAYER_SPLIT_STACK`) action at https://github.com/napari/napari/blob/b9e613acdcd088e61634e5dbdc553c942eb5a75d/napari/_app_model/actions/_layer_actions.py#L53-L58

and the `Projections` (`MenuId.LAYERS_PROJECT`) submenu item at https://github.com/napari/napari/blob/b9e613acdcd088e61634e5dbdc553c942eb5a75d/napari/_app_model/_submenus.py#L16-L23

right?

Also, seems like the `Convert data type` (`MenuId.LAYERS_CONVERT_DTYPE`) submenu item also needs to be disabled (using probably as `enablement` kwarg `_ONLY_LABELS`)?:

![convert](https://user-images.githubusercontent.com/16781833/208728880-4da9d9f9-2bb5-4a33-b6d0-1f5ec676137f.png)




Resolving patch:
diff --git a/napari/_app_model/_submenus.py b/napari/_app_model/_submenus.py
--- a/napari/_app_model/_submenus.py
+++ b/napari/_app_model/_submenus.py
@@ -1,6 +1,7 @@
 from app_model.types import SubmenuItem
 
 from napari._app_model.constants import MenuGroup, MenuId
+from napari._app_model.context import LayerListContextKeys as LLCK
 from napari.utils.translations import trans
 
 SUBMENUS = [
@@ -11,6 +12,7 @@
             title=trans._('Convert data type'),
             group=MenuGroup.LAYERLIST_CONTEXT.CONVERSION,
             order=None,
+            enablement=LLCK.all_selected_layers_labels,
         ),
     ),
     (
@@ -20,6 +22,7 @@
             title=trans._('Projections'),
             group=MenuGroup.LAYERLIST_CONTEXT.SPLIT_MERGE,
             order=None,
+            enablement=LLCK.active_layer_is_image_3d,
         ),
     ),
     (
diff --git a/napari/_app_model/actions/_layer_actions.py b/napari/_app_model/actions/_layer_actions.py
--- a/napari/_app_model/actions/_layer_actions.py
+++ b/napari/_app_model/actions/_layer_actions.py
@@ -37,10 +37,6 @@
     'group': MenuGroup.LAYERLIST_CONTEXT.LINK,
 }
 
-_ONLY_LABELS = LLCK.num_selected_labels_layers == LLCK.num_selected_layers
-_IMAGE_IS_3D = (LLCK.active_layer_type == "image") & LLCK.active_layer_ndim > 2
-
-
 # Statically defined Layer actions.
 # modifying this list at runtime has no effect.
 LAYER_ACTIONS: List[Action] = [
@@ -55,7 +51,7 @@
         title=CommandId.LAYER_SPLIT_STACK.title,
         callback=_layer_actions._split_stack,
         menus=[{**LAYERCTX_SPLITMERGE, 'when': ~LLCK.active_layer_is_rgb}],
-        enablement=LLCK.active_layer_type == "image",
+        enablement=LLCK.active_layer_is_image_3d,
     ),
     Action(
         id=CommandId.LAYER_SPLIT_RGB,
@@ -151,7 +147,10 @@
             id=cmd,
             title=cmd.title,
             callback=partial(_layer_actions._convert_dtype, mode=_dtype),
-            enablement=(_ONLY_LABELS & (LLCK.active_layer_dtype != _dtype)),
+            enablement=(
+                LLCK.all_selected_layers_labels
+                & (LLCK.active_layer_dtype != _dtype)
+            ),
             menus=[{'id': MenuId.LAYERS_CONVERT_DTYPE}],
         )
     )
@@ -163,7 +162,7 @@
             id=cmd,
             title=cmd.title,
             callback=partial(_layer_actions._project, mode=mode),
-            enablement=_IMAGE_IS_3D,
+            enablement=LLCK.active_layer_is_image_3d,
             menus=[{'id': MenuId.LAYERS_PROJECT}],
         )
     )
diff --git a/napari/_app_model/context/_layerlist_context.py b/napari/_app_model/context/_layerlist_context.py
--- a/napari/_app_model/context/_layerlist_context.py
+++ b/napari/_app_model/context/_layerlist_context.py
@@ -122,6 +122,14 @@ def _same_type(s: LayerSel) -> bool:
     return len({x._type_string for x in s}) == 1
 
 
+def _active_is_image_3d(s: LayerSel) -> bool:
+    return (
+        _active_type(s) == "image"
+        and _active_ndim(s) is not None
+        and (_active_ndim(s) > 3 or (_active_ndim(s) > 2 and not _is_rgb(s)))
+    )
+
+
 class LayerListContextKeys(ContextNamespace['LayerSel']):
     """These are the available context keys relating to a LayerList.
 
@@ -206,6 +214,11 @@ class LayerListContextKeys(ContextNamespace['LayerSel']):
         trans._("Shape of the active layer, or `None` if nothing is active."),
         _active_shape,
     )
+    active_layer_is_image_3d = ContextKey(
+        False,
+        trans._("True when the active layer is a 3D image."),
+        _active_is_image_3d,
+    )
     active_layer_dtype = ContextKey(
         None,
         trans._("Dtype of the active layer, or `None` if nothing is active."),
@@ -221,3 +234,8 @@ class LayerListContextKeys(ContextNamespace['LayerSel']):
         trans._("True when all selected layers are of the same type."),
         _same_type,
     )
+    all_selected_layers_labels = ContextKey(
+        False,
+        trans._("True when all selected layers are labels."),
+        _only_labels,
+    )


Test patch: