bootstack 0.4.2__py3-none-any.whl → 0.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,6 @@ from typing import Any, Literal
5
5
 
6
6
  from typing_extensions import Unpack
7
7
 
8
- from bootstack.events import ChangeEvent
9
8
  from bootstack.widgets._impl.primitives.button import Button
10
9
  from bootstack.widgets._impl.composites.field import Field, FieldOptions
11
10
  from bootstack.widgets._impl.mixins import configure_delegate
@@ -62,7 +61,6 @@ class PathEntry(Field):
62
61
  self._default_extension = default_extension
63
62
  self._default_filename = default_filename
64
63
  self._dialog_result = None
65
- self._prev_value: str | None = value
66
64
 
67
65
  super().__init__(master=master, label=label, message=message, value=value, **kwargs)
68
66
 
@@ -154,15 +152,5 @@ class PathEntry(Field):
154
152
  display_text = result
155
153
 
156
154
  if result:
157
- prev_value = self._prev_value
158
- self._prev_value = display_text
159
155
  self.value = display_text
160
- self.event_generate(
161
- '<<Change>>',
162
- data=ChangeEvent(
163
- value=display_text,
164
- prev_value=prev_value,
165
- text=display_text,
166
- ),
167
- when="tail"
168
- )
156
+ self._entry._check_if_changed()
@@ -716,27 +716,29 @@ class RangeSlider(ConfigureDelegationMixin, tk.Frame):
716
716
  self._sync()
717
717
  lo = self._lo_var.get()
718
718
  hi = self._hi_var.get()
719
- self.event_generate("<<Change>>", data=RangeSliderEvent(
720
- low_value=lo,
721
- high_value=hi,
722
- prev_low_value=self._prev_lovalue,
723
- prev_high_value=self._prev_hivalue,
724
- ))
725
- self._prev_lovalue = lo
726
- self._prev_hivalue = hi
719
+ if hi != self._prev_hivalue or lo != self._prev_lovalue:
720
+ self.event_generate("<<Change>>", data=RangeSliderEvent(
721
+ low_value=lo,
722
+ high_value=hi,
723
+ prev_low_value=self._prev_lovalue,
724
+ prev_high_value=self._prev_hivalue,
725
+ ))
726
+ self._prev_lovalue = lo
727
+ self._prev_hivalue = hi
727
728
 
728
729
  def _on_hi_write(self, *_: Any) -> None:
729
730
  self._sync()
730
731
  lo = self._lo_var.get()
731
732
  hi = self._hi_var.get()
732
- self.event_generate("<<Change>>", data=RangeSliderEvent(
733
- low_value=lo,
734
- high_value=hi,
735
- prev_low_value=self._prev_lovalue,
736
- prev_high_value=self._prev_hivalue,
737
- ))
738
- self._prev_lovalue = lo
739
- self._prev_hivalue = hi
733
+ if lo != self._prev_lovalue or hi != self._prev_hivalue:
734
+ self.event_generate("<<Change>>", data=RangeSliderEvent(
735
+ low_value=lo,
736
+ high_value=hi,
737
+ prev_low_value=self._prev_lovalue,
738
+ prev_high_value=self._prev_hivalue,
739
+ ))
740
+ self._prev_lovalue = lo
741
+ self._prev_hivalue = hi
740
742
 
741
743
  def _on_map(self) -> None:
742
744
  if getattr(self, '_theme_update_pending', False):
@@ -579,8 +579,9 @@ class Slider(ConfigureDelegationMixin, tk.Frame):
579
579
  def _on_var_write(self, *_: Any) -> None:
580
580
  self._sync()
581
581
  val = self._var.get()
582
- self.event_generate("<<Change>>", data=SliderEvent(value=val, prev_value=self._prev_value))
583
- self._prev_value = val
582
+ if val != self._prev_value:
583
+ self.event_generate("<<Change>>", data=SliderEvent(value=val, prev_value=self._prev_value))
584
+ self._prev_value = val
584
585
 
585
586
  def _mouse_focus_set(self, _event: tk.Event) -> None:
586
587
  self._mouse_pressed = True
@@ -114,6 +114,9 @@ class TextArea(GridFrame):
114
114
  self._valid_signal: Signal = Signal(True)
115
115
  self._error_signal: Signal = Signal("")
116
116
 
117
+ self._prev_changed_value = value
118
+ self._prev_input_text = value
119
+
117
120
  # ── label (row 0) ─────────────────────────────────────────────────
118
121
  if label:
119
122
  _lbl_text = f"{label} *" if required else label
