11"""ipywidgets-based session orchestrating the DrawCanvas. Internal."""
2+
23from __future__ import annotations
34
45import base64
@@ -56,8 +57,7 @@ def _fmt_banner(msg: str, kind: BannerKind = "info") -> str:
5657def _validate (sdata : sd .SpatialData , coordinate_system : str , element : str ) -> None :
5758 if coordinate_system not in sdata .coordinate_systems :
5859 raise ValueError (
59- f"Unknown coordinate system { coordinate_system !r} . "
60- f"Available: { list (sdata .coordinate_systems )} "
60+ f"Unknown coordinate system { coordinate_system !r} . Available: { list (sdata .coordinate_systems )} "
6161 )
6262 if element not in sdata .images :
6363 raise ValueError (f"Unknown image element { element !r} . Available: { list (sdata .images )} " )
@@ -79,79 +79,129 @@ def __init__(
7979 element : str ,
8080 * ,
8181 persist : bool = True ,
82+ max_width : int = 880 ,
8283 ) -> None :
8384 _validate (sdata , coordinate_system , element )
8485
8586 self ._sdata = sdata
8687 self ._cs = coordinate_system
8788 self ._element = element
8889 self ._persist_enabled = persist
90+ self ._max_width = max_width
8991 self .canvas : DrawCanvas | None = None
9092 self ._extent : RenderExtent | None = None
9193 self ._commits : list [str ] = []
9294
9395 self ._style = W .HTML (value = _CSS )
9496
97+ icon_btn_layout = W .Layout (width = "36px" )
9598 self .tool_tb = W .ToggleButtons (
9699 options = [("Rect" , "rectangle" ), ("Polygon" , "polygon" ), ("Lasso" , "lasso" )],
97100 value = "rectangle" ,
98- description = "Tool:" ,
101+ description = "" ,
102+ tooltips = ["R" , "P" , "L" ],
99103 )
100104 self .tool_tb .observe (self ._on_tool_change , names = "value" )
101105 self .close_poly_btn = self ._trigger_btn (
102- "Close polygon" , "check" , "close_poly_trigger" , tooltip = "Enter" , disabled = True ,
106+ "" ,
107+ "check" ,
108+ "close_poly_trigger" ,
109+ tooltip = "Close polygon (Enter)" ,
110+ disabled = True ,
111+ layout = icon_btn_layout ,
103112 )
104113 self .undo_btn = self ._trigger_btn (
105- "Undo" , "rotate-left" , "undo_trigger" , tooltip = "Ctrl+Z" , disabled = True ,
114+ "" ,
115+ "rotate-left" ,
116+ "undo_trigger" ,
117+ tooltip = "Undo (Ctrl+Z)" ,
118+ disabled = True ,
119+ layout = icon_btn_layout ,
106120 )
107121 self .clear_btn = self ._trigger_btn (
108- "Clear" , "trash" , "clear_trigger" , tooltip = "Delete" ,
122+ "" ,
123+ "trash" ,
124+ "clear_trigger" ,
125+ tooltip = "Clear canvas" ,
109126 after = lambda : self ._set_banner ("Canvas cleared." , "info" ),
127+ layout = icon_btn_layout ,
128+ )
129+ self .fit_btn = self ._trigger_btn (
130+ "" ,
131+ "compress" ,
132+ "fit_trigger" ,
133+ tooltip = "Fit view (F)" ,
134+ layout = icon_btn_layout ,
110135 )
111- self .fit_btn = self ._trigger_btn ("Fit view" , "compress" , "fit_trigger" , tooltip = "F" )
112- self .shape_count_lbl = W .Label (value = "0 shape(s) on canvas" )
113136
114- self .name_tx = W .Text (value = "" , placeholder = "name…" , description = "Name:" )
115- self .save_btn = W .Button (description = "Save" , button_style = "success" , icon = "save" )
137+ self .name_tx = W .Text (
138+ value = "" ,
139+ placeholder = "name…" ,
140+ layout = W .Layout (flex = "1 1 140px" , min_width = "100px" ),
141+ )
142+ self .save_btn = W .Button (
143+ description = "Save" ,
144+ button_style = "success" ,
145+ icon = "save" ,
146+ tooltip = "Save shapes to sdata.shapes[name]" ,
147+ )
116148 self .save_btn .on_click (self ._on_save )
117149
118150 save_row_widgets : list [W .Widget ] = [self .name_tx , self .save_btn ]
119151 self .persist_btn : W .Button | None = None
120152 if persist :
121153 self .persist_btn = W .Button (
122- description = "Write to disk" , button_style = "warning" , icon = "hdd-o" ,
154+ description = "" ,
155+ icon = "hdd-o" ,
156+ button_style = "warning" ,
157+ tooltip = "Write last save to disk" ,
158+ layout = icon_btn_layout ,
123159 )
124160 self .persist_btn .on_click (self ._on_persist )
125161 self .persist_btn .disabled = True
126162 save_row_widgets .append (self .persist_btn )
127163
128- self .banner = W .HTML (value = _fmt_banner (
129- f"Annotating <b>{ element !r} </b> in coordinate system <b>{ coordinate_system !r} </b>. "
130- "Pick a tool and draw. Click canvas first so keyboard shortcuts work. "
131- "<b>R/P/L</b> tools · <b>Wheel</b> zoom · <b>Shift+drag</b> pan · "
132- "<b>Alt+click</b> shape to delete · <b>Ctrl+Z</b> undo · <b>F</b> fit" ,
133- "hint" ,
134- ))
164+ self .banner = W .HTML (
165+ value = _fmt_banner (
166+ f"Annotating <b>{ element !r} </b> in coordinate system <b>{ coordinate_system !r} </b>. "
167+ "Pick a tool and draw. Click canvas first so keyboard shortcuts work. "
168+ "<b>R/P/L</b> tools · <b>Wheel</b> zoom · <b>Shift+drag</b> pan · "
169+ "<b>Alt+click</b> shape to delete · <b>Ctrl+Z</b> undo · <b>F</b> fit" ,
170+ "hint" ,
171+ )
172+ )
135173 self .plot_box = W .VBox ([])
136174
137- def section (label : str ) -> W .HTML :
138- return W .HTML (value = f"<div class='sdp-section-title'>{ label } </div>" )
139-
140- controls_card = W .VBox ([
141- W .HTML (value = (
142- f"<div class='sdp-title'>Annotate</div>"
143- f"<div class='sdp-context'>{ element !r} · { coordinate_system !r} </div>"
144- )),
145- section ("Draw" ),
146- W .HBox ([self .tool_tb , self .close_poly_btn , self .undo_btn , self .clear_btn , self .fit_btn ]),
147- W .HBox ([self .shape_count_lbl ]),
148- section ("Save" ),
149- W .HBox (save_row_widgets ),
150- self .banner ,
151- ])
175+ row_layout = W .Layout (
176+ display = "flex" ,
177+ flex_flow = "row wrap" ,
178+ align_items = "center" ,
179+ gap = "6px" ,
180+ )
181+ card_layout = W .Layout (max_width = f"{ max_width } px" , width = "100%" )
182+ toolbar = W .Box (
183+ children = [self .tool_tb , self .close_poly_btn , self .undo_btn , self .clear_btn , self .fit_btn ],
184+ layout = row_layout ,
185+ )
186+ save_row = W .Box (children = save_row_widgets , layout = row_layout )
187+
188+ controls_card = W .VBox (
189+ [
190+ W .HTML (
191+ value = (
192+ f"<div class='sdp-title'>Annotate</div>"
193+ f"<div class='sdp-context'>{ element !r} · { coordinate_system !r} </div>"
194+ )
195+ ),
196+ toolbar ,
197+ save_row ,
198+ self .banner ,
199+ ],
200+ layout = card_layout ,
201+ )
152202 controls_card .add_class ("sdp-card" )
153203
154- canvas_card = W .VBox ([self .plot_box ])
204+ canvas_card = W .VBox ([self .plot_box ], layout = card_layout )
155205 canvas_card .add_class ("sdp-card" )
156206
157207 self .controls = W .VBox ([self ._style , controls_card , canvas_card ])
@@ -172,9 +222,12 @@ def _trigger_btn(
172222 tooltip : str = "" ,
173223 disabled : bool = False ,
174224 after : Any = None ,
225+ layout : W .Layout | None = None ,
175226 ) -> W .Button :
176227 btn = W .Button (description = description , icon = icon , tooltip = tooltip )
177228 btn .disabled = disabled
229+ if layout is not None :
230+ btn .layout = layout
178231
179232 def _on_click (_b : W .Button ) -> None :
180233 if self .canvas is None :
@@ -195,15 +248,14 @@ def _render(self) -> None:
195248 image_url = data_url ,
196249 image_width = extent .image_w ,
197250 image_height = extent .image_h ,
251+ max_display_width = self ._max_width ,
198252 tool = self .tool_tb .value ,
199253 )
200254 self .canvas .observe (self ._on_shapes_change , names = "shapes" )
201255 self .plot_box .children = (self .canvas ,)
202- self .shape_count_lbl .value = "0 shape(s) on canvas"
203256
204257 def _on_shapes_change (self , change : dict [str , Any ]) -> None :
205258 shapes = change ["new" ] or []
206- self .shape_count_lbl .value = f"{ len (shapes )} shape(s) on canvas"
207259 self .undo_btn .disabled = len (shapes ) == 0
208260
209261 def _on_tool_change (self , change : dict [str , Any ]) -> None :
@@ -214,24 +266,24 @@ def _on_tool_change(self, change: dict[str, Any]) -> None:
214266 self ._set_banner (f"Tool: <b>{ change ['new' ]} </b>" , "info" )
215267
216268 def _collect_polygons (self ) -> list [Polygon ]:
217- assert self .canvas is not None and self ._extent is not None
269+ assert self .canvas is not None
270+ assert self ._extent is not None
218271 polys : list [Polygon ] = []
219272 for sh in self .canvas .shapes :
220273 p = pixel_shape_to_polygon (sh , self ._extent )
221274 if p is not None :
222275 polys .append (p )
223276 return polys
224277
225- def _commit_polygons (self , polys : list [Polygon ], name : str ) -> tuple [ str , bool ] :
278+ def _commit_polygons (self , polys : list [Polygon ], name : str ) -> str :
226279 shapes_model = build_shapes_model (polys , self ._cs )
227280 target = commit_to_memory (self ._sdata , shapes_model , name )
228281 self ._commits .append (target )
229- return target , target != name
282+ return target
230283
231284 def _reset_canvas_state (self ) -> None :
232285 assert self .canvas is not None
233286 self .canvas .clear_trigger += 1
234- self .shape_count_lbl .value = "0 shape(s) on canvas"
235287
236288 def _on_save (self , _btn : W .Button ) -> None :
237289 name = self .name_tx .value .strip ()
@@ -250,13 +302,10 @@ def _on_save(self, _btn: W.Button) -> None:
250302 )
251303 return
252304
253- target , renamed = self ._commit_polygons (polys , name )
305+ target = self ._commit_polygons (polys , name )
254306 self ._reset_canvas_state ()
255307
256- msg = f"Saved <b>{ target !r} </b> with { len (polys )} polygon(s)."
257- if renamed :
258- msg += " (name collided; renamed)"
259- self ._set_banner (msg , "success" )
308+ self ._set_banner (f"Saved <b>{ target !r} </b> with { len (polys )} polygon(s)." , "success" )
260309 if self .persist_btn is not None :
261310 self .persist_btn .disabled = self ._sdata .path is None
262311
0 commit comments