Repository: gaphor/gaphor

Issue:
Activity Diagram: name of Parameter Nodes does not show in Model Browser
### Describe the bug
as from the title

### To Reproduce
Steps to reproduce the behavior:
1. Create a Diagram in SysML profile
2. Create an Activity
3. add one Parameter Node and change the name
4. The name does not show in the model browser that shows <none> instead

### Expected behavior
The Parameter node shown in the model browser should show its name instead of <none>

### Screenshots

![Image](https://github.com/user-attachments/assets/4cee3e0e-2d20-492e-9933-61a2c95627b8)

### OS
- [ ] Linux (Please put in notes the specific distro)
- [ ] macOS
- [x] Windows

### Version
Version of Gaphor: 3.0.0




Discussion:
Hi @bgkerion, 

Thanks for your report. It looks like we need to add a custom formatter for `ActivityParameterNode`, like we do in [umlfmt.py](https://github.com/gaphor/gaphor/blob/main/gaphor/UML/umlfmt.py).

Do you feel like adding a PR for this?
Hi @amolenaar, 
I'm super busy right now but I can try to do it as soon as possible. 



Patch:
diff --git a/gaphor/UML/treemodel.py b/gaphor/UML/treemodel.py
index 875bdf1fb7..14578c339a 100644
--- a/gaphor/UML/treemodel.py
+++ b/gaphor/UML/treemodel.py
@@ -186,11 +186,33 @@ def template(self) -> str:
     def root(self) -> Gio.ListStore:
         return self.branches[Root].elements
 
-    def sync(self, element):
-        if owner(element) and (tree_item := self.tree_item_for_element(element)):
-            tree_item.sync()
-            if self._on_sync:
-                self._on_sync()
+    def sync(self, element, seen=None):
+        if seen is None:
+            seen = set()
+
+        if (
+            element in seen
+            or not owner(element)
+            or not (tree_item := self.tree_item_for_element(element))
+        ):
+            return
+
+        seen.add(element)
+        tree_item.sync()
+        if self._on_sync:
+            self._on_sync()
+
+        if own := owner(element):
+            self.sync(own, seen)
+
+        if isinstance(element, UML.Parameter) and element.activityParameterNode:
+            for node in element.activityParameterNode:
+                self.sync(node, seen)
+
+        if isinstance(element, UML.Type):
+            for e in self.element_factory.select(UML.TypedElement):
+                if e.type is element:
+                    self.sync(e, seen)
 
     def child_model(self, item: TreeItem) -> Gio.ListStore | None:
         """This method will create branches on demand (lazy)."""
diff --git a/gaphor/UML/umlfmt.py b/gaphor/UML/umlfmt.py
index cc045b348d..994e8f7af2 100644
--- a/gaphor/UML/umlfmt.py
+++ b/gaphor/UML/umlfmt.py
@@ -259,3 +259,12 @@ def format_include(el):
 def format_call_behavior_action_name(el):
     """Name conforms to UML2.5.1 16.3.4.1 naming description"""
     return el.behavior.name if el.behavior and not el.name else el.name or ""
+
+
+@format.register(UML.ActivityParameterNode)
+def format_activity_parameter_node_name(
+    el, direction=False, type=False, multiplicity=False, default=False
+):
+    if el.parameter is None:
+        return ""
+    return format_parameter(el.parameter, direction, type, multiplicity, default)


Test patch:
diff --git a/gaphor/UML/tests/test_umlfmt.py b/gaphor/UML/tests/test_umlfmt.py
index d20e0733a9..b9dc7638f1 100644
--- a/gaphor/UML/tests/test_umlfmt.py
+++ b/gaphor/UML/tests/test_umlfmt.py
@@ -170,3 +170,22 @@ def test_pin(factory):
     recipes.set_multiplicity_upper_value(pin, "*")
 
     assert format(pin) == "foo: MyClass[1..*]"
+
+
+@pytest.mark.parametrize(
+    "text,formatted_text",
+    [
+        ("", ""),
+        ("param", "in param"),
+        ("in param: str", "in param: str"),
+        ("param = val", "in param = val"),
+    ],
+)
+def test_activity_parameter_node(factory, text, formatted_text):
+    """Test simple operation formatting."""
+    p = factory.create(UML.Parameter)
+    n = factory.create(UML.ActivityParameterNode)
+    n.parameter = p
+    parse(p, text)
+
+    assert formatted_text == format(n)