@@ -192,6 +195,7 @@ class TextArea(GridFrame):
192
195
  self._core._signal_text_sink = lambda text: setattr(self, "value", text)
193
196
  if textsignal is not None:
194
197
  self._core.bind_signal(textsignal)
198
+ self._prev_input_text = self.value
195
199
 
196
200
  # ── placeholder ───────────────────────────────────────────────────
197
201
  self._default_fg = self._core.text.cget("foreground")
@@ -223,6 +227,7 @@ class TextArea(GridFrame):
223
227
  # Fire <<Input>> on every edit, <<Changed>> + validation on blur.
224
228
  self._core.text.bind("<<Change>>", self._on_core_change, add="+")
225
229
  self._core.text.bind("<FocusOut>", self._on_focus_out, add="+")
230
+ self._core.text.bind("<FocusIn>", self._on_focus_in, add="+")
226
231
 
227
232
  # ── constructor callbacks ─────────────────────────────────────────
228
233
  if on_input is not None:
@@ -348,24 +353,35 @@ class TextArea(GridFrame):
348
353
 
349
354
  def _on_core_change(self, _event: tk.Event) -> None:
350
355
  if not self._showing_placeholder:
356
+ text = self.value
357
+ if text == self._prev_input_text:
358
+ return
359
+ self._prev_input_text = text
351
360
  self.event_generate(
352
361
  "<<Input>>",
353
- data=InputEvent(text=self.value),
362
+ data=InputEvent(text=text),
354
363
  when="tail",
355
364
  )
356
365
 
357
366
  def _on_focus_out(self, _event: tk.Event) -> None:
358
367
  if self._placeholder_text and not self._core.value:
359
368
  self._show_placeholder()
360
- if not self._showing_placeholder:
369
+ if not self._showing_placeholder and self.value != self._prev_changed_value:
361
370
  self.event_generate(
362
371
  "<<Changed>>",
363
- data=ChangeEvent(value=self.value, text=self.value),
372
+ data=ChangeEvent(
373
+ value=self.value,
374
+ text=self.value,
375
+ prev_value=self._prev_changed_value),
364
376
  when="tail",
365
377
  )
378
+ self._prev_changed_value = self.value
366
379
  if self._rules:
367
380
  self._run_validation(trigger="blur")
368
381
 
382
+ def _on_focus_in(self, _event: tk.Event) -> None:
383
+ self._prev_changed_value = self.value
384
+
369
385
  # ── public value API ──────────────────────────────────────────────────
370
386
 
371
387
  @property
@@ -102,9 +102,11 @@ class OptionMenu(MenuButton):
102
102
  # Store the textvariable if provided, or create a new one. This variable
103
103
  # holds the raw VALUE-KEY text (untranslated): it drives the radio
104
104
  # highlight, the public value/selection, and <<Change>> in value-space.
105
+ _initial_text = self._resolve_initial_text(value)
106
+ self._prev_value = self._value_by_text.get(_initial_text, _initial_text) if _initial_text else None
105
107
  self._textvariable = kwargs.pop('textvariable', None)
106
108
  if self._textvariable is None:
107
- self._textvariable = StringVar(value=self._resolve_initial_text(value))
109
+ self._textvariable = StringVar(value=_initial_text)
108
110
 
109
111
  # A separate variable backs the visible button face, holding the
110
112
  # (optionally translated) DISPLAY text — decoupled from the value key so
@@ -208,14 +210,17 @@ class OptionMenu(MenuButton):
208
210
 
209
211
  def _on_change(text: str) -> None:
210
212
  self._sync_display()
213
+ value = self._value_by_text.get(text, text) if text else None
211
214
  self.event_generate(
212
215
  '<<Change>>',
213
- data=ChangeEvent(value=self._value_by_text.get(text, text) if text else None),
216
+ data=ChangeEvent(
217
+ value=value,
218
+ prev_value=self._prev_value,
219
+ text=self._display_text(text)
220
+ )
214
221
  )
222
+ self._prev_value = value
215
223
 
216
- # Store it here, not at the call site (#476). A caller that discards the
217
- # return leaves an untracked subscription the guard above can never
218
- # cancel, and every one of them emits its own <<Change>>.
219
224
  self._bind_id = self.textsignal.subscribe(_on_change)
220
225
  return self._bind_id
221
226
 
@@ -140,21 +140,34 @@ class CodeEditor(PublicWidgetBase):
140
140
  internal_kwargs["extensions"] = extensions
141
141
 
142
142
  self._internal = _InternalCodeEditor(tk_master, **internal_kwargs)
143
+ self._prev_value = self._internal.value
144
+ self._prev_input_text = self._internal.value
143
145
 
