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 )
@@ -94,14 +98,13 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
9498 v .link_requested .connect (lambda * w : self ._collect (lambda e : graph .link (e , * w )))
9599 v .unlink_requested .connect (lambda * w : self ._collect (lambda e : graph .unlink (e , * w )))
96100 v .delete_requested .connect (lambda ids : self ._collect (lambda e : graph .delete (e , ids )))
97- v .param_changed .connect (
98- lambda i , n , x : self ._collect (lambda e : graph .set_param (e , i , n , x ))
99- )
100- v .label_changed .connect (lambda i , t : self ._collect (lambda e : graph .set_label (e , i , t )))
101101 v .moved .connect (lambda i , x , y : self ._dropped .__setitem__ (i , (x , y )))
102102 v .create_requested .connect (self ._create )
103103 v .picked .connect (self ._picked )
104104 v .settled .connect (self ._settle )
105+ self .bar = PropertyBar ()
106+ self .bar .param_changed .connect (self ._param )
107+ self .bar .label_changed .connect (self ._label )
105108
106109 page = QWidget ()
107110 lay = QVBoxLayout (page )
@@ -111,6 +114,7 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
111114 kit .row (self .title , self .hint , self .rage , self .fit , self .spread , stretch = True )
112115 )
113116 lay .addWidget (self .view , 1 )
117+ lay .addWidget (self .bar )
114118 self .no_scene = NoScene (studio )
115119 self .no_scene .say (
116120 "No port manifest" ,
@@ -132,6 +136,16 @@ def _natural_rage(self, on: bool) -> None:
132136 "natural rage" , lambda : self .ws .edit ("" , lambda : graph .set_natural_rage (doc , on ))
133137 )()
134138
139+ # what the bar tells: the same intents as the canvas's, one undo step each
140+
141+ def _param (self , node : str , name : str , value : object ) -> None :
142+ self ._collect (lambda e : graph .set_param (e , node , name , value ))
143+ self ._settle ()
144+
145+ def _label (self , node : str , text : str ) -> None :
146+ self ._collect (lambda e : graph .set_label (e , node , text ))
147+ self ._settle ()
148+
135149 # what the canvas tells
136150
137151 def _collect (self , fn : Callable [[graph .Edits ], str ]) -> None :
@@ -169,9 +183,11 @@ def edit() -> None:
169183 m = self .ws .manifest
170184 if made is not None and m is not None and made in m .behaviour .blocks :
171185 self .view .select ([made ])
186+ self ._show_bar ()
172187
173188 def _picked (self , node : object ) -> None :
174- """A move node picks its move, as the Moves table does."""
189+ """A move node picks its move, as the Moves table does; the bar shows the node."""
190+ self ._show_bar ()
175191 name = graph .move_name (node ) if isinstance (node , str ) else None
176192 if name is None or name == self .ws .move :
177193 return
@@ -198,9 +214,45 @@ def sync(self) -> None:
198214 self ._draw (m )
199215 kit .put (self .rage , graph .natural_rage (m ))
200216 self ._follow (m )
217+ self ._show_bar ()
218+
219+ def _show_bar (self ) -> None :
220+ """The bar shows the one node selected, from the document: a refused edit goes back."""
221+ m , ids = self .ws .manifest , self .view .selected ()
222+ self .bar .show_detail (self ._detail (m , ids [0 ]) if m is not None and len (ids ) == 1 else None )
223+
224+ def _detail (self , m : Manifest , node : str ) -> Detail | None :
225+ name = graph .move_name (node )
226+ if name is not None :
227+ mv = m .moves .get (name )
228+ return None if mv is None else Detail (node , name , self ._move_tip (mv ), label = mv .label )
229+ blk = m .behaviour .blocks .get (node )
230+ if blk is None or self ._read is None :
231+ return None
232+ k = KINDS [blk .kind ]
233+ values = model .params (blk )
234+ tip = self ._block_tip (k , self ._read .refused .get (node ))
235+ return Detail (
236+ node ,
237+ k .title ,
238+ tip ,
239+ k .params ,
240+ {p .name : values .get (p .name ) for p in k .params },
241+ graph .options (m , blk .kind ),
242+ blk .label ,
243+ )
244+
245+ @staticmethod
246+ def _block_tip (k : Kind , why : str | None ) -> str :
247+ return "\n " .join ([k .tip , * ([f"{ WARN } { why } " ] if why else [])])
248+
249+ @staticmethod
250+ def _move_tip (mv : Move ) -> str :
251+ plays = f"plays { mv .clip } " if mv .clip else f"plays anim { mv .anim } "
252+ return "\n " .join ([f"{ kind (mv )} · { plays } " , * ([f"Note: { mv .label } " ] if mv .label else [])])
201253
202254 def _draw (self , m : Manifest ) -> None :
203- r = graph .read (m )
255+ r = self . _read = graph .read (m )
204256 nodes = self ._blocks (m , r ) + self ._moves (m )
205257 self .view .show (nodes , [LinkSpec (* w ) for w in graph .wires (m )])
206258 loose = f" · { len (r .loose )} loose" if r .loose else ""
@@ -215,15 +267,7 @@ def _blocks(m: Manifest, r: graph.Reading) -> list[NodeSpec]:
215267 k = KINDS [blk .kind ]
216268 why = r .refused .get (i )
217269 marks = [* (f"#{ n } " for n in r .priority .get (i , [])), * ([WARN ] if why else [])]
218- values = model .params (blk )
219- options : dict [str , list [tuple [str , object ]]] = {}
220- for p in k .params :
221- if p .type == "part" :
222- options [p .name ] = list (graph .part_options (m , p .optional ))
223- elif p .type == "mains" :
224- options [p .name ] = list (graph .main_options ())
225- elif p .type in NAMES :
226- options [p .name ] = [(n , n ) for n in model .names (m , p .type )]
270+ note = [f"Note: { blk .label } " ] if blk .label else []
227271 out .append (
228272 NodeSpec (
229273 i ,
@@ -232,13 +276,10 @@ def _blocks(m: Manifest, r: graph.Reading) -> list[NodeSpec]:
232276 blk .at ,
233277 (graph .IN ,),
234278 (graph .OUT ,),
235- k .params ,
236- {p .name : values .get (p .name ) for p in k .params },
237- options ,
279+ model .summary (m , blk ),
238280 " " .join (marks ),
239281 i in r .loose ,
240- blk .label ,
241- "\n " .join ([k .tip , * ([f"{ WARN } { why } " ] if why else [])]),
282+ "\n " .join ([BehaviourPanel ._block_tip (k , why ), * note ]),
242283 )
243284 )
244285 return out
@@ -248,7 +289,6 @@ def _moves(m: Manifest) -> list[NodeSpec]:
248289 spots = graph .spots (m )
249290 out = []
250291 for name , mv in m .moves .items ():
251- plays = f"plays { mv .clip } " if mv .clip else f"plays anim { mv .anim } "
252292 out .append (
253293 NodeSpec (
254294 graph .move_id (name ),
@@ -257,8 +297,7 @@ def _moves(m: Manifest) -> list[NodeSpec]:
257297 spots [name ],
258298 (graph .PLAY ,),
259299 (graph .WHILE , graph .THEN ),
260- label = mv .label ,
261- tip = f"{ kind (mv )} · { plays } " ,
300+ tip = BehaviourPanel ._move_tip (mv ),
262301 )
263302 )
264303 return out
0 commit comments