Repository: napari/napari

Issue:
Bug: toggle_visibility layer action is unpredictable with linked layers
## 🐛 Bug

With linked layers, the toggle_visibility layer action can be unpredictable, when more then 1 linked layer is selected:
Initial state (top 3 layers are linked)
<img width="266" alt="image" src="https://user-images.githubusercontent.com/76622105/227026467-4e1784a9-5725-46d1-be2e-58597571f86f.png">

Select some layers, including 2 linked and 1 unlinked:
<img width="268" alt="image" src="https://user-images.githubusercontent.com/76622105/227026529-0e20ef6b-7c3e-46d7-bb93-a18ac2ea343d.png">

Toggle should yield the top 3 visible (due to link) and 4rth not visible. But instead the linked layers are all hidden:
<img width="267" alt="image" src="https://user-images.githubusercontent.com/76622105/227026742-361adab7-4ece-4fab-a7ab-ae5dc1790ffd.png">

@czaki and @jni noted this in community meeting today.
They explained it that when the first is toggled, it toggles all the linked layers, making them visible, then when the next linked on is toggled, it flips them back to hidden.


## To Reproduce

Steps to reproduce the behavior:

1. Make 4+ layers
2. Link 3 of them
3. Select 2+ of the linked layers and one more not linked on
4. Try to toggle visibility

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

## Expected behavior

The visibility of all of the linked layers should be toggled—just once.

## Environment

```
napari: 0.5.0a2.dev51+gfc038f36
Platform: macOS-13.2.1-arm64-arm-64bit
System: MacOS 13.2.1
Python: 3.10.9 | packaged by conda-forge | (main, Feb 2 2023, 20:26:08) [Clang 14.0.6 ]
Qt: 5.15.6
PyQt5: 5.15.7
NumPy: 1.24.2
SciPy: 1.10.1
Dask: 2023.3.1
VisPy: 0.12.2
magicgui: 0.7.2
superqt: unknown
in-n-out: 0.1.7
app-model: 0.1.2
npe2: 0.6.2

OpenGL:
- GL version: 2.1 Metal - 83
- 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

```

## Additional context

<!-- Add any other context about the problem here. -->



Discussion:

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
@@ -89,8 +89,13 @@ def _merge_stack(ll: LayerList, rgb=False):
 
 
 def _toggle_visibility(ll: LayerList):
-    for lay in ll.selection:
-        lay.visible = not lay.visible
+    current_visibility_state = []
+    for layer in ll.selection:
+        current_visibility_state.append(layer.visible)
+
+    for visibility, layer in zip(current_visibility_state, ll.selection):
+        if layer.visible == visibility:
+            layer.visible = not visibility
 
 
 def _link_selected_layers(ll: LayerList):


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
@@ -7,10 +7,51 @@
     _convert,
     _convert_dtype,
     _duplicate_layer,
+    _link_selected_layers,
     _project,
+    _toggle_visibility,
 )
 
 
+def test_toggle_visibility():
+    """Test toggling visibility of a layer."""
+    layer_list = LayerList()
+    layer_list.append(Points([[0, 0]]))
+    layer_list[0].visible = False
+
+    layer_list.selection.active = layer_list[0]
+    _toggle_visibility(layer_list)
+
+    assert layer_list[0].visible is True
+
+
+def test_toggle_visibility_with_linked_layers():
+    """Test toggling visibility of a layer."""
+    layer_list = LayerList()
+    layer_list.append(Points([[0, 0]]))
+    layer_list.append(Points([[0, 0]]))
+    layer_list.append(Points([[0, 0]]))
+    layer_list.append(Points([[0, 0]]))
+
+    layer_list.selection.active = layer_list[0]
+    layer_list.selection.add(layer_list[1])
+    layer_list.selection.add(layer_list[2])
+
+    _link_selected_layers(layer_list)
+
+    layer_list[3].visible = False
+
+    layer_list.selection.remove(layer_list[0])
+    layer_list.selection.add(layer_list[3])
+
+    _toggle_visibility(layer_list)
+
+    assert layer_list[0].visible is False
+    assert layer_list[1].visible is False
+    assert layer_list[2].visible is False
+    assert layer_list[3].visible is True
+
+
 def test_duplicate_layers():
     def _dummy():
         pass