144
146
  # Generate <<CursorMove>> so on_cursor_move() subscribers always work.
145
147
  t = self._internal.core.text
146
148
  t.bind("<KeyRelease>", lambda e: t.event_generate("<<CursorMove>>"), add="+")
147
149
  t.bind("<ButtonRelease-1>", lambda e: t.event_generate("<<CursorMove>>"), add="+")
148
150
 
149
- # Re-emit the core's raw <<Change>> (which carries a low-level
150
- # {"op", "index"} dict) as typed payloads on the public widget, so
151
- # on_change()/on_input() deliver the editor text — like TextArea.
152
151
  def _emit_typed_change(_e: Any = None) -> None:
153
152
  text = self._internal.value
154
- self._internal.event_generate("<<BsChange>>", data=ChangeEvent(value=text, text=text))
153
+ if text == self._prev_input_text:
154
+ return
155
+ self._prev_input_text = text
155
156
  self._internal.event_generate("<<BsInput>>", data=InputEvent(text=text))
156
157
  t.bind("<<Change>>", _emit_typed_change, add="+")
157
158
 
159
+ def _emit_typed_blur(_e: Any = None) -> None:
160
+ text = self._internal.value
161
+ if text != self._prev_value:
162
+ self._internal.event_generate("<<BsChange>>",
163
+ data=ChangeEvent(value=text, text=text, prev_value=self._prev_value))
164
+ self._prev_value = text
165
+ t.bind("<FocusOut>", _emit_typed_blur, add="+")
166
+
167
+ def _on_focus_in(_e: Any = None) -> None:
168
+ self._prev_value = self._internal.value
169
+ t.bind("<FocusIn>", _on_focus_in, add="+")
170
+
158
171
  self._attach_to_parent(layout_kw)
159
172
 
160
173
  # ----- Event routing -----
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bootstack
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Build desktop GUI apps in pure Python and ship them as a standalone executable — built on Tk, no Tkinter boilerplate.
5
5
  Author-email: Israel Dryer <israel.dryer@gmail.com>
6
6
  License-Expression: MIT
@@ -260,7 +260,7 @@ bootstack/widgets/calendar.py,sha256=zkSCufG-s429qDr2ncb1qFe-MwYb2wLWgrlfvqdwaMQ
260
260
  bootstack/widgets/card.py,sha256=wRrF64HurkT6QRUoqcX6Ath7IUlYXBLVRn1zCcZScAg,7071
261
261
  bootstack/widgets/carousel.py,sha256=Y_G1s-aavbV8bMgdEW_5ups4I5i4EF7WJg-7jvK9p8U,9453
262
262
  bootstack/widgets/chart.py,sha256=lo-faUePR2SQvUyhXJ_ZALh5vmIY4GwfXtYmDkxbijE,14195
263
- bootstack/widgets/codeeditor.py,sha256=8gjUCZnIjPzMKmgWp2FXLCQ4D1IR-9mK8cCCwh0Wuyc,26354
263
+ bootstack/widgets/codeeditor.py,sha256=oMkUMzRTxNLap_8yeEFzFDxq09FOvN2sPrg6oCUxPrg,26817
264
264
  bootstack/widgets/contextmenu.py,sha256=EnAGQQzEbOSbb9M-CK7lAzKFj81veOeR995an6kz7zk,13720
265
265
  bootstack/widgets/datatable.py,sha256=KeUo35cSjWNAdNeBtrWuuNO-dvZ7T8Ja810AENMI7E0,29828
266
266
  bootstack/widgets/datefield.py,sha256=8avae2cFfjCskNKGiO10NwzUV9PhYTrKWplMk5qQmP8,15292
@@ -351,7 +351,7 @@ bootstack/widgets/_impl/composites/meter.py,sha256=EvgUUjqkxmQLsq6VDJ6rI2VrDPfDo
351
351
  bootstack/widgets/_impl/composites/numericentry.py,sha256=krjud6vUKrcMvZViG1WGwwvcrYrVbmY7BHjIbTYfH_4,8651
352
352
  bootstack/widgets/_impl/composites/pagestack.py,sha256=SQu-6qVoje9_cBOINJuI0YJXi2fnna5lhstyIhmpbaY,15776
353
353
  bootstack/widgets/_impl/composites/passwordentry.py,sha256=qOIGDkl5MFfXwJpJEL143z18M0Zfkoca44cRHSWaAqU,6352
