pgwidgets-python 0.2.1__py3-none-any.whl → 0.2.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.
- pgwidgets/async_/application.py +101 -16
- pgwidgets/async_/widget.py +38 -6
- pgwidgets/method_types.py +15 -0
- pgwidgets/sync/application.py +98 -18
- pgwidgets/sync/widget.py +42 -6
- {pgwidgets_python-0.2.1.dist-info → pgwidgets_python-0.2.3.dist-info}/METADATA +1 -1
- {pgwidgets_python-0.2.1.dist-info → pgwidgets_python-0.2.3.dist-info}/RECORD +10 -10
- {pgwidgets_python-0.2.1.dist-info → pgwidgets_python-0.2.3.dist-info}/WHEEL +0 -0
- {pgwidgets_python-0.2.1.dist-info → pgwidgets_python-0.2.3.dist-info}/licenses/LICENSE.md +0 -0
- {pgwidgets_python-0.2.1.dist-info → pgwidgets_python-0.2.3.dist-info}/top_level.txt +0 -0
pgwidgets/async_/application.py
CHANGED
|
@@ -91,6 +91,13 @@ class Session:
|
|
|
91
91
|
_STATE_KEY_TO_SETTER = {v: k for k, v in SPECIAL_SETTERS.items()}
|
|
92
92
|
# e.g. {"size": "resize"}
|
|
93
93
|
|
|
94
|
+
# Reverse map: state_key -> callback action that auto-syncs it
|
|
95
|
+
# (e.g. "size" -> "resize", "position" -> "move"). Used during
|
|
96
|
+
# state replay to skip keys that weren't actively opted into via
|
|
97
|
+
# _auto_sync_actions — those were captured passively for getter
|
|
98
|
+
# support but must not be replayed (would pin layout).
|
|
99
|
+
_STATE_KEY_TO_SYNC_ACTION = {v: k for k, v in STATE_SYNC_CALLBACKS.items()}
|
|
100
|
+
|
|
94
101
|
# State keys handled by fixed-value methods (show/hide)
|
|
95
102
|
_FIXED_STATE_KEYS = {}
|
|
96
103
|
for _mname, (_key, _val) in FIXED_SETTERS.items():
|
|
@@ -300,30 +307,46 @@ class Session:
|
|
|
300
307
|
|
|
301
308
|
def _dispatch_callback(self, wid, action, *args):
|
|
302
309
|
"""Dispatch a callback through the configured concurrency mode."""
|
|
303
|
-
|
|
304
|
-
|
|
310
|
+
# Suppress callbacks during reconstruction — they are side
|
|
311
|
+
# effects of state replay, not user actions. Exception: 'map'
|
|
312
|
+
# is a one-shot lifecycle event that the JS-side observers fire
|
|
313
|
+
# when a widget first gains a visible layout box; the timing
|
|
314
|
+
# can land inside the reconstruction window, and dropping it
|
|
315
|
+
# means the user's map handler never runs until a later layout
|
|
316
|
+
# change (e.g. window resize) triggers a re-fire.
|
|
317
|
+
if self._reconstructing and action != 'map':
|
|
318
|
+
return
|
|
305
319
|
|
|
306
320
|
# Auto-sync: some callbacks carry state that should be reflected
|
|
307
321
|
# in the Python-side widget (e.g. move -> position, resize -> size).
|
|
322
|
+
# We always *capture* the value so get_size()/get_position() return
|
|
323
|
+
# current values, but only *push* it to other browsers (and replay
|
|
324
|
+
# it on reconstruction) when the widget opted in via auto-sync.
|
|
325
|
+
# Otherwise a layout-determined size would replay as a literal
|
|
326
|
+
# resize(W, H) — pinning the widget to pixel dimensions and
|
|
327
|
+
# killing flex growth.
|
|
308
328
|
state_key = STATE_SYNC_CALLBACKS.get(action)
|
|
309
329
|
if state_key is not None:
|
|
310
330
|
widget = self._widget_map.get(wid)
|
|
311
331
|
if widget is not None:
|
|
332
|
+
auto = action in widget._auto_sync_actions
|
|
312
333
|
if len(args) == 1 and isinstance(args[0], dict):
|
|
313
334
|
d = args[0]
|
|
314
335
|
if "width" in d and "height" in d:
|
|
315
336
|
new_val = (d["width"], d["height"])
|
|
316
337
|
if widget._state.get(state_key) != new_val:
|
|
317
338
|
widget._state[state_key] = new_val
|
|
318
|
-
|
|
319
|
-
|
|
339
|
+
if auto:
|
|
340
|
+
self._push(wid, "resize",
|
|
341
|
+
d["width"], d["height"])
|
|
320
342
|
else:
|
|
321
343
|
new_val = tuple(args)
|
|
322
344
|
if widget._state.get(state_key) != new_val:
|
|
323
345
|
widget._state[state_key] = new_val
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
346
|
+
if auto:
|
|
347
|
+
setter = (self._STATE_KEY_TO_SETTER.get(state_key)
|
|
348
|
+
or f"set_{state_key}")
|
|
349
|
+
self._push(wid, setter, *args)
|
|
327
350
|
# If this widget wraps a child (e.g. MDISubWindow),
|
|
328
351
|
# propagate geometry into the parent's children options
|
|
329
352
|
# so reconstruction replays with the current pos/size.
|
|
@@ -735,17 +758,37 @@ class Session:
|
|
|
735
758
|
cls = self._widget_classes.get(cls_name, Widget) if cls_name else Widget
|
|
736
759
|
widget = cls._from_existing(self, wid, cls_name or "Widget")
|
|
737
760
|
self._widget_map[wid] = widget
|
|
738
|
-
# Auto-listen for state-syncing callbacks (move, resize)
|
|
739
|
-
# so position/size changes are tracked for reconstruction.
|
|
761
|
+
# Auto-listen for state-syncing callbacks (move, resize).
|
|
740
762
|
# Register locally (sync) and send the listen message as
|
|
741
763
|
# true fire-and-forget (no result awaited) since
|
|
742
|
-
# _resolve_return is not async.
|
|
764
|
+
# _resolve_return is not async. For visual widgets we
|
|
765
|
+
# always listen for 'resize' so get_size() can return a
|
|
766
|
+
# current value, but we only mark the action as
|
|
767
|
+
# auto-syncing (which triggers push-to-peers and
|
|
768
|
+
# replay-on-reconstruction) when the widget defn opts in.
|
|
769
|
+
# Non-visual Callback-base objects (Timer, TextBufferRef,
|
|
770
|
+
# …) get nothing.
|
|
771
|
+
defn = WIDGETS.get(cls_name, {}) if cls_name else {}
|
|
772
|
+
opt_names_set = set(defn.get("options", []))
|
|
773
|
+
all_callbacks = defn.get("callbacks", [])
|
|
774
|
+
is_visual = defn.get("base") != "callback"
|
|
743
775
|
for action in STATE_SYNC_CALLBACKS:
|
|
744
|
-
|
|
745
|
-
if
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
776
|
+
req_opt = STATE_SYNC_REQUIRES_OPTION.get(action)
|
|
777
|
+
if req_opt is not None:
|
|
778
|
+
opted_in = req_opt in opt_names_set
|
|
779
|
+
else:
|
|
780
|
+
opted_in = action in all_callbacks
|
|
781
|
+
if not is_visual:
|
|
782
|
+
continue
|
|
783
|
+
if action == "resize" or opted_in:
|
|
784
|
+
key = f"{wid}:{action}"
|
|
785
|
+
if key not in self._callbacks:
|
|
786
|
+
self._callbacks[key] = [lambda wid, *a: None]
|
|
787
|
+
self._fire_and_forget_listen(wid, action)
|
|
788
|
+
if opted_in:
|
|
789
|
+
widget._auto_sync_actions.add(action)
|
|
790
|
+
elif action == "resize":
|
|
791
|
+
widget._passive_sync_actions.add(action)
|
|
749
792
|
return widget
|
|
750
793
|
if isinstance(val, list):
|
|
751
794
|
return [self._resolve_return(v) for v in val]
|
|
@@ -839,12 +882,23 @@ class Session:
|
|
|
839
882
|
await self._listen(new_widget._wid, act,
|
|
840
883
|
lambda wid, *a: None)
|
|
841
884
|
new_widget._auto_sync_actions.add(act)
|
|
842
|
-
# Replay any state the proxy accumulated (e.g. set_tooltip)
|
|
885
|
+
# Replay any state the proxy accumulated (e.g. set_tooltip).
|
|
886
|
+
# Skip passively-captured auto-sync state (size, position): we
|
|
887
|
+
# capture those from callbacks so getters work, but replaying
|
|
888
|
+
# would pin the widget to layout-determined pixel dimensions
|
|
889
|
+
# (same logic as _reconstruct_widget).
|
|
890
|
+
user_set = getattr(old_widget, "_user_set_state", set())
|
|
891
|
+
auto = getattr(old_widget, "_auto_sync_actions", set())
|
|
843
892
|
for key, value in old_widget._state.items():
|
|
844
893
|
if key.startswith("_"):
|
|
845
894
|
continue
|
|
846
895
|
if key in sync_keys:
|
|
847
896
|
continue
|
|
897
|
+
sync_action = self._STATE_KEY_TO_SYNC_ACTION.get(key)
|
|
898
|
+
if (sync_action is not None
|
|
899
|
+
and key not in user_set
|
|
900
|
+
and sync_action not in auto):
|
|
901
|
+
continue
|
|
848
902
|
method_name = (self._STATE_KEY_TO_SETTER.get(key)
|
|
849
903
|
or f"set_{key}")
|
|
850
904
|
if isinstance(value, tuple):
|
|
@@ -852,6 +906,13 @@ class Session:
|
|
|
852
906
|
else:
|
|
853
907
|
await self._call(new_widget._wid, method_name, value)
|
|
854
908
|
new_widget._state[key] = value
|
|
909
|
+
# Propagate user-set / auto-sync membership to the new
|
|
910
|
+
# widget so subsequent reconstructions replay consistently.
|
|
911
|
+
if sync_action is not None:
|
|
912
|
+
if key in user_set:
|
|
913
|
+
new_widget._user_set_state.add(key)
|
|
914
|
+
if sync_action in auto:
|
|
915
|
+
new_widget._auto_sync_actions.add(sync_action)
|
|
855
916
|
|
|
856
917
|
async def _ensure_reconstructed(self, widget):
|
|
857
918
|
"""Ensure a widget has been created on the JS side."""
|
|
@@ -1003,6 +1064,20 @@ class Session:
|
|
|
1003
1064
|
if key.startswith("_"):
|
|
1004
1065
|
continue
|
|
1005
1066
|
|
|
1067
|
+
# Skip auto-sync state (size, position) that came in
|
|
1068
|
+
# passively via a callback (e.g. layout-determined size).
|
|
1069
|
+
# We capture those so getters like get_size()/
|
|
1070
|
+
# get_position() work, but replaying them would pin the
|
|
1071
|
+
# widget to pixel dimensions and override flex/expanding
|
|
1072
|
+
# layout. Replay only if the user explicitly set the
|
|
1073
|
+
# value, or if the widget opted into the sync action
|
|
1074
|
+
# (e.g. an interactively-resizable widget).
|
|
1075
|
+
sync_action = self._STATE_KEY_TO_SYNC_ACTION.get(key)
|
|
1076
|
+
if (sync_action is not None
|
|
1077
|
+
and key not in widget._user_set_state
|
|
1078
|
+
and sync_action not in widget._auto_sync_actions):
|
|
1079
|
+
continue
|
|
1080
|
+
|
|
1006
1081
|
# Binary-payload state (e.g. set_binary_image) replays via
|
|
1007
1082
|
# _send_binary so the bytes go in a raw frame, not embedded
|
|
1008
1083
|
# as base64 in JSON.
|
|
@@ -1047,10 +1122,13 @@ class Session:
|
|
|
1047
1122
|
# _listen calls treat them as first-time registrations and
|
|
1048
1123
|
# actually send the "listen" message to the browser.
|
|
1049
1124
|
wid = widget._wid
|
|
1125
|
+
passive = getattr(widget, "_passive_sync_actions", set())
|
|
1050
1126
|
for action in list(saved_cbs.keys()):
|
|
1051
1127
|
self._callbacks.pop(f"{wid}:{action}", None)
|
|
1052
1128
|
for action in widget._auto_sync_actions:
|
|
1053
1129
|
self._callbacks.pop(f"{wid}:{action}", None)
|
|
1130
|
+
for action in passive:
|
|
1131
|
+
self._callbacks.pop(f"{wid}:{action}", None)
|
|
1054
1132
|
|
|
1055
1133
|
for action, entries in saved_cbs.items():
|
|
1056
1134
|
for handler, extra_args, extra_kwargs, style in entries:
|
|
@@ -1067,6 +1145,13 @@ class Session:
|
|
|
1067
1145
|
if action not in widget._registered_callbacks:
|
|
1068
1146
|
await self._listen(widget._wid, action,
|
|
1069
1147
|
lambda wid, *a: None)
|
|
1148
|
+
# Passive listeners (e.g. 'resize' for getter support on
|
|
1149
|
+
# widgets that didn't opt into auto-sync).
|
|
1150
|
+
for action in passive:
|
|
1151
|
+
if (action not in widget._registered_callbacks
|
|
1152
|
+
and action not in widget._auto_sync_actions):
|
|
1153
|
+
await self._listen(widget._wid, action,
|
|
1154
|
+
lambda wid, *a: None)
|
|
1070
1155
|
|
|
1071
1156
|
async def reconstruct(self):
|
|
1072
1157
|
"""Replay the entire widget tree to all connected browsers.
|
pgwidgets/async_/widget.py
CHANGED
|
@@ -79,6 +79,16 @@ class Widget:
|
|
|
79
79
|
self._constructor_options = {}
|
|
80
80
|
self._registered_callbacks = {}
|
|
81
81
|
self._auto_sync_actions = set()
|
|
82
|
+
# Actions we listen to passively for getter support
|
|
83
|
+
# (e.g. 'resize' on every visual widget so get_size() returns
|
|
84
|
+
# a current value), but that aren't in _auto_sync_actions and
|
|
85
|
+
# therefore don't push to peers or replay on reconstruction.
|
|
86
|
+
self._passive_sync_actions = set()
|
|
87
|
+
# State keys the user explicitly set via a setter call.
|
|
88
|
+
# Used during reconstruction to decide whether a state key
|
|
89
|
+
# should be replayed: passively-captured callback state
|
|
90
|
+
# (e.g. layout-determined size) is NOT in this set.
|
|
91
|
+
self._user_set_state = set()
|
|
82
92
|
self._replay_calls = []
|
|
83
93
|
self._add_seq = 0
|
|
84
94
|
|
|
@@ -196,6 +206,8 @@ class Widget:
|
|
|
196
206
|
obj._constructor_options = {}
|
|
197
207
|
obj._registered_callbacks = {}
|
|
198
208
|
obj._auto_sync_actions = set()
|
|
209
|
+
obj._passive_sync_actions = set()
|
|
210
|
+
obj._user_set_state = set()
|
|
199
211
|
obj._replay_calls = []
|
|
200
212
|
obj._stale = False
|
|
201
213
|
return obj
|
|
@@ -212,15 +224,31 @@ class Widget:
|
|
|
212
224
|
opt_names_set = set(defn.get("options", []))
|
|
213
225
|
all_callbacks = defn.get("callbacks", [])
|
|
214
226
|
|
|
215
|
-
# State-sync callbacks (move -> position, resize -> size)
|
|
227
|
+
# State-sync callbacks (move -> position, resize -> size).
|
|
228
|
+
# For visual widgets we always *listen* so getters like
|
|
229
|
+
# get_size() / get_position() can return current values.
|
|
230
|
+
# But we only add the action to _auto_sync_actions — which
|
|
231
|
+
# controls push-to-peers and replay-on-reconstruction — when
|
|
232
|
+
# the widget actually opted in (e.g. via the 'resizable' option
|
|
233
|
+
# or by declaring the callback in its defn). This keeps
|
|
234
|
+
# layout-determined sizes from being replayed as literal
|
|
235
|
+
# resize() calls that would pin flex/expanding widgets.
|
|
236
|
+
is_visual = defn.get("base") != "callback"
|
|
216
237
|
for action in STATE_SYNC_CALLBACKS:
|
|
217
238
|
req_opt = STATE_SYNC_REQUIRES_OPTION.get(action)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
239
|
+
opted_in = False
|
|
240
|
+
if req_opt is not None:
|
|
241
|
+
opted_in = req_opt in opt_names_set
|
|
242
|
+
else:
|
|
243
|
+
opted_in = action in all_callbacks
|
|
244
|
+
if not is_visual:
|
|
221
245
|
continue
|
|
222
|
-
|
|
223
|
-
|
|
246
|
+
if action == "resize" or opted_in:
|
|
247
|
+
await session._listen(wid, action, lambda wid, *a: None)
|
|
248
|
+
if opted_in:
|
|
249
|
+
self._auto_sync_actions.add(action)
|
|
250
|
+
elif action == "resize":
|
|
251
|
+
self._passive_sync_actions.add(action)
|
|
224
252
|
|
|
225
253
|
# Per-widget-class state sync (e.g. Slider "activated" -> value)
|
|
226
254
|
cls_sync = WIDGET_CALLBACK_SYNC.get(js_class, {})
|
|
@@ -461,6 +489,9 @@ def _make_setter(method_name, param_names, state_key):
|
|
|
461
489
|
self._state[state_key] = args[0]
|
|
462
490
|
else:
|
|
463
491
|
self._state[state_key] = args
|
|
492
|
+
# Mark as user-set so reconstruction knows to replay this key
|
|
493
|
+
# (callback-captured values for the same key don't get marked).
|
|
494
|
+
self._user_set_state.add(state_key)
|
|
464
495
|
return await self._call(method_name, *args)
|
|
465
496
|
method.__name__ = method_name
|
|
466
497
|
method.__qualname__ = f"Widget.{method_name}"
|
|
@@ -473,6 +504,7 @@ def _make_fixed_setter(method_name, state_key, fixed_value):
|
|
|
473
504
|
"""Create a no-arg async method that sets a fixed state value (show/hide)."""
|
|
474
505
|
async def method(self):
|
|
475
506
|
self._state[state_key] = fixed_value
|
|
507
|
+
self._user_set_state.add(state_key)
|
|
476
508
|
return await self._call(method_name)
|
|
477
509
|
method.__name__ = method_name
|
|
478
510
|
method.__qualname__ = f"Widget.{method_name}"
|
pgwidgets/method_types.py
CHANGED
|
@@ -235,6 +235,10 @@ STATE_DEFAULTS = {
|
|
|
235
235
|
"TabWidget": {"index": -1},
|
|
236
236
|
"StackWidget": {"index": -1},
|
|
237
237
|
"MDIWidget": {"index": -1},
|
|
238
|
+
# ScrollBar uses 1-arg set_scroll_percent / set_thumb_percent,
|
|
239
|
+
# so a scalar default is correct (not the (0.0, 0.0) tuple used
|
|
240
|
+
# by the 2-arg scroll widgets in STATE_KEY_DEFAULTS).
|
|
241
|
+
"ScrollBar": {"scroll_percent": 0.0, "thumb_percent": 0.0},
|
|
238
242
|
}
|
|
239
243
|
|
|
240
244
|
# Cross-widget default values for state keys, used as a fallback when
|
|
@@ -245,6 +249,17 @@ STATE_KEY_DEFAULTS = {
|
|
|
245
249
|
"size": (0, 0),
|
|
246
250
|
"position": (0, 0),
|
|
247
251
|
"index": -1,
|
|
252
|
+
# Scroll widgets that take (h_pct, v_pct). ScrollBar overrides
|
|
253
|
+
# these in STATE_DEFAULTS with scalar 0.0.
|
|
254
|
+
"scroll_position": (0.0, 0.0),
|
|
255
|
+
"scroll_percent": (0.0, 0.0),
|
|
256
|
+
"thumb_percent": (0.0, 0.0),
|
|
257
|
+
# set_expanding(horizontal, vertical) → tuple of bools
|
|
258
|
+
"expanding": (False, False),
|
|
259
|
+
"enabled": True,
|
|
260
|
+
"state": False,
|
|
261
|
+
# HTMLMediaElement convention: 0.0 (muted) to 1.0 (full).
|
|
262
|
+
"volume": 1.0,
|
|
248
263
|
}
|
|
249
264
|
|
|
250
265
|
# Widgets with incrementally-built item lists.
|
pgwidgets/sync/application.py
CHANGED
|
@@ -384,18 +384,28 @@ class Session:
|
|
|
384
384
|
|
|
385
385
|
def _dispatch_callback(self, wid, action, *args):
|
|
386
386
|
"""Dispatch a callback through the configured concurrency mode."""
|
|
387
|
-
#
|
|
388
|
-
#
|
|
389
|
-
#
|
|
390
|
-
#
|
|
391
|
-
|
|
392
|
-
|
|
387
|
+
# Suppress callbacks during reconstruction — they are side
|
|
388
|
+
# effects of state replay, not user actions. Exception: 'map'
|
|
389
|
+
# is a one-shot lifecycle event that fires when a widget first
|
|
390
|
+
# gains a visible layout box; the JS-side observers may fire
|
|
391
|
+
# it during the reconstruction window and we MUST forward it
|
|
392
|
+
# or the user's map handler never runs until something later
|
|
393
|
+
# triggers a re-fire (e.g. a window resize).
|
|
394
|
+
if self._reconstructing and action != 'map':
|
|
395
|
+
return
|
|
393
396
|
# Auto-sync: some callbacks carry state that should be reflected
|
|
394
397
|
# in the Python-side widget (e.g. move -> position, resize -> size).
|
|
398
|
+
# We always *capture* the value so get_size()/get_position() return
|
|
399
|
+
# current values, but only *push* it to other browsers (and replay
|
|
400
|
+
# it on reconstruction) when the widget opted in via auto-sync.
|
|
401
|
+
# Otherwise a layout-determined size would replay as a literal
|
|
402
|
+
# resize(W, H) — pinning the widget to pixel dimensions and
|
|
403
|
+
# killing flex growth.
|
|
395
404
|
state_key = STATE_SYNC_CALLBACKS.get(action)
|
|
396
405
|
if state_key is not None:
|
|
397
406
|
widget = self._widget_map.get(wid)
|
|
398
407
|
if widget is not None:
|
|
408
|
+
auto = action in widget._auto_sync_actions
|
|
399
409
|
# Normalize: resize sends {width, height} dict, move
|
|
400
410
|
# sends (x, y) as separate args. Store as a flat tuple
|
|
401
411
|
# matching the corresponding setter's signature.
|
|
@@ -405,15 +415,17 @@ class Session:
|
|
|
405
415
|
new_val = (d["width"], d["height"])
|
|
406
416
|
if widget._state.get(state_key) != new_val:
|
|
407
417
|
widget._state[state_key] = new_val
|
|
408
|
-
|
|
409
|
-
|
|
418
|
+
if auto:
|
|
419
|
+
self._push(wid, "resize",
|
|
420
|
+
d["width"], d["height"])
|
|
410
421
|
else:
|
|
411
422
|
new_val = tuple(args)
|
|
412
423
|
if widget._state.get(state_key) != new_val:
|
|
413
424
|
widget._state[state_key] = new_val
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
425
|
+
if auto:
|
|
426
|
+
setter = (self._STATE_KEY_TO_SETTER.get(state_key)
|
|
427
|
+
or f"set_{state_key}")
|
|
428
|
+
self._push(wid, setter, *args)
|
|
417
429
|
# If this widget wraps a child (e.g. MDISubWindow),
|
|
418
430
|
# propagate geometry into the parent's children options
|
|
419
431
|
# so reconstruction replays with the current pos/size.
|
|
@@ -804,13 +816,33 @@ class Session:
|
|
|
804
816
|
cls = self._widget_classes.get(cls_name, Widget) if cls_name else Widget
|
|
805
817
|
widget = cls._from_existing(self, wid, cls_name or "Widget")
|
|
806
818
|
self._widget_map[wid] = widget
|
|
807
|
-
# Auto-listen for state-syncing callbacks (move, resize)
|
|
808
|
-
#
|
|
819
|
+
# Auto-listen for state-syncing callbacks (move, resize).
|
|
820
|
+
# For visual widgets we always listen for 'resize' so that
|
|
821
|
+
# get_size() can return a current value, but we only mark
|
|
822
|
+
# the action as auto-syncing (which triggers push-to-peers
|
|
823
|
+
# and replay-on-reconstruction) when the widget defn
|
|
824
|
+
# actually opts in. Non-visual Callback-base objects (e.g.
|
|
825
|
+
# TextBufferRef, Timer) get nothing.
|
|
826
|
+
defn = WIDGETS.get(cls_name, {}) if cls_name else {}
|
|
827
|
+
opt_names_set = set(defn.get("options", []))
|
|
828
|
+
all_callbacks = defn.get("callbacks", [])
|
|
829
|
+
is_visual = defn.get("base") != "callback"
|
|
809
830
|
for action in STATE_SYNC_CALLBACKS:
|
|
810
|
-
|
|
811
|
-
if
|
|
812
|
-
|
|
813
|
-
|
|
831
|
+
req_opt = STATE_SYNC_REQUIRES_OPTION.get(action)
|
|
832
|
+
if req_opt is not None:
|
|
833
|
+
opted_in = req_opt in opt_names_set
|
|
834
|
+
else:
|
|
835
|
+
opted_in = action in all_callbacks
|
|
836
|
+
if not is_visual:
|
|
837
|
+
continue
|
|
838
|
+
if action == "resize" or opted_in:
|
|
839
|
+
key = f"{wid}:{action}"
|
|
840
|
+
if key not in self._callbacks:
|
|
841
|
+
self._listen(wid, action, lambda wid, *a: None)
|
|
842
|
+
if opted_in:
|
|
843
|
+
widget._auto_sync_actions.add(action)
|
|
844
|
+
elif action == "resize":
|
|
845
|
+
widget._passive_sync_actions.add(action)
|
|
814
846
|
return widget
|
|
815
847
|
if isinstance(val, list):
|
|
816
848
|
return [self._resolve_return(v) for v in val]
|
|
@@ -907,6 +939,13 @@ class Session:
|
|
|
907
939
|
_STATE_KEY_TO_SETTER = {v: k for k, v in SPECIAL_SETTERS.items()}
|
|
908
940
|
# e.g. {"size": "resize"}
|
|
909
941
|
|
|
942
|
+
# Reverse map: state_key -> callback action that auto-syncs it
|
|
943
|
+
# (e.g. "size" -> "resize", "position" -> "move"). Used during
|
|
944
|
+
# state replay to skip keys that weren't actively opted into via
|
|
945
|
+
# _auto_sync_actions — those were captured passively for getter
|
|
946
|
+
# support but must not be replayed (would pin layout).
|
|
947
|
+
_STATE_KEY_TO_SYNC_ACTION = {v: k for k, v in STATE_SYNC_CALLBACKS.items()}
|
|
948
|
+
|
|
910
949
|
# State keys handled by fixed-value methods (show/hide)
|
|
911
950
|
_FIXED_STATE_KEYS = {}
|
|
912
951
|
for _mname, (_key, _val) in FIXED_SETTERS.items():
|
|
@@ -952,12 +991,23 @@ class Session:
|
|
|
952
991
|
self._listen(new_widget._wid, act,
|
|
953
992
|
lambda wid, *a: None)
|
|
954
993
|
new_widget._auto_sync_actions.add(act)
|
|
955
|
-
# Replay any state the proxy accumulated (e.g. set_tooltip)
|
|
994
|
+
# Replay any state the proxy accumulated (e.g. set_tooltip).
|
|
995
|
+
# Skip passively-captured auto-sync state (size, position): we
|
|
996
|
+
# capture those from callbacks so getters work, but replaying
|
|
997
|
+
# would pin the widget to layout-determined pixel dimensions
|
|
998
|
+
# (same logic as _reconstruct_widget).
|
|
999
|
+
user_set = getattr(old_widget, "_user_set_state", set())
|
|
1000
|
+
auto = getattr(old_widget, "_auto_sync_actions", set())
|
|
956
1001
|
for key, value in old_widget._state.items():
|
|
957
1002
|
if key.startswith("_"):
|
|
958
1003
|
continue
|
|
959
1004
|
if key in sync_keys:
|
|
960
1005
|
continue
|
|
1006
|
+
sync_action = self._STATE_KEY_TO_SYNC_ACTION.get(key)
|
|
1007
|
+
if (sync_action is not None
|
|
1008
|
+
and key not in user_set
|
|
1009
|
+
and sync_action not in auto):
|
|
1010
|
+
continue
|
|
961
1011
|
method_name = (self._STATE_KEY_TO_SETTER.get(key)
|
|
962
1012
|
or f"set_{key}")
|
|
963
1013
|
if isinstance(value, tuple):
|
|
@@ -965,6 +1015,13 @@ class Session:
|
|
|
965
1015
|
else:
|
|
966
1016
|
self._call(new_widget._wid, method_name, value)
|
|
967
1017
|
new_widget._state[key] = value
|
|
1018
|
+
# Propagate user-set / auto-sync membership to the new
|
|
1019
|
+
# widget so subsequent reconstructions replay consistently.
|
|
1020
|
+
if sync_action is not None:
|
|
1021
|
+
if key in user_set:
|
|
1022
|
+
new_widget._user_set_state.add(key)
|
|
1023
|
+
if sync_action in auto:
|
|
1024
|
+
new_widget._auto_sync_actions.add(sync_action)
|
|
968
1025
|
|
|
969
1026
|
def _ensure_reconstructed(self, widget):
|
|
970
1027
|
"""Ensure a widget has been created on the JS side.
|
|
@@ -1147,6 +1204,20 @@ class Session:
|
|
|
1147
1204
|
if key.startswith("_"):
|
|
1148
1205
|
continue
|
|
1149
1206
|
|
|
1207
|
+
# Skip auto-sync state (size, position) that came in
|
|
1208
|
+
# passively via a callback (e.g. layout-determined size).
|
|
1209
|
+
# We capture those so getters like get_size()/
|
|
1210
|
+
# get_position() work, but replaying them would pin the
|
|
1211
|
+
# widget to pixel dimensions and override flex/expanding
|
|
1212
|
+
# layout. Replay only if the user explicitly set the
|
|
1213
|
+
# value, or if the widget opted into the sync action
|
|
1214
|
+
# (e.g. an interactively-resizable widget).
|
|
1215
|
+
sync_action = self._STATE_KEY_TO_SYNC_ACTION.get(key)
|
|
1216
|
+
if (sync_action is not None
|
|
1217
|
+
and key not in widget._user_set_state
|
|
1218
|
+
and sync_action not in widget._auto_sync_actions):
|
|
1219
|
+
continue
|
|
1220
|
+
|
|
1150
1221
|
# Binary-payload state (e.g. set_binary_image) replays via
|
|
1151
1222
|
# _send_binary so the bytes go in a raw frame, not embedded
|
|
1152
1223
|
# as base64 in JSON.
|
|
@@ -1193,10 +1264,13 @@ class Session:
|
|
|
1193
1264
|
# _listen calls treat them as first-time registrations and
|
|
1194
1265
|
# actually send the "listen" message to the browser.
|
|
1195
1266
|
wid = widget._wid
|
|
1267
|
+
passive = getattr(widget, "_passive_sync_actions", set())
|
|
1196
1268
|
for action in list(saved_cbs.keys()):
|
|
1197
1269
|
self._callbacks.pop(f"{wid}:{action}", None)
|
|
1198
1270
|
for action in widget._auto_sync_actions:
|
|
1199
1271
|
self._callbacks.pop(f"{wid}:{action}", None)
|
|
1272
|
+
for action in passive:
|
|
1273
|
+
self._callbacks.pop(f"{wid}:{action}", None)
|
|
1200
1274
|
|
|
1201
1275
|
for action, entries in saved_cbs.items():
|
|
1202
1276
|
for handler, extra_args, extra_kwargs, style in entries:
|
|
@@ -1211,6 +1285,12 @@ class Session:
|
|
|
1211
1285
|
for action in widget._auto_sync_actions:
|
|
1212
1286
|
if action not in widget._registered_callbacks:
|
|
1213
1287
|
self._listen(widget._wid, action, lambda wid, *a: None)
|
|
1288
|
+
# Passive listeners (e.g. 'resize' for getter support on
|
|
1289
|
+
# widgets that didn't opt into auto-sync).
|
|
1290
|
+
for action in passive:
|
|
1291
|
+
if (action not in widget._registered_callbacks
|
|
1292
|
+
and action not in widget._auto_sync_actions):
|
|
1293
|
+
self._listen(widget._wid, action, lambda wid, *a: None)
|
|
1214
1294
|
|
|
1215
1295
|
def _child_method_for(self, parent):
|
|
1216
1296
|
"""Determine which child method a container uses."""
|
pgwidgets/sync/widget.py
CHANGED
|
@@ -75,6 +75,16 @@ class Widget:
|
|
|
75
75
|
self._constructor_options = {}
|
|
76
76
|
self._registered_callbacks = {}
|
|
77
77
|
self._auto_sync_actions = set()
|
|
78
|
+
# Actions we listen to passively for getter support
|
|
79
|
+
# (e.g. 'resize' on every visual widget so get_size() returns
|
|
80
|
+
# a current value), but that aren't in _auto_sync_actions and
|
|
81
|
+
# therefore don't push to peers or replay on reconstruction.
|
|
82
|
+
self._passive_sync_actions = set()
|
|
83
|
+
# State keys the user explicitly set via a setter call.
|
|
84
|
+
# Used during reconstruction to decide whether a state key
|
|
85
|
+
# should be replayed: passively-captured callback state
|
|
86
|
+
# (e.g. layout-determined size) is NOT in this set.
|
|
87
|
+
self._user_set_state = set()
|
|
78
88
|
self._replay_calls = []
|
|
79
89
|
self._add_seq = 0 # insertion order across _children + _replay_calls
|
|
80
90
|
|
|
@@ -160,6 +170,8 @@ class Widget:
|
|
|
160
170
|
obj._constructor_options = {}
|
|
161
171
|
obj._registered_callbacks = {}
|
|
162
172
|
obj._auto_sync_actions = set()
|
|
173
|
+
obj._passive_sync_actions = set()
|
|
174
|
+
obj._user_set_state = set()
|
|
163
175
|
obj._replay_calls = []
|
|
164
176
|
obj._add_seq = 0
|
|
165
177
|
obj._stale = False
|
|
@@ -177,15 +189,35 @@ class Widget:
|
|
|
177
189
|
opt_names_set = set(defn.get("options", []))
|
|
178
190
|
all_callbacks = defn.get("callbacks", [])
|
|
179
191
|
|
|
180
|
-
# State-sync callbacks (move -> position, resize -> size)
|
|
192
|
+
# State-sync callbacks (move -> position, resize -> size).
|
|
193
|
+
# For visual widgets we always *listen* so getters like
|
|
194
|
+
# get_size() / get_position() can return current values.
|
|
195
|
+
# But we only add the action to _auto_sync_actions — which
|
|
196
|
+
# controls push-to-peers and replay-on-reconstruction — when
|
|
197
|
+
# the widget actually opted in (e.g. via the 'resizable' option
|
|
198
|
+
# or by declaring the callback in its defn). This keeps
|
|
199
|
+
# layout-determined sizes from being replayed as literal
|
|
200
|
+
# resize() calls that would pin flex/expanding widgets.
|
|
201
|
+
is_visual = defn.get("base") != "callback"
|
|
181
202
|
for action in STATE_SYNC_CALLBACKS:
|
|
182
203
|
req_opt = STATE_SYNC_REQUIRES_OPTION.get(action)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
204
|
+
opted_in = False
|
|
205
|
+
if req_opt is not None:
|
|
206
|
+
opted_in = req_opt in opt_names_set
|
|
207
|
+
else:
|
|
208
|
+
opted_in = action in all_callbacks
|
|
209
|
+
# Visual widgets get the listen unconditionally for resize
|
|
210
|
+
# (it's a universal base-class callback) and for any move
|
|
211
|
+
# they declare. Non-visual Callback-base objects get
|
|
212
|
+
# nothing here.
|
|
213
|
+
if not is_visual:
|
|
186
214
|
continue
|
|
187
|
-
|
|
188
|
-
|
|
215
|
+
if action == "resize" or opted_in:
|
|
216
|
+
session._listen(wid, action, lambda wid, *a: None)
|
|
217
|
+
if opted_in:
|
|
218
|
+
self._auto_sync_actions.add(action)
|
|
219
|
+
elif action == "resize":
|
|
220
|
+
self._passive_sync_actions.add(action)
|
|
189
221
|
|
|
190
222
|
# Per-widget-class state sync (e.g. Slider "activated" -> value)
|
|
191
223
|
cls_sync = WIDGET_CALLBACK_SYNC.get(js_class, {})
|
|
@@ -432,6 +464,9 @@ def _make_setter(method_name, param_names, state_key):
|
|
|
432
464
|
self._state[state_key] = args[0]
|
|
433
465
|
else:
|
|
434
466
|
self._state[state_key] = args
|
|
467
|
+
# Mark as user-set so reconstruction knows to replay this key
|
|
468
|
+
# (callback-captured values for the same key don't get marked).
|
|
469
|
+
self._user_set_state.add(state_key)
|
|
435
470
|
return self._call(method_name, *args)
|
|
436
471
|
method.__name__ = method_name
|
|
437
472
|
method.__qualname__ = f"Widget.{method_name}"
|
|
@@ -444,6 +479,7 @@ def _make_fixed_setter(method_name, state_key, fixed_value):
|
|
|
444
479
|
"""Create a no-arg method that sets a fixed state value (show/hide)."""
|
|
445
480
|
def method(self):
|
|
446
481
|
self._state[state_key] = fixed_value
|
|
482
|
+
self._user_set_state.add(state_key)
|
|
447
483
|
return self._call(method_name)
|
|
448
484
|
method.__name__ = method_name
|
|
449
485
|
method.__qualname__ = f"Widget.{method_name}"
|
|
@@ -2,19 +2,19 @@ pgwidgets/__init__.py,sha256=fKKHAt9GxLbficihVyImWsqrnQvdQZQAmeGaRUF8cBE,857
|
|
|
2
2
|
pgwidgets/_json.py,sha256=o21qywJ6yAldbqxTq3nLwgK9O67r3a8JxhvI_WAJnMY,2184
|
|
3
3
|
pgwidgets/callbacks.py,sha256=gA2FnX0N5BmbmOMyEV35yHySgAfVjfAZcZhZbo7j4-c,3314
|
|
4
4
|
pgwidgets/defs.py,sha256=Q8qhvTeansFU1Av6j5ncdwF2xSNHrpXuKErdHWQ3HiA,436
|
|
5
|
-
pgwidgets/method_types.py,sha256=
|
|
5
|
+
pgwidgets/method_types.py,sha256=S5V6EvJ9DZnSQaiaEhPkAuCtqrlHde5wj16TZd0vHvQ,16327
|
|
6
6
|
pgwidgets/async_/Widgets.py,sha256=vld2xkvusBBEVbhbezaJsjBRRj3L7c17Up0pOVs8PGo,723
|
|
7
7
|
pgwidgets/async_/__init__.py,sha256=rXB-v9XRrYt1imYuYikhkzIyiRqaYhlTpQei2LHaQ18,564
|
|
8
|
-
pgwidgets/async_/application.py,sha256=
|
|
9
|
-
pgwidgets/async_/widget.py,sha256=
|
|
8
|
+
pgwidgets/async_/application.py,sha256=mab8nRl7dNwnvNx64F0RapIJvJXy4HW_eJ1Bbuhw83w,69612
|
|
9
|
+
pgwidgets/async_/widget.py,sha256=H27v7AzHNg5obzr2e8UyOyj2lbGfWhEhZUrIDqrZNbg,37612
|
|
10
10
|
pgwidgets/extras/__init__.py,sha256=AXUmFtnn4RSlp6pJX6z55j-m_29VmfzpYIjQvAy7pO0,325
|
|
11
11
|
pgwidgets/extras/file_browser.py,sha256=PWzJltjQZbmsD1ykbbC2xJ6ErEkJj7KafhmVkd9q9Ac,16963
|
|
12
12
|
pgwidgets/sync/Widgets.py,sha256=7SaocMVMzFHhi51pjuEAr47Wez90b_a61kDbiv0jOfM,706
|
|
13
13
|
pgwidgets/sync/__init__.py,sha256=SF5RTAvtu8BbYBWzpiCPipy6DJzNXf6nqk9xdvwxUhQ,542
|
|
14
|
-
pgwidgets/sync/application.py,sha256=
|
|
15
|
-
pgwidgets/sync/widget.py,sha256=
|
|
16
|
-
pgwidgets_python-0.2.
|
|
17
|
-
pgwidgets_python-0.2.
|
|
18
|
-
pgwidgets_python-0.2.
|
|
19
|
-
pgwidgets_python-0.2.
|
|
20
|
-
pgwidgets_python-0.2.
|
|
14
|
+
pgwidgets/sync/application.py,sha256=a1Xu5VRif9WKHxMWvPN3oyTqjPsrdw54Smp4Vz16ObY,80309
|
|
15
|
+
pgwidgets/sync/widget.py,sha256=tQDkCOkcevDrGnG4LhBf69law82-UB1fF-r08GUZPSE,37913
|
|
16
|
+
pgwidgets_python-0.2.3.dist-info/licenses/LICENSE.md,sha256=LoM3fMTiMnQuHRCJghdjOtjnCrL8soBpu2PFk24Xvyg,1528
|
|
17
|
+
pgwidgets_python-0.2.3.dist-info/METADATA,sha256=e5O9EkVS7VKRrgwxNwtKgLOcs6emH7hpkFqyfJOKAUE,4568
|
|
18
|
+
pgwidgets_python-0.2.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
19
|
+
pgwidgets_python-0.2.3.dist-info/top_level.txt,sha256=wwL6fBq0gU-JwzlM6TdduY1qYpu39ysqnnbQT-1bqAs,10
|
|
20
|
+
pgwidgets_python-0.2.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|