3737ACCENT = "#89b4fa" # blue – minimal
3838ACCENT_OPT = "#a6e3a1" # green – optional
3939ACCENT_REJ = "#f38ba8" # red – rejection / neutral
40+ ACCENT_ALWAYS = "#cba6f7" # purple – loosest/always-true (no bit set)
4041FG = "#cdd6f4"
4142FG_DIM = "#6c7086"
4243BORDER = "#45475a"
@@ -81,6 +82,17 @@ def bin_type(b):
8182 return "neutral"
8283
8384
85+ def has_only_skipped_minimal_bins (group ):
86+ """
87+ Returns True when a group has at least one minimal bin but ALL of them have
88+ BitPosition=="X" — meaning the cut is the loosest (always-true) selection
89+ and no bit needs to be set. The group should still be displayed so the
90+ user knows the cut exists.
91+ """
92+ minimal = [b for b in group if b .get ("MinimalCut" , "0" ) == "1" and b .get ("OptionalCut" , "0" ) == "0" ]
93+ return bool (minimal ) and all (b .get ("BitPosition" , "X" ).upper () == "X" for b in minimal )
94+
95+
8496def load_bins_from_hist (hist ):
8597 nbins = hist .GetNbinsX ()
8698 bins = []
@@ -105,8 +117,8 @@ def __init__(self, rootfile=None, tdir="femto-producer"):
105117 super ().__init__ ()
106118 self .title ("CutCulator" )
107119 self .configure (bg = BG )
108- self .geometry ("920x720 " )
109- self .minsize (780 , 560 )
120+ self .geometry ("980x820 " )
121+ self .minsize (780 , 600 )
110122 self .resizable (True , True )
111123
112124 self ._rootfile_path = rootfile
@@ -153,14 +165,23 @@ def _build_ui(self):
153165 # ── legend ──
154166 legend = tk .Frame (self , bg = BG , pady = 4 , padx = 18 )
155167 legend .pack (fill = "x" )
156- for color , label in [(ACCENT , "Minimal" ), (ACCENT_OPT , "Optional" ), (ACCENT_REJ , "Neutral" )]:
168+ for color , label in [
169+ (ACCENT , "Minimal" ),
170+ (ACCENT_OPT , "Optional" ),
171+ (ACCENT_REJ , "Neutral" ),
172+ (ACCENT_ALWAYS , "Loosest (no bit set)" ),
173+ ]:
157174 dot = tk .Label (legend , text = "●" , font = FONT_BODY , bg = BG , fg = color )
158175 dot .pack (side = "left" )
159176 tk .Label (legend , text = label , font = FONT_SMALL , bg = BG , fg = FG_DIM ).pack (side = "left" , padx = (2 , 12 ))
160177
178+ # ── main paned area: selection list (top) + summary (bottom) ──
179+ paned = tk .PanedWindow (self , orient = "vertical" , bg = BORDER , sashwidth = 4 , sashrelief = "flat" )
180+ paned .pack (fill = "both" , expand = True , padx = 12 , pady = 4 )
181+
161182 # ── scrollable selection area ──
162- outer = tk .Frame (self , bg = BG )
163- outer . pack ( fill = "both " , expand = True , padx = 12 , pady = 4 )
183+ outer = tk .Frame (paned , bg = BG )
184+ paned . add ( outer , stretch = "always " , minsize = 200 )
164185
165186 self ._canvas = tk .Canvas (outer , bg = BG , highlightthickness = 0 , bd = 0 )
166187 vsb = ttk .Scrollbar (outer , orient = "vertical" , command = self ._canvas .yview )
@@ -176,6 +197,32 @@ def _build_ui(self):
176197 self ._canvas .bind_all ("<Button-4>" , self ._on_mousewheel )
177198 self ._canvas .bind_all ("<Button-5>" , self ._on_mousewheel )
178199
200+ # ── summary panel ──
201+ summary_outer = tk .Frame (paned , bg = BG_CARD )
202+ paned .add (summary_outer , stretch = "never" , minsize = 120 )
203+
204+ summary_hdr = tk .Frame (summary_outer , bg = BG_CARD , pady = 5 , padx = 10 )
205+ summary_hdr .pack (fill = "x" )
206+ tk .Label (summary_hdr , text = "Selected cuts" , font = FONT_HEAD , bg = BG_CARD , fg = FG ).pack (side = "left" )
207+ self ._lbl_summary_empty = tk .Label (summary_hdr , text = "(none)" , font = FONT_SMALL , bg = BG_CARD , fg = FG_DIM )
208+ self ._lbl_summary_empty .pack (side = "left" , padx = 8 )
209+
210+ tk .Frame (summary_outer , bg = BORDER , height = 1 ).pack (fill = "x" )
211+
212+ summary_scroll_outer = tk .Frame (summary_outer , bg = BG_CARD )
213+ summary_scroll_outer .pack (fill = "both" , expand = True )
214+
215+ self ._summary_canvas = tk .Canvas (summary_scroll_outer , bg = BG_CARD , highlightthickness = 0 , bd = 0 , height = 100 )
216+ summary_vsb = ttk .Scrollbar (summary_scroll_outer , orient = "vertical" , command = self ._summary_canvas .yview )
217+ self ._summary_canvas .configure (yscrollcommand = summary_vsb .set )
218+ summary_vsb .pack (side = "right" , fill = "y" )
219+ self ._summary_canvas .pack (side = "left" , fill = "both" , expand = True )
220+
221+ self ._summary_inner = tk .Frame (self ._summary_canvas , bg = BG_CARD )
222+ self ._summary_canvas_win = self ._summary_canvas .create_window ((0 , 0 ), window = self ._summary_inner , anchor = "nw" )
223+ self ._summary_inner .bind ("<Configure>" , self ._on_summary_inner_configure )
224+ self ._summary_canvas .bind ("<Configure>" , self ._on_summary_canvas_configure )
225+
179226 # ── bottom result bar ──
180227 bottom = tk .Frame (self , bg = BG_CARD , pady = 10 , padx = 18 )
181228 bottom .pack (fill = "x" , side = "bottom" )
@@ -224,6 +271,12 @@ def _on_inner_configure(self, _e=None):
224271 def _on_canvas_configure (self , e ):
225272 self ._canvas .itemconfig (self ._canvas_win , width = e .width )
226273
274+ def _on_summary_inner_configure (self , _e = None ):
275+ self ._summary_canvas .configure (scrollregion = self ._summary_canvas .bbox ("all" ))
276+
277+ def _on_summary_canvas_configure (self , e ):
278+ self ._summary_canvas .itemconfig (self ._summary_canvas_win , width = e .width )
279+
227280 def _on_mousewheel (self , e ):
228281 if e .num == 4 :
229282 self ._canvas .yview_scroll (- 1 , "units" )
@@ -292,13 +345,17 @@ def _build_selections(self):
292345 self ._on_inner_configure ()
293346
294347 def _build_group_card (self , sel_name , group ):
295- # categorise
348+ # categorise into selectable bins
296349 minimal = [(i , b ) for i , b in enumerate (group ) if bin_type (b ) == "minimal" ]
297350 optional = [(i , b ) for i , b in enumerate (group ) if bin_type (b ) == "optional" ]
298351 neutral = [(i , b ) for i , b in enumerate (group ) if bin_type (b ) == "neutral" ]
299352
300- if not (minimal or optional or neutral ):
301- return # nothing to show (all "skip")
353+ # detect loosest/always-true: minimal bins exist but all have BitPosition==X
354+ loosest_only = has_only_skipped_minimal_bins (group )
355+
356+ # skip entirely if there is truly nothing to show
357+ if not (minimal or optional or neutral or loosest_only ):
358+ return
302359
303360 card = tk .Frame (self ._inner , bg = BG_CARD , bd = 0 , highlightthickness = 1 , highlightbackground = BORDER )
304361 card .pack (fill = "x" , padx = 10 , pady = 5 , ipadx = 8 , ipady = 6 )
@@ -308,27 +365,42 @@ def _build_group_card(self, sel_name, group):
308365 hdr .pack (fill = "x" , padx = 6 , pady = (4 , 2 ))
309366 tk .Label (hdr , text = sel_name , font = FONT_HEAD , bg = BG_CARD , fg = FG ).pack (side = "left" )
310367
311- # show the loosest (most permissive) minimal threshold as a hint.
312- # the truly loosest threshold has mSkipMostPermissiveBit=true so its
313- # BitPosition is "X" — it lands in the "skip" category. check there first,
314- # then fall back to the loosest bit-carrying minimal bin.
315- skipped_minimal = [
316- b for b in group if b .get ("MinimalCut" , "0" ) == "1" and b .get ("BitPosition" , "X" ).upper () == "X"
317- ]
318- if skipped_minimal :
319- loosest_val = format_value_with_comment (skipped_minimal [0 ])
368+ if loosest_only :
369+ # All minimal bins are BitPosition==X → loosest cut, no bit set
320370 tk .Label (
321- hdr , text = f"minimal cut → loosest selection: { loosest_val } " , font = FONT_SMALL , bg = BG_CARD , fg = FG_DIM
371+ hdr ,
372+ text = "loosest selection — no bit set" ,
373+ font = FONT_SMALL ,
374+ bg = BG_CARD ,
375+ fg = ACCENT_ALWAYS ,
322376 ).pack (side = "left" , padx = 10 )
323- elif minimal :
324- loosest_val = format_value_with_comment (minimal [0 ][1 ])
325- tk .Label (
326- hdr , text = f"minimal cut → loosest selection: { loosest_val } " , font = FONT_SMALL , bg = BG_CARD , fg = FG_DIM
327- ).pack (side = "left" , padx = 10 )
328- elif optional :
329- tk .Label (hdr , text = "optional" , font = FONT_SMALL , bg = BG_CARD , fg = ACCENT_OPT ).pack (side = "left" , padx = 10 )
330- elif neutral :
331- tk .Label (hdr , text = "neutral" , font = FONT_SMALL , bg = BG_CARD , fg = ACCENT_REJ ).pack (side = "left" , padx = 10 )
377+ else :
378+ # show the loosest (most permissive) minimal threshold as a hint
379+ skipped_minimal = [
380+ b for b in group if b .get ("MinimalCut" , "0" ) == "1" and b .get ("BitPosition" , "X" ).upper () == "X"
381+ ]
382+ if skipped_minimal :
383+ loosest_val = format_value_with_comment (skipped_minimal [0 ])
384+ tk .Label (
385+ hdr ,
386+ text = f"minimal cut → loosest selection: { loosest_val } " ,
387+ font = FONT_SMALL ,
388+ bg = BG_CARD ,
389+ fg = FG_DIM ,
390+ ).pack (side = "left" , padx = 10 )
391+ elif minimal :
392+ loosest_val = format_value_with_comment (minimal [0 ][1 ])
393+ tk .Label (
394+ hdr ,
395+ text = f"minimal cut → loosest selection: { loosest_val } " ,
396+ font = FONT_SMALL ,
397+ bg = BG_CARD ,
398+ fg = FG_DIM ,
399+ ).pack (side = "left" , padx = 10 )
400+ elif optional :
401+ tk .Label (hdr , text = "optional" , font = FONT_SMALL , bg = BG_CARD , fg = ACCENT_OPT ).pack (side = "left" , padx = 10 )
402+ elif neutral :
403+ tk .Label (hdr , text = "neutral" , font = FONT_SMALL , bg = BG_CARD , fg = ACCENT_REJ ).pack (side = "left" , padx = 10 )
332404
333405 # separator
334406 tk .Frame (card , bg = BORDER , height = 1 ).pack (fill = "x" , padx = 6 , pady = 2 )
@@ -337,19 +409,41 @@ def _build_group_card(self, sel_name, group):
337409 bins_frame = tk .Frame (card , bg = BG_CARD )
338410 bins_frame .pack (fill = "x" , padx = 6 , pady = 4 )
339411
340- for i , b in minimal :
341- self ._build_bin_row (bins_frame , sel_name , i , b , "minimal" )
342- for i , b in optional :
343- self ._build_bin_row (bins_frame , sel_name , i , b , "optional" )
344- for i , b in neutral :
345- self ._build_bin_row (bins_frame , sel_name , i , b , "neutral" )
412+ if loosest_only :
413+ # Display loosest minimal bins as informational rows — no checkbox, no bit
414+ for b in group :
415+ if b .get ("MinimalCut" , "0" ) == "1" and b .get ("OptionalCut" , "0" ) == "0" :
416+ self ._build_loosest_row (bins_frame , b )
417+ else :
418+ for i , b in minimal :
419+ self ._build_bin_row (bins_frame , sel_name , i , b , "minimal" )
420+ for i , b in optional :
421+ self ._build_bin_row (bins_frame , sel_name , i , b , "optional" )
422+ for i , b in neutral :
423+ self ._build_bin_row (bins_frame , sel_name , i , b , "neutral" )
424+
425+ def _build_loosest_row (self , parent , b ):
426+ """Informational row for a loosest/always-true cut — no checkbox, no bit."""
427+ label_text = format_value_with_comment (b )
428+
429+ row = tk .Frame (parent , bg = BG_CARD )
430+ row .pack (fill = "x" , pady = 1 )
431+
432+ tk .Label (row , text = "●" , font = FONT_BODY , bg = BG_CARD , fg = ACCENT_ALWAYS ).pack (side = "left" , padx = (0 , 4 ))
433+ tk .Label (
434+ row ,
435+ text = label_text ,
436+ font = FONT_BODY ,
437+ bg = BG_CARD ,
438+ fg = FG_DIM , # dimmed to signal non-interactive
439+ ).pack (side = "left" , fill = "x" , expand = True )
440+ # no bit-position badge since it's "X"
346441
347442 def _build_bin_row (self , parent , sel_name , idx , b , kind ):
348443 color = {"minimal" : ACCENT , "optional" : ACCENT_OPT , "neutral" : ACCENT_REJ }[kind ]
349444 label_text = format_value_with_comment (b )
350445
351446 var = tk .BooleanVar (value = False )
352-
353447 self ._vars [(sel_name , idx )] = var
354448
355449 row = tk .Frame (parent , bg = BG_CARD )
@@ -358,7 +452,7 @@ def _build_bin_row(self, parent, sel_name, idx, b, kind):
358452 # coloured dot
359453 tk .Label (row , text = "●" , font = FONT_BODY , bg = BG_CARD , fg = color ).pack (side = "left" , padx = (0 , 4 ))
360454
361- # checkbox styled as a toggle button
455+ # checkbox
362456 cb = tk .Checkbutton (
363457 row ,
364458 text = label_text ,
@@ -382,22 +476,105 @@ def _build_bin_row(self, parent, sel_name, idx, b, kind):
382476 if pos .upper () != "X" :
383477 tk .Label (row , text = f"bit { pos } " , font = FONT_SMALL , bg = BG_CARD , fg = FG_DIM , width = 8 ).pack (side = "right" , padx = 4 )
384478
385- # ── Bitmask computation ───────────────── ──────────────────────────────────
479+ # ── Bitmask computation + summary update ──────────────────────────────────
386480 def _update_bitmask (self ):
387481 bitmask = 0
482+ selected_entries = [] # list of (sel_name, label_text, bit_pos, color, is_loosest)
483+
484+ # User-selected cuts (from checkboxes)
388485 for (sel_name , idx ), var in self ._vars .items ():
389486 if not var .get ():
390487 continue
391488 b = self ._groups [sel_name ][idx ]
392489 pos = b .get ("BitPosition" , "X" )
393- if pos .upper () == "X" :
394- continue
395- bitmask |= 1 << int (pos )
490+ kind = bin_type (b )
491+ color = {"minimal" : ACCENT , "optional" : ACCENT_OPT , "neutral" : ACCENT_REJ }.get (kind , FG_DIM )
492+
493+ if pos .upper () != "X" :
494+ bitmask |= 1 << int (pos )
495+ label_text = format_value_with_comment (b )
496+ selected_entries .append ((sel_name , label_text , pos , color , False ))
497+
498+ # Loosest-only cuts: always shown in summary, never set a bit
499+ for sel_name , group in self ._groups .items ():
500+ if has_only_skipped_minimal_bins (group ):
501+ for b in group :
502+ if b .get ("MinimalCut" , "0" ) == "1" and b .get ("OptionalCut" , "0" ) == "0" :
503+ label_text = format_value_with_comment (b )
504+ selected_entries .append ((sel_name , label_text , "X" , ACCENT_ALWAYS , True ))
396505
397506 self ._lbl_dec .config (text = str (bitmask ))
398507 self ._lbl_hex .config (text = hex (bitmask ))
399508 self ._lbl_bin .config (text = bin (bitmask ))
400509
510+ self ._rebuild_summary (selected_entries )
511+
512+ def _rebuild_summary (self , entries ):
513+ """Rebuild the selected-cuts summary panel."""
514+ for w in self ._summary_inner .winfo_children ():
515+ w .destroy ()
516+
517+ if not entries :
518+ self ._lbl_summary_empty .config (text = "(none)" )
519+ self ._on_summary_inner_configure ()
520+ return
521+
522+ self ._lbl_summary_empty .config (text = "" )
523+
524+ # group by selection name, preserving is_loosest flag
525+ by_name = {}
526+ for sel_name , label_text , pos , color , is_loosest in entries :
527+ by_name .setdefault (sel_name , []).append ((label_text , pos , color , is_loosest ))
528+
529+ for sel_name , items in by_name .items ():
530+ block = tk .Frame (self ._summary_inner , bg = BG_CARD )
531+ block .pack (fill = "x" , padx = 10 , pady = (3 , 0 ))
532+
533+ # name on its own line — no truncation
534+ tk .Label (
535+ block ,
536+ text = f"{ sel_name } :" ,
537+ font = FONT_BODY ,
538+ bg = BG_CARD ,
539+ fg = FG ,
540+ anchor = "w" ,
541+ ).pack (fill = "x" )
542+
543+ # values indented below the name
544+ values_row = tk .Frame (block , bg = BG_CARD )
545+ values_row .pack (fill = "x" , padx = (16 , 0 ), pady = (0 , 2 ))
546+
547+ for label_text , pos , color , is_loosest in items :
548+ entry_frame = tk .Frame (values_row , bg = BG_CARD )
549+ entry_frame .pack (side = "left" , padx = (0 , 12 ))
550+
551+ tk .Label (entry_frame , text = "●" , font = FONT_SMALL , bg = BG_CARD , fg = color ).pack (side = "left" )
552+ tk .Label (
553+ entry_frame ,
554+ text = label_text ,
555+ font = FONT_SMALL ,
556+ bg = BG_CARD ,
557+ fg = FG_DIM if is_loosest else FG ,
558+ ).pack (side = "left" , padx = (2 , 0 ))
559+ if is_loosest :
560+ tk .Label (
561+ entry_frame ,
562+ text = "(no bit)" ,
563+ font = FONT_SMALL ,
564+ bg = BG_CARD ,
565+ fg = ACCENT_ALWAYS ,
566+ ).pack (side = "left" , padx = (4 , 0 ))
567+ elif pos .upper () != "X" :
568+ tk .Label (
569+ entry_frame ,
570+ text = f"[bit { pos } ]" ,
571+ font = FONT_SMALL ,
572+ bg = BG_CARD ,
573+ fg = FG_DIM ,
574+ ).pack (side = "left" , padx = (4 , 0 ))
575+
576+ self ._on_summary_inner_configure ()
577+
401578 # ── Utilities ─────────────────────────────────────────────────────────────
402579 def _copy (self , text ):
403580 self .clipboard_clear ()
0 commit comments