354
- bootstack/widgets/_impl/composites/pathentry.py,sha256=Cnd0US82sxqP8VXlOvrZRQcbxssP2yFsF2AUwwXNn2s,6270
354
+ bootstack/widgets/_impl/composites/pathentry.py,sha256=uDVQFYbQonhI7YoFLk0ycHEat6aoHSJRpVVWfhu5uLI,5852
355
355
  bootstack/widgets/_impl/composites/picture.py,sha256=vzKWvuTPXpFkmlcZrxsGFQLyf2QtLIuIZO4uKgpXl_Q,10962
356
356
  bootstack/widgets/_impl/composites/radiogroup.py,sha256=CEYhCfBgH19Y7x-63nqKdycbawm_JBFp0tQRNE7B70Q,18986
357
357
  bootstack/widgets/_impl/composites/scrolledtext.py,sha256=Dkp2ATIe-UADhvVDl6W6xEEwe-3AWkv2mESC5wP9Xik,15000
@@ -386,8 +386,8 @@ bootstack/widgets/_impl/composites/sidenav/header.py,sha256=4aAmqY98l4fx5M2URIGH
386
386
  bootstack/widgets/_impl/composites/sidenav/separator.py,sha256=QEDZjXSmYD65-soOpDmx3RQ-UBF5Jb3P_lmTZyy9jDM,1376
387
387
  bootstack/widgets/_impl/composites/slider/__init__.py,sha256=G4LcfzXCxD78j24D7iOXg72BPQM5FSh7l89tI2dckAA,190
388
388
  bootstack/widgets/_impl/composites/slider/_shared.py,sha256=xbwghVe-mKno5gDJsQNXqK8M13rPZrY4qh3kAz7SRvM,7615
389
- bootstack/widgets/_impl/composites/slider/rangeslider.py,sha256=xhzv68hhSx2iImtPdXBzuk9p41tdOJxzdl8APnGAnhY,43770
390
- bootstack/widgets/_impl/composites/slider/slider.py,sha256=OWdhzahCDsXP1xNtDoHb82iQMbtYyzR895CePBbEQP0,38996
389
+ bootstack/widgets/_impl/composites/slider/rangeslider.py,sha256=KLWKGyo7khUXrkfz-tYuiGZuJIKUiNQBISXL-6Dbzpk,43966
390
+ bootstack/widgets/_impl/composites/slider/slider.py,sha256=Ertqs1qrucPv46lugXXwNcQb607phCqiJPBgr49i1BY,39041
391
391
  bootstack/widgets/_impl/composites/tableview/__init__.py,sha256=ccQKtGH53tGj7K1ezfsbY2iEyIH1wRgd1qEQ0Glqra4,127
392
392
  bootstack/widgets/_impl/composites/tableview/tableview.py,sha256=QuhguaAvMCi9k3tiyyha8WkFlEgtQUPjj3Gk4rc5HDc,151228
393
393
  bootstack/widgets/_impl/composites/tableview/types.py,sha256=oDmJco19M42Zv_lUEpsfeM7kjjuHHsNBMVZoWRe-QDs,4906
@@ -406,7 +406,7 @@ bootstack/widgets/_impl/composites/textarea/redirector.py,sha256=vAL8Y3HuevC8Fab
406
406
  bootstack/widgets/_impl/composites/textarea/search_overlay.py,sha256=humVYBFN-EbfbIFC6IavtsNEFGHJtuYZCZ5W7SmZUeU,18669
407
407
  bootstack/widgets/_impl/composites/textarea/sidebar.py,sha256=X3LcIv-OMvymL3xTcNMEhP5ddzh53gxreDWmeMItXg8,3151
408
408
  bootstack/widgets/_impl/composites/textarea/style_registry.py,sha256=NOgBOGLJzYvw-Mf6O0YV7kwIoUc_yvOOWlqZDDzC6Qc,7678
409
- bootstack/widgets/_impl/composites/textarea/textarea.py,sha256=Dk3LGmJUH8Nc5dv5-0x77-X7t8S_IT8RNhJ5dpMgEgo,25586
409
+ bootstack/widgets/_impl/composites/textarea/textarea.py,sha256=PZpKe0rxNoHNOvU-4Fs_7h29jrJdK8QbV7qVvp9ayiw,26221
410
410
  bootstack/widgets/_impl/composites/textarea/undo.py,sha256=gTkQ0UEh9F5BivjdbN96AfBqLxK348S2rCvri6sb0Pk,8622
411
411
  bootstack/widgets/_impl/composites/textarea/extensions/__init__.py,sha256=jQRM97jFIw-9rdnkHuY8tXMijeZiuYmqBVhX8TLG1pg,48
412
412
  bootstack/widgets/_impl/composites/textarea/extensions/bracket_matcher.py,sha256=70lGUz6AWTKnBcsGmhJ1PZghzSq-aYdtITQGZjITJPY,5054
@@ -441,7 +441,7 @@ bootstack/widgets/_impl/primitives/frame.py,sha256=Q2FWhgGkBU74YgYnKr9TFlkbrf_qX
441
441
  bootstack/widgets/_impl/primitives/gridframe.py,sha256=hr74jDwX9pdw0LquQNANHFkpyubw7vmyX8vUhXI7VfY,21629
442
442
  bootstack/widgets/_impl/primitives/label.py,sha256=2XxnObg6xmbLIkefCMYkb9hwyZIuU8FJclvcMpnszW0,3495
443
443
  bootstack/widgets/_impl/primitives/labelframe.py,sha256=IB5fqGgwG44vDrszFRBLEqrJGW5IXX1veK5AhetPGOE,2013
444
- bootstack/widgets/_impl/primitives/optionmenu.py,sha256=9fPutiWHs9kmn-FKmbXCCMkfG7yAdYEfQSsbi218INU,16543
444
+ bootstack/widgets/_impl/primitives/optionmenu.py,sha256=cH15ls1msZx74FzWIj3mZdBwBCJ3uYf0iwVbA3mesfQ,16671
445
445
  bootstack/widgets/_impl/primitives/packframe.py,sha256=maUgSNlnJFoLr6ErpRMoO4922uJMPrxkfzTkc2jc-tg,8754
446
446
  bootstack/widgets/_impl/primitives/panedwindow.py,sha256=kSuXphwDDwITTenmHVMLwevBg5czbeJjJzRHUO504KM,1697
447
447
  bootstack/widgets/_impl/primitives/progressbar.py,sha256=Mmj-AUmfQEB671OzDvWxezowcBliZdQkuwWaCHBedmU,3129
@@ -453,10 +453,10 @@ bootstack/widgets/_impl/primitives/sizegrip.py,sha256=uwQRzOEHjNPXtUURhT6k2TNYdy
453
453
  bootstack/widgets/_impl/primitives/spinbox.py,sha256=5m0tZrqCw745nvIiBrJWnZ0q8fZ7bNEvopjROMbQolc,4526
454
454
  bootstack/widgets/_impl/primitives/switch.py,sha256=7dt43uE2P_WdkxZZsGtCFRAQ20nI2jx5VUqGSrWwEmA,2543
455
455
  bootstack/widgets/_impl/primitives/treeview.py,sha256=vnSowgzmtNEWst_i9ZgcacHjdvuheF9xmeTvTWE1bLg,2709
456
- bootstack-0.4.2.dist-info/licenses/LICENSE,sha256=CUeTPUOM8Qc0yuqneJUwuUmDBooET5UfsHjMrkdBeCg,1097
457
- bootstack-0.4.2.dist-info/licenses/NOTICE,sha256=c62TcygUKbXx1V3FEgziroDDry_NwmycqKEg_CRnYBU,1503
458
- bootstack-0.4.2.dist-info/METADATA,sha256=rjQd8aCKIj2jphR1hvz0tYgnjVgE4HgBzyra8_rSArE,16107
459
- bootstack-0.4.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
460
- bootstack-0.4.2.dist-info/entry_points.txt,sha256=phOlfRy3Th86IYlNhQiUrTLLLzxie1IG-kMas_5iMdA,49
461
- bootstack-0.4.2.dist-info/top_level.txt,sha256=IsKBAac2vRCegvRFSCzwcBr7TpHp4bLnLFgA75LOkXs,10
462
- bootstack-0.4.2.dist-info/RECORD,,
456
+ bootstack-0.4.3.dist-info/licenses/LICENSE,sha256=CUeTPUOM8Qc0yuqneJUwuUmDBooET5UfsHjMrkdBeCg,1097
457
+ bootstack-0.4.3.dist-info/licenses/NOTICE,sha256=c62TcygUKbXx1V3FEgziroDDry_NwmycqKEg_CRnYBU,1503
458
+ bootstack-0.4.3.dist-info/METADATA,sha256=JKJN3UE_7DG1uuWeGPU1ysx3Lhlh7g-PcabMP4cGXHg,16107
459
+ bootstack-0.4.3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
460
+ bootstack-0.4.3.dist-info/entry_points.txt,sha256=phOlfRy3Th86IYlNhQiUrTLLLzxie1IG-kMas_5iMdA,49
461
+ bootstack-0.4.3.dist-info/top_level.txt,sha256=IsKBAac2vRCegvRFSCzwcBr7TpHp4bLnLFgA75LOkXs,10
462
+ bootstack-0.4.3.dist-info/RECORD,,