11# SPDX-License-Identifier: GPL-3.0-or-later
22# SPDX-FileCopyrightText: 2026 sp00ktober
3- """The Behaviour dock : the port's behaviour graph (`mhfu_port.behaviour`) on a node canvas.
3+ """The Behaviour tab : the port's behaviour graph (`mhfu_port.behaviour`) on a node canvas.
44
5- A node is a move or a block; a wire is a link the manifest stores (`monster/behaviour.py`). The
6- document is the owner: every gesture of the canvas arrives as intents, is applied as one undo
7- step, and the canvas is shown again from the document.
5+ A node is a move or a block, drawn compact; a wire is a link the manifest stores
6+ (`monster/behaviour.py`). The selected node's values and note are edited in the bar under the
7+ canvas. The document is the owner: every gesture of the canvas and every edit of the bar arrives
8+ as intents, is applied as one undo step, and is shown again from the document.
89"""
910
1011from __future__ import annotations
1314from typing import TYPE_CHECKING
1415
1516from mhfu_port import behaviour as model
16- from mhfu_port .behaviour import KINDS
17- from mhfu_port .manifest import Manifest , ManifestError
17+ from mhfu_port .behaviour import KINDS , Kind
18+ from mhfu_port .manifest import Manifest , ManifestError , Move
1819from PySide6 .QtWidgets import QSizePolicy , QVBoxLayout , QWidget
1920
2021from mhfu_studio .monster import behaviour as graph
2122from mhfu_studio .monster .panels .common import kind
22- from mhfu_studio .monster .panels .node_graph import NAMES , GraphView , LinkSpec , NodeSpec
23+ from mhfu_studio .monster .panels .node_graph import GraphView , LinkSpec , NodeSpec
24+ from mhfu_studio .monster .panels .properties import Detail , PropertyBar
2325from mhfu_studio .monster .panels .widgets import NoScene
2426from mhfu_studio .shell .overlay import Color , Ink
2527from mhfu_studio .ui import kit , theme
@@ -65,6 +67,8 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
6567 self ._dropped : dict [str , graph .Point ] = {}
6668 #: the block the gesture creates
6769 self ._made : str | None = None
70+ #: what the canvas was last drawn from, for the bar
71+ self ._read : graph .Reading | None = None
6872
6973 self .title = kit .label (role = "title" , wrap = False )
7074 self .hint = kit .label (HINT , role = "muted" , wrap = False )
@@ -88,21 +92,21 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
8892 v .link_requested .connect (lambda * w : self ._collect (lambda e : graph .link (e , * w )))
8993 v .unlink_requested .connect (lambda * w : self ._collect (lambda e : graph .unlink (e , * w )))
9094 v .delete_requested .connect (lambda ids : self ._collect (lambda e : graph .delete (e , ids )))
91- v .param_changed .connect (
92- lambda i , n , x : self ._collect (lambda e : graph .set_param (e , i , n , x ))
93- )
94- v .label_changed .connect (lambda i , t : self ._collect (lambda e : graph .set_label (e , i , t )))
9595 v .moved .connect (lambda i , x , y : self ._dropped .__setitem__ (i , (x , y )))
9696 v .create_requested .connect (self ._create )
9797 v .picked .connect (self ._picked )
9898 v .settled .connect (self ._settle )
99+ self .bar = PropertyBar ()
100+ self .bar .param_changed .connect (self ._param )
101+ self .bar .label_changed .connect (self ._label )
99102
100103 page = QWidget ()
101104 lay = QVBoxLayout (page )
102105 lay .setContentsMargins (10 , 8 , 10 , 8 )
103106 lay .setSpacing (6 )
104107 lay .addWidget (kit .row (self .title , self .hint , self .fit , self .spread , stretch = True ))
105108 lay .addWidget (self .view , 1 )
109+ lay .addWidget (self .bar )
106110 self .no_scene = NoScene (studio )
107111 self .no_scene .say (
108112 "No port manifest" ,
@@ -117,6 +121,16 @@ def _spread(self) -> str:
117121 raise ManifestError ("no port manifest open" )
118122 return graph .spread (doc )
119123
124+ # what the bar tells: the same intents as the canvas's, one undo step each
125+
126+ def _param (self , node : str , name : str , value : object ) -> None :
127+ self ._collect (lambda e : graph .set_param (e , node , name , value ))
128+ self ._settle ()
129+
130+ def _label (self , node : str , text : str ) -> None :
131+ self ._collect (lambda e : graph .set_label (e , node , text ))
132+ self ._settle ()
133+
120134 # what the canvas tells
121135
122136 def _collect (self , fn : Callable [[graph .Edits ], str ]) -> None :
@@ -154,9 +168,11 @@ def edit() -> None:
154168 m = self .ws .manifest
155169 if made is not None and m is not None and made in m .behaviour .blocks :
156170 self .view .select ([made ])
171+ self ._show_bar ()
157172
158173 def _picked (self , node : object ) -> None :
159- """A move node picks its move, as the Moves table does."""
174+ """A move node picks its move, as the Moves table does; the bar shows the node."""
175+ self ._show_bar ()
160176 name = graph .move_name (node ) if isinstance (node , str ) else None
161177 if name is None or name == self .ws .move :
162178 return
@@ -182,9 +198,45 @@ def sync(self) -> None:
182198 self ._drawn = m
183199 self ._draw (m )
184200 self ._follow (m )
201+ self ._show_bar ()
202+
203+ def _show_bar (self ) -> None :
204+ """The bar shows the one node selected, from the document: a refused edit goes back."""
205+ m , ids = self .ws .manifest , self .view .selected ()
206+ self .bar .show_detail (self ._detail (m , ids [0 ]) if m is not None and len (ids ) == 1 else None )
207+
208+ def _detail (self , m : Manifest , node : str ) -> Detail | None :
209+ name = graph .move_name (node )
210+ if name is not None :
211+ mv = m .moves .get (name )
212+ return None if mv is None else Detail (node , name , self ._move_tip (mv ), label = mv .label )
213+ blk = m .behaviour .blocks .get (node )
214+ if blk is None or self ._read is None :
215+ return None
216+ k = KINDS [blk .kind ]
217+ values = model .params (blk )
218+ tip = self ._block_tip (k , self ._read .refused .get (node ))
219+ return Detail (
220+ node ,
221+ k .title ,
222+ tip ,
223+ k .params ,
224+ {p .name : values .get (p .name ) for p in k .params },
225+ graph .options (m , blk .kind ),
226+ blk .label ,
227+ )
228+
229+ @staticmethod
230+ def _block_tip (k : Kind , why : str | None ) -> str :
231+ return "\n " .join ([k .tip , * ([f"{ WARN } { why } " ] if why else [])])
232+
233+ @staticmethod
234+ def _move_tip (mv : Move ) -> str :
235+ plays = f"plays { mv .clip } " if mv .clip else f"plays anim { mv .anim } "
236+ return "\n " .join ([f"{ kind (mv )} · { plays } " , * ([f"Note: { mv .label } " ] if mv .label else [])])
185237
186238 def _draw (self , m : Manifest ) -> None :
187- r = graph .read (m )
239+ r = self . _read = graph .read (m )
188240 nodes = self ._blocks (m , r ) + self ._moves (m )
189241 self .view .show (nodes , [LinkSpec (* w ) for w in graph .wires (m )])
190242 loose = f" · { len (r .loose )} loose" if r .loose else ""
@@ -199,15 +251,7 @@ def _blocks(m: Manifest, r: graph.Reading) -> list[NodeSpec]:
199251 k = KINDS [blk .kind ]
200252 why = r .refused .get (i )
201253 marks = [* (f"#{ n } " for n in r .priority .get (i , [])), * ([WARN ] if why else [])]
202- values = model .params (blk )
203- options : dict [str , list [tuple [str , object ]]] = {}
204- for p in k .params :
205- if p .type == "part" :
206- options [p .name ] = list (graph .part_options (m , p .optional ))
207- elif p .type == "mains" :
208- options [p .name ] = list (graph .main_options ())
209- elif p .type in NAMES :
210- options [p .name ] = [(n , n ) for n in model .names (m , p .type )]
254+ note = [f"Note: { blk .label } " ] if blk .label else []
211255 out .append (
212256 NodeSpec (
213257 i ,
@@ -216,13 +260,10 @@ def _blocks(m: Manifest, r: graph.Reading) -> list[NodeSpec]:
216260 blk .at ,
217261 (graph .IN ,),
218262 (graph .OUT ,),
219- k .params ,
220- {p .name : values .get (p .name ) for p in k .params },
221- options ,
263+ model .summary (m , blk ),
222264 " " .join (marks ),
223265 i in r .loose ,
224- blk .label ,
225- "\n " .join ([k .tip , * ([f"{ WARN } { why } " ] if why else [])]),
266+ "\n " .join ([BehaviourPanel ._block_tip (k , why ), * note ]),
226267 )
227268 )
228269 return out
@@ -232,7 +273,6 @@ def _moves(m: Manifest) -> list[NodeSpec]:
232273 spots = graph .spots (m )
233274 out = []
234275 for name , mv in m .moves .items ():
235- plays = f"plays { mv .clip } " if mv .clip else f"plays anim { mv .anim } "
236276 out .append (
237277 NodeSpec (
238278 graph .move_id (name ),
@@ -241,8 +281,7 @@ def _moves(m: Manifest) -> list[NodeSpec]:
241281 spots [name ],
242282 (graph .PLAY ,),
243283 (graph .WHILE , graph .THEN ),
244- label = mv .label ,
245- tip = f"{ kind (mv )} · { plays } " ,
284+ tip = BehaviourPanel ._move_tip (mv ),
246285 )
247286 )
248287 return out
0 commit comments