33"""The Behaviour tab: the port's behaviour graph (`mhfu_port.behaviour`) on a node canvas.
44
55A 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.
6+ (`monster/behaviour.py`). The outline beside the canvas scopes it: All is the canvas as stored,
7+ a trigger shows only its rules, one framed row each in the order they are checked (`rows`), laid
8+ out for display and never written to a block's `at`. The selected node's values and note are
9+ edited in the bar under the canvas. The document is the owner: every gesture of the canvas and
10+ every edit of the bar arrives as intents, is applied as one undo step, and is shown again from
11+ the document.
912"""
1013
1114from __future__ import annotations
1215
16+ import dataclasses
1317from collections .abc import Callable
1418from typing import TYPE_CHECKING
1519
1620from mhfu_port import behaviour as model
17- from mhfu_port .behaviour import KINDS , Kind
21+ from mhfu_port .behaviour import KINDS , Kind , Trigger
1822from mhfu_port .manifest import Manifest , ManifestError , Move
19- from PySide6 .QtWidgets import QSizePolicy , QVBoxLayout , QWidget
23+ from PySide6 .QtWidgets import QHBoxLayout , QSizePolicy , QVBoxLayout , QWidget
2024
2125from mhfu_studio .monster import behaviour as graph
26+ from mhfu_studio .monster .behaviour import ALL
27+ from mhfu_studio .monster .panels import rows , triggers
2228from mhfu_studio .monster .panels .common import kind
2329from mhfu_studio .monster .panels .node_graph import GraphView , LinkSpec , NodeSpec
2430from mhfu_studio .monster .panels .properties import Detail , PropertyBar
3642 "state" : Ink .AXIS_Y ,
3743 "condition" : Ink .AXIS_Z ,
3844 "modifier" : Ink .WARNING ,
45+ "effect" : Ink .EFFECT ,
3946 "move" : Ink .SELECTION ,
4047}
41- #: the effect role's violet, for a dark theme and a light one: no `Ink` is one
42- EFFECT : dict [bool , Color ] = {True : (0.82 , 0.44 , 0.91 , 1.0 ), False : (0.56 , 0.14 , 0.67 , 1.0 )}
4348MOVE_ROLE = "move"
4449WARN = chr (0x26A0 )
4550HINT = (
4651 "Right-click adds a block · drag from a port to wire · higher blocks are checked first,"
4752 " but paths that play nothing run before all of them"
4853)
49-
50-
51- def roles () -> dict [str , Color ]:
52- """`ROLES` and the effect's violet, for the current theme."""
53- return {** ROLES , "effect" : EFFECT [theme .current ().dark ]}
54+ #: said for the intents a trigger's rows do not take yet, and over the rows
55+ SCOPED = "Create and wire in All for now"
5456
5557
5658class BehaviourPanel (kit .Panel ):
@@ -67,8 +69,11 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
6769 self ._dropped : dict [str , graph .Point ] = {}
6870 #: the block the gesture creates
6971 self ._made : str | None = None
70- #: what the canvas was last drawn from, for the bar
72+ #: what the canvas was last drawn from, for the bar and the outline
7173 self ._read : graph .Reading | None = None
74+ self ._triggers : list [Trigger ] = []
75+ self ._scope : str | None = None
76+ self ._outlined : object = None
7277
7378 self .title = kit .label (role = "title" , wrap = False )
7479 self .hint = kit .label (HINT , role = "muted" , wrap = False )
@@ -95,20 +100,28 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
95100 self .view = GraphView ()
96101 self .view .set_palette (graph .palette ())
97102 v = self .view
98- v .link_requested .connect (lambda * w : self ._collect (lambda e : graph .link ( e , * w )))
99- v .unlink_requested .connect (lambda * w : self ._collect (lambda e : graph .unlink ( e , * w )))
100- v .delete_requested .connect (lambda ids : self ._collect ( lambda e : graph . delete ( e , ids )) )
101- v .moved .connect (lambda i , x , y : self ._dropped . __setitem__ ( i , ( x , y )) )
103+ v .link_requested .connect (lambda * w : self ._collect (self . _wire ( graph .link , * w )))
104+ v .unlink_requested .connect (lambda * w : self ._collect (self . _wire ( graph .unlink , * w )))
105+ v .delete_requested .connect (self ._delete )
106+ v .moved .connect (self ._moved )
102107 v .create_requested .connect (self ._create )
103108 v .picked .connect (self ._picked )
104109 v .settled .connect (self ._settle )
105110 self .bar = PropertyBar ()
106111 self .bar .param_changed .connect (self ._param )
107112 self .bar .label_changed .connect (self ._label )
108113
114+ self .outline = triggers .Outline ()
115+ self .outline .picked .connect (self ._scoped )
109116 page = QWidget ()
110- lay = QVBoxLayout (page )
111- lay .setContentsMargins (10 , 8 , 10 , 8 )
117+ side = QHBoxLayout (page )
118+ side .setContentsMargins (10 , 8 , 10 , 8 )
119+ side .setSpacing (10 )
120+ side .addWidget (self .outline )
121+ main = QWidget ()
122+ side .addWidget (main , 1 )
123+ lay = QVBoxLayout (main )
124+ lay .setContentsMargins (0 , 0 , 0 , 0 )
112125 lay .setSpacing (6 )
113126 lay .addWidget (
114127 kit .row (self .title , self .hint , self .rage , self .fit , self .spread , stretch = True )
@@ -136,6 +149,11 @@ def _natural_rage(self, on: bool) -> None:
136149 "natural rage" , lambda : self .ws .edit ("" , lambda : graph .set_natural_rage (doc , on ))
137150 )()
138151
152+ def _scoped (self , key : str ) -> None :
153+ """A row of the outline picked: that trigger's rules, framed to fit."""
154+ self .ws .scope = key
155+ self .sync ()
156+
139157 # what the bar tells: the same intents as the canvas's, one undo step each
140158
141159 def _param (self , node : str , name : str , value : object ) -> None :
@@ -156,11 +174,34 @@ def _collect(self, fn: Callable[[graph.Edits], str]) -> None:
156174 self ._gesture = graph .Gesture (doc )
157175 self ._gesture .run (fn )
158176
177+ def _scope_all (self ) -> bool :
178+ return self .ws .scope == ALL
179+
180+ def _wire (self , edit : Callable [..., str ], * wire : str ) -> Callable [[graph .Edits ], str ]:
181+ """The edit of a wire intent, or the refusal a trigger's rows give for it."""
182+ return (lambda e : edit (e , * wire )) if self ._scope_all () else self ._not_here
183+
184+ @staticmethod
185+ def _not_here (_ : graph .Edits ) -> str :
186+ raise ManifestError (SCOPED )
187+
188+ def _delete (self , ids : list [str ]) -> None :
189+ nodes = list (dict .fromkeys (rows .node_of (i ) for i in ids )) # a reference is its move
190+ self ._collect (lambda e : graph .delete (e , nodes ))
191+
192+ def _moved (self , node : str , x : float , y : float ) -> None :
193+ if self ._scope_all (): # the rows are laid out for display: a drag snaps back
194+ self ._dropped [node ] = (x , y )
195+
159196 def _create (self , kind_ : str , x : float , y : float ) -> None :
160197 doc = self .ws .doc
161- if doc is not None :
162- self ._made = model .new_id (doc .manifest .behaviour )
163- self ._collect (lambda e : graph .add_block (e , kind_ , (x , y )))
198+ if doc is None :
199+ return
200+ if not self ._scope_all ():
201+ self ._collect (self ._not_here )
202+ return
203+ self ._made = model .new_id (doc .manifest .behaviour )
204+ self ._collect (lambda e : graph .add_block (e , kind_ , (x , y )))
164205
165206 def _settle (self ) -> None :
166207 """The gesture is whole: one undo step, or one refusal."""
@@ -188,7 +229,7 @@ def edit() -> None:
188229 def _picked (self , node : object ) -> None :
189230 """A move node picks its move, as the Moves table does; the bar shows the node."""
190231 self ._show_bar ()
191- name = graph .move_name (node ) if isinstance (node , str ) else None
232+ name = graph .move_name (rows . node_of ( node ) ) if isinstance (node , str ) else None
192233 if name is None or name == self .ws .move :
193234 return
194235
@@ -207,21 +248,39 @@ def sync(self) -> None:
207248 return
208249 if theme .current () is not self ._theme :
209250 self ._theme = theme .current ()
210- self .view .set_roles (roles () )
251+ self .view .set_roles (ROLES )
211252 self .view .set_canvas (theme .current ().view )
253+ self ._outlined = None
212254 if self ._drawn is not m :
213255 self ._drawn = m
214- self ._draw (m )
256+ self ._read = graph .read (m )
257+ self ._triggers = model .triggers (m )
258+ self ._scope = None
215259 kit .put (self .rage , graph .natural_rage (m ))
216- self ._follow (m )
260+ block = self .ws .take_block ()
261+ if block in m .behaviour .blocks :
262+ self ._reach (block )
263+ if self .ws .scope not in (ALL , * (t .key for t in self ._triggers )):
264+ self .ws .scope = ALL # its trigger is gone
265+ if self ._scope != self .ws .scope or self ._outlined is None :
266+ self ._draw (m )
267+ self ._follow (m , block )
217268 self ._show_bar ()
218269
270+ def _reach (self , block : str ) -> None :
271+ """A block that a finding named and the scope does not draw: its trigger's rows, or All
272+ for a block on no rule."""
273+ scope = next ((t for t in self ._triggers if t .key == self .ws .scope ), None )
274+ if scope is not None and not any (block in p .blocks for _ , p in scope .rules ):
275+ self .ws .scope = triggers .owner (self ._triggers , block ) or ALL
276+
219277 def _show_bar (self ) -> None :
220278 """The bar shows the one node selected, from the document: a refused edit goes back."""
221279 m , ids = self .ws .manifest , self .view .selected ()
222280 self .bar .show_detail (self ._detail (m , ids [0 ]) if m is not None and len (ids ) == 1 else None )
223281
224282 def _detail (self , m : Manifest , node : str ) -> Detail | None :
283+ node = rows .node_of (node )
225284 name = graph .move_name (node )
226285 if name is not None :
227286 mv = m .moves .get (name )
@@ -252,13 +311,52 @@ def _move_tip(mv: Move) -> str:
252311 return "\n " .join ([f"{ kind (mv )} · { plays } " , * ([f"Note: { mv .label } " ] if mv .label else [])])
253312
254313 def _draw (self , m : Manifest ) -> None :
255- r = self ._read = graph .read (m )
256- nodes = self ._blocks (m , r ) + self ._moves (m )
257- self .view .show (nodes , [LinkSpec (* w ) for w in graph .wires (m )])
258- loose = f" · { len (r .loose )} loose" if r .loose else ""
259- self .title .setText (
260- f"{ len (m .behaviour .blocks )} blocks · { len (r .paths )} of { model .SEAM_RULES } paths" + loose
314+ r = self ._read
315+ assert r is not None
316+ scope = self .ws .scope
317+ total = len (r .paths )
318+ key = (tuple ((t .key , t .ranks ) for t in self ._triggers ), total , scope , theme .current ())
319+ if key != self ._outlined :
320+ self ._outlined = key
321+ self .outline .set_triggers (self ._triggers , total , scope )
322+ blocks , moves = self ._blocks (m , r ), self ._moves (m )
323+ t = next ((t for t in self ._triggers if t .key == scope ), None )
324+ if t is None :
325+ self .view .show (blocks + moves , [LinkSpec (* w ) for w in graph .wires (m )])
326+ loose = f" · { len (r .loose )} loose" if r .loose else ""
327+ self .title .setText (
328+ f"{ len (m .behaviour .blocks )} blocks · { total } of { model .SEAM_RULES } paths" + loose
329+ )
330+ else :
331+ self ._draw_rows (m , r , t , blocks , moves )
332+ self .title .setText (triggers .says (t , total ))
333+ self .hint .setText (HINT if t is None else SCOPED )
334+ self .spread .setEnabled (t is None )
335+ if self ._scope is not None and self ._scope != scope :
336+ self .view .fit ()
337+ self ._scope = scope
338+
339+ def _draw_rows (
340+ self ,
341+ m : Manifest ,
342+ r : graph .Reading ,
343+ t : Trigger ,
344+ blocks : list [NodeSpec ],
345+ moves : list [NodeSpec ],
346+ ) -> None :
347+ """`t`'s rules as rows: drawn once to measure the nodes, then placed."""
348+ notes = {i : b .label for i , b in m .behaviour .blocks .items ()}
349+ plan = rows .plan (
350+ t ,
351+ {n .id : n for n in blocks },
352+ notes ,
353+ dict (zip (m .moves , moves , strict = True )),
354+ rows .players (r .paths ),
261355 )
356+ links = list (plan .links )
357+ self .view .show ([dataclasses .replace (n , at = (0.0 , 0.0 )) for n in plan .nodes ], links )
358+ placed , frames = rows .place (plan , self .view .sizes ())
359+ self .view .show (placed , links , frames )
262360
263361 @staticmethod
264362 def _blocks (m : Manifest , r : graph .Reading ) -> list [NodeSpec ]:
@@ -302,10 +400,9 @@ def _moves(m: Manifest) -> list[NodeSpec]:
302400 )
303401 return out
304402
305- def _follow (self , m : Manifest ) -> None :
403+ def _follow (self , m : Manifest , block : str | None ) -> None :
306404 """A block a finding named is picked and framed; a move picked elsewhere is selected."""
307405 ws = self .ws
308- block = ws .take_block ()
309406 if block in m .behaviour .blocks :
310407 self .view .select ([block ])
311408 self .view .frame ([block ])
0 commit comments