pgwidgets-python 0.3.4__py3-none-any.whl → 0.4.0__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 -2
- pgwidgets/async_/widget.py +436 -2
- pgwidgets/method_types.py +165 -8
- pgwidgets/sync/application.py +126 -2
- pgwidgets/sync/widget.py +437 -2
- pgwidgets/tree_model.py +356 -0
- {pgwidgets_python-0.3.4.dist-info → pgwidgets_python-0.4.0.dist-info}/METADATA +3 -4
- {pgwidgets_python-0.3.4.dist-info → pgwidgets_python-0.4.0.dist-info}/RECORD +11 -12
- {pgwidgets_python-0.3.4.dist-info → pgwidgets_python-0.4.0.dist-info}/WHEEL +1 -1
- pgwidgets_python-0.3.4.dist-info/scm_file_list.json +0 -71
- pgwidgets_python-0.3.4.dist-info/scm_version.json +0 -8
- {pgwidgets_python-0.3.4.dist-info → pgwidgets_python-0.4.0.dist-info}/licenses/LICENSE.md +0 -0
- {pgwidgets_python-0.3.4.dist-info → pgwidgets_python-0.4.0.dist-info}/top_level.txt +0 -0
pgwidgets/method_types.py
CHANGED
|
@@ -87,9 +87,13 @@ ACTION_METHODS = {
|
|
|
87
87
|
# Per-cell / row / column / table colour overrides — the
|
|
88
88
|
# ``set_*`` naming would otherwise classify them as SETTERs,
|
|
89
89
|
# whose single-state-slot semantics can't represent the
|
|
90
|
-
# accumulated dict of overrides we actually keep.
|
|
91
|
-
#
|
|
92
|
-
#
|
|
90
|
+
# accumulated dict of overrides we actually keep. Listed here
|
|
91
|
+
# so no SETTER is generated; _add_tree_view_methods() then
|
|
92
|
+
# supplies generators that accumulate them into the _cell_styles
|
|
93
|
+
# / _row_styles / _column_styles / _table_style dicts, which
|
|
94
|
+
# reconstruct() replays (see TREE_OVERRIDE_REPLAY). Same
|
|
95
|
+
# arrangement as expand_item / sort_by_column above.
|
|
96
|
+
"set_colors",
|
|
93
97
|
"set_cell_color", "set_row_color", "set_column_color",
|
|
94
98
|
"set_table_color",
|
|
95
99
|
"clear_cell_color", "clear_row_color", "clear_column_color",
|
|
@@ -107,8 +111,14 @@ ACTION_METHODS = {
|
|
|
107
111
|
"raise_", "lower",
|
|
108
112
|
# Timer
|
|
109
113
|
"start", "cancel", "set", "cond_set",
|
|
110
|
-
#
|
|
111
|
-
|
|
114
|
+
# Tree / table / column mutations. Listed here so no SETTER is
|
|
115
|
+
# generated (none of them sets a single state slot);
|
|
116
|
+
# _add_tree_view_methods() then supplies generators that apply each
|
|
117
|
+
# mutation to the tree / rows / columns model in _state, so the bulk
|
|
118
|
+
# state replayed on reconnect reflects every change since, not just
|
|
119
|
+
# the last wholesale set. See pgwidgets.tree_model.
|
|
120
|
+
"add_item", "remove_item", "update_tree", "remove_items", "delete_tree",
|
|
121
|
+
"update_data", "update_rows",
|
|
112
122
|
"insert_row", "append_row", "delete_row",
|
|
113
123
|
"insert_column", "append_column", "delete_column",
|
|
114
124
|
"set_cell",
|
|
@@ -232,7 +242,123 @@ CHILD_SELECT_METHODS = {
|
|
|
232
242
|
# (e.g. Splitter.set_sizes needs panes to already exist).
|
|
233
243
|
POST_CHILDREN_STATE_KEYS = {"sizes", "index", "_collapsed_paths",
|
|
234
244
|
"_expanded_paths", "_sort", "scroll_position",
|
|
235
|
-
"scroll_percent"
|
|
245
|
+
"scroll_percent",
|
|
246
|
+
# Tree/table colour overrides. Unlike the row
|
|
247
|
+
# data -- which folds into the tree/rows model
|
|
248
|
+
# and is replayed by the bulk setter -- these
|
|
249
|
+
# have no bulk setter, so they are accumulated
|
|
250
|
+
# and replayed on top of the data.
|
|
251
|
+
"_cell_styles", "_row_styles",
|
|
252
|
+
"_column_styles", "_table_style"}
|
|
253
|
+
|
|
254
|
+
# Maps a colour-override state key to the method that replays one stored
|
|
255
|
+
# entry during reconstruction. Each entry holds the original call's
|
|
256
|
+
# argument tuple, so replaying is a straight re-dispatch. _table_style
|
|
257
|
+
# holds a single tuple rather than a dict of them.
|
|
258
|
+
TREE_OVERRIDE_REPLAY = {
|
|
259
|
+
"_cell_styles": "set_cell_color",
|
|
260
|
+
"_row_styles": "set_row_color",
|
|
261
|
+
"_column_styles": "set_column_color",
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
# Colour overrides are replayed in batches rather than one call per
|
|
265
|
+
# cell: each single call is a blocking websocket round-trip *and* a full
|
|
266
|
+
# re-render in the browser, so restoring a few hundred coloured cells
|
|
267
|
+
# one at a time is slow and visibly iterative. Cells are chunked so no
|
|
268
|
+
# single message grows unreasonably large.
|
|
269
|
+
COLOUR_BATCH_SIZE = 500
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# Colour calls that can be folded into a single ``set_colors``, mapped
|
|
273
|
+
# to the spec key they contribute to and the names of their arguments.
|
|
274
|
+
_COLOUR_CALL_SPEC = {
|
|
275
|
+
"set_cell_color": ("cells", ("path", "col_key", "fg", "bg", "bold")),
|
|
276
|
+
"set_row_color": ("rows", ("path", "fg", "bg", "bold")),
|
|
277
|
+
"set_column_color": ("columns", ("col_key", "fg", "bg", "bold")),
|
|
278
|
+
"set_table_color": ("table", ("fg", "bg", "bold")),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def coalesce_colour_calls(calls):
|
|
283
|
+
"""Fold runs of colour calls in a batch into single ``set_colors``.
|
|
284
|
+
|
|
285
|
+
Each ``set_*_color`` re-renders the widget, so a few hundred of them
|
|
286
|
+
are slow in the browser even when they arrive in one message -- the
|
|
287
|
+
reason the reconnect path uses ``set_colors`` instead. Doing the
|
|
288
|
+
same for batched calls means an app can keep writing the simple
|
|
289
|
+
per-cell loop and still get one render.
|
|
290
|
+
|
|
291
|
+
Only *consecutive* calls on the same widget are merged, so ordering
|
|
292
|
+
against any interleaved non-colour call is preserved. Runs of one
|
|
293
|
+
are left alone. ``set_colors`` and the batch message arrived
|
|
294
|
+
together, so a browser that understands the batch understands the
|
|
295
|
+
merged call.
|
|
296
|
+
"""
|
|
297
|
+
out = []
|
|
298
|
+
i = 0
|
|
299
|
+
n = len(calls)
|
|
300
|
+
while i < n:
|
|
301
|
+
call = calls[i]
|
|
302
|
+
if call.get("method") not in _COLOUR_CALL_SPEC:
|
|
303
|
+
out.append(call)
|
|
304
|
+
i += 1
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
wid = call.get("wid")
|
|
308
|
+
run = []
|
|
309
|
+
while (i < n and calls[i].get("method") in _COLOUR_CALL_SPEC
|
|
310
|
+
and calls[i].get("wid") == wid):
|
|
311
|
+
run.append(calls[i])
|
|
312
|
+
i += 1
|
|
313
|
+
|
|
314
|
+
if len(run) == 1:
|
|
315
|
+
out.append(run[0])
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
spec = {}
|
|
319
|
+
for entry in run:
|
|
320
|
+
key, names = _COLOUR_CALL_SPEC[entry["method"]]
|
|
321
|
+
values = dict(zip(names, entry.get("args", [])))
|
|
322
|
+
if key == "table":
|
|
323
|
+
spec["table"] = values
|
|
324
|
+
else:
|
|
325
|
+
spec.setdefault(key, []).append(values)
|
|
326
|
+
out.append({"wid": wid, "method": "set_colors", "args": [spec]})
|
|
327
|
+
return out
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def colour_batches(widget, chunk=COLOUR_BATCH_SIZE):
|
|
331
|
+
"""Return the ``set_colors`` specs that restore a widget's colours.
|
|
332
|
+
|
|
333
|
+
The row, column and table layers ride along in the first batch; the
|
|
334
|
+
per-cell overrides are split across as many batches as needed.
|
|
335
|
+
"""
|
|
336
|
+
state = getattr(widget, "_state", None) or {}
|
|
337
|
+
cells = [dict(zip(("path", "col_key", "fg", "bg", "bold"), args))
|
|
338
|
+
for args in state.get("_cell_styles", {}).values()]
|
|
339
|
+
rows = [dict(zip(("path", "fg", "bg", "bold"), args))
|
|
340
|
+
for args in state.get("_row_styles", {}).values()]
|
|
341
|
+
columns = [dict(zip(("col_key", "fg", "bg", "bold"), args))
|
|
342
|
+
for args in state.get("_column_styles", {}).values()]
|
|
343
|
+
table = state.get("_table_style")
|
|
344
|
+
|
|
345
|
+
if not (cells or rows or columns or table):
|
|
346
|
+
return []
|
|
347
|
+
|
|
348
|
+
first = {}
|
|
349
|
+
if rows:
|
|
350
|
+
first["rows"] = rows
|
|
351
|
+
if columns:
|
|
352
|
+
first["columns"] = columns
|
|
353
|
+
if table is not None:
|
|
354
|
+
first["table"] = dict(zip(("fg", "bg", "bold"), table))
|
|
355
|
+
if cells[:chunk]:
|
|
356
|
+
first["cells"] = cells[:chunk]
|
|
357
|
+
|
|
358
|
+
batches = [first]
|
|
359
|
+
for i in range(chunk, len(cells), chunk):
|
|
360
|
+
batches.append({"cells": cells[i:i + chunk]})
|
|
361
|
+
return batches
|
|
236
362
|
|
|
237
363
|
# Default state values applied when a widget is created without
|
|
238
364
|
# explicitly setting the key (e.g. TextEntry() with no text arg).
|
|
@@ -297,8 +423,12 @@ CLEAR_RESETS = {
|
|
|
297
423
|
"TextArea": ["text"],
|
|
298
424
|
"TextSource": ["text"],
|
|
299
425
|
"ComboBox": ["text", "index", "_items"],
|
|
300
|
-
|
|
301
|
-
|
|
426
|
+
# The JS clear() empties its per-cell and per-row style maps but
|
|
427
|
+
# keeps the column / table layers, so mirror exactly that.
|
|
428
|
+
"TreeView": ["tree", "data", "_collapsed_paths", "_sort",
|
|
429
|
+
"_cell_styles", "_row_styles"],
|
|
430
|
+
"TableView": ["rows", "data", "_collapsed_paths", "_sort",
|
|
431
|
+
"_cell_styles", "_row_styles"],
|
|
302
432
|
"HtmlView": ["html"],
|
|
303
433
|
"ExternalWidget": ["content"],
|
|
304
434
|
}
|
|
@@ -419,7 +549,34 @@ BINARY_STATE_KEYS = {
|
|
|
419
549
|
}
|
|
420
550
|
|
|
421
551
|
|
|
552
|
+
def _combobox_get_index(self):
|
|
553
|
+
"""Index of the item matching the current text, or -1 when the text is
|
|
554
|
+
not one of the offerings (e.g. a value typed into an editable combo box
|
|
555
|
+
but not yet committed with Enter). Mirrors the JS get_index(), which
|
|
556
|
+
is items.indexOf(input value), and stays consistent with get_text()
|
|
557
|
+
(the cached entry text, kept live by the 'modified' callback)."""
|
|
558
|
+
items = self._state.get("_items", [])
|
|
559
|
+
try:
|
|
560
|
+
return items.index(self._state.get("text", ""))
|
|
561
|
+
except ValueError:
|
|
562
|
+
return -1
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _combobox_set_index(self, idx):
|
|
566
|
+
"""Select the item at *idx*, keeping the cached text consistent with
|
|
567
|
+
the index so get_text() agrees with get_index() after a programmatic
|
|
568
|
+
change (the generic setter would leave 'text' stale)."""
|
|
569
|
+
items = self._state.get("_items", [])
|
|
570
|
+
if 0 <= idx < len(items):
|
|
571
|
+
self._state["text"] = items[idx]
|
|
572
|
+
self._state["index"] = idx
|
|
573
|
+
self._user_set_state.add("index")
|
|
574
|
+
return self._call("set_index", idx)
|
|
575
|
+
|
|
576
|
+
|
|
422
577
|
CUSTOM_METHODS = {
|
|
578
|
+
("ComboBox", "get_index"): _combobox_get_index,
|
|
579
|
+
("ComboBox", "set_index"): _combobox_set_index,
|
|
423
580
|
("TabWidget", "index_to_widget"): _index_to_widget,
|
|
424
581
|
("TabWidget", "index_of"): _index_of,
|
|
425
582
|
("StackWidget", "index_to_widget"): _index_to_widget,
|
pgwidgets/sync/application.py
CHANGED
|
@@ -11,6 +11,7 @@ the UI can be reconstructed when a browser reconnects.
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import asyncio
|
|
14
|
+
import contextlib
|
|
14
15
|
import json
|
|
15
16
|
import logging
|
|
16
17
|
import mimetypes
|
|
@@ -33,6 +34,8 @@ from pgwidgets.method_types import (
|
|
|
33
34
|
STATE_SYNC_CALLBACKS, STATE_SYNC_REQUIRES_OPTION,
|
|
34
35
|
WIDGET_CALLBACK_SYNC, POST_CHILDREN_STATE_KEYS, ITEM_LIST_CONFIG,
|
|
35
36
|
CHILD_CLOSE_CALLBACKS, REPLAY_METHODS, TREE_VIEW_WIDGETS,
|
|
37
|
+
TREE_OVERRIDE_REPLAY, colour_batches, JS_ONLY_METHODS,
|
|
38
|
+
coalesce_colour_calls,
|
|
36
39
|
BINARY_STATE_KEYS, _send_binary_auto,
|
|
37
40
|
)
|
|
38
41
|
|
|
@@ -189,6 +192,14 @@ class Session:
|
|
|
189
192
|
|
|
190
193
|
self._reconstructing = False # suppress callbacks during reconstruction
|
|
191
194
|
|
|
195
|
+
# Batching (see batch()). While _batch_depth is non-zero, calls
|
|
196
|
+
# are buffered here instead of being sent one at a time.
|
|
197
|
+
self._batch_depth = 0
|
|
198
|
+
self._batch_calls = []
|
|
199
|
+
# cleared if a browser rejects the 'batch' message; a new
|
|
200
|
+
# connection may be a newer client, so add_connection resets it
|
|
201
|
+
self._batch_supported = True
|
|
202
|
+
|
|
192
203
|
# Browser viewport size (updated by 'viewport' messages from JS).
|
|
193
204
|
self._screen_size = (0, 0)
|
|
194
205
|
|
|
@@ -247,6 +258,9 @@ class Session:
|
|
|
247
258
|
"""Add a browser connection to this session."""
|
|
248
259
|
if ws not in self._connections:
|
|
249
260
|
self._connections.append(ws)
|
|
261
|
+
# a reconnecting browser may be a newer client than the one
|
|
262
|
+
# that made us give up on batching
|
|
263
|
+
self._batch_supported = True
|
|
250
264
|
|
|
251
265
|
def remove_connection(self, ws):
|
|
252
266
|
"""Remove a browser connection from this session."""
|
|
@@ -524,6 +538,14 @@ class Session:
|
|
|
524
538
|
widget._state[spec[0]] = tuple(args)
|
|
525
539
|
else:
|
|
526
540
|
widget._state[spec] = args[0]
|
|
541
|
+
# ComboBox: a value committed with Enter in the browser may be
|
|
542
|
+
# a new entry the browser appended to its list; mirror that
|
|
543
|
+
# append so our _items (and thus get_index) stays consistent.
|
|
544
|
+
if (widget._js_class == "ComboBox" and action == "activated"
|
|
545
|
+
and len(args) >= 2):
|
|
546
|
+
items = widget._state.setdefault("_items", [])
|
|
547
|
+
if args[1] not in items:
|
|
548
|
+
items.append(args[1])
|
|
527
549
|
# Dialog autoclose: the JS side auto-hides when a button is
|
|
528
550
|
# clicked. Sync that to Python so reconstruction doesn't
|
|
529
551
|
# re-show a dialog that was autoclosed.
|
|
@@ -540,7 +562,8 @@ class Session:
|
|
|
540
562
|
key_path = tuple(path) if isinstance(path, list) else path
|
|
541
563
|
expanded = widget._state.setdefault(
|
|
542
564
|
"_expanded_paths", set())
|
|
543
|
-
expanded
|
|
565
|
+
if expanded != "_all":
|
|
566
|
+
expanded.add(key_path)
|
|
544
567
|
collapsed = widget._state.get("_collapsed_paths")
|
|
545
568
|
if collapsed is not None and collapsed != "_all":
|
|
546
569
|
collapsed.discard(key_path)
|
|
@@ -549,7 +572,7 @@ class Session:
|
|
|
549
572
|
path = args[1]
|
|
550
573
|
key_path = tuple(path) if isinstance(path, list) else path
|
|
551
574
|
expanded = widget._state.get("_expanded_paths")
|
|
552
|
-
if expanded is not None:
|
|
575
|
+
if expanded is not None and expanded != "_all":
|
|
553
576
|
expanded.discard(key_path)
|
|
554
577
|
collapsed = widget._state.setdefault(
|
|
555
578
|
"_collapsed_paths", set())
|
|
@@ -725,11 +748,101 @@ class Session:
|
|
|
725
748
|
})
|
|
726
749
|
return wid
|
|
727
750
|
|
|
751
|
+
@contextlib.contextmanager
|
|
752
|
+
def batch(self):
|
|
753
|
+
"""Apply many updates as one message.
|
|
754
|
+
|
|
755
|
+
Every call normally goes to the browser on its own and blocks
|
|
756
|
+
for the reply, and widgets like TreeView re-render on each one.
|
|
757
|
+
For a burst of updates -- a refresh rewriting a few hundred
|
|
758
|
+
cells, say -- that is slow and paints in visible stages. Inside
|
|
759
|
+
this context the calls are buffered and sent as a single batch
|
|
760
|
+
on exit, which the browser applies with rendering suspended, so
|
|
761
|
+
each widget draws once::
|
|
762
|
+
|
|
763
|
+
with tree.get_session().batch():
|
|
764
|
+
for path, col, value in changes:
|
|
765
|
+
tree.set_cell(path, col, value)
|
|
766
|
+
|
|
767
|
+
Widget state is still updated as each call is made -- only the
|
|
768
|
+
traffic is deferred -- so the Python-side model stays correct
|
|
769
|
+
throughout, and a reconnect landing mid-batch reconstructs from
|
|
770
|
+
it correctly.
|
|
771
|
+
|
|
772
|
+
Calls made inside a batch return None. Methods that genuinely
|
|
773
|
+
need an answer from the browser (getters, and the browser-only
|
|
774
|
+
queries) flush the batch first and execute immediately, so
|
|
775
|
+
ordering is preserved.
|
|
776
|
+
|
|
777
|
+
Nests: only the outermost block flushes. The buffer is flushed
|
|
778
|
+
on the way out even if the body raises, because those calls have
|
|
779
|
+
already been applied to the model and dropping them would leave
|
|
780
|
+
the browser diverged from it.
|
|
781
|
+
"""
|
|
782
|
+
self._batch_depth += 1
|
|
783
|
+
try:
|
|
784
|
+
yield self
|
|
785
|
+
finally:
|
|
786
|
+
self._batch_depth -= 1
|
|
787
|
+
if self._batch_depth == 0:
|
|
788
|
+
self._flush_batch()
|
|
789
|
+
|
|
790
|
+
def _flush_batch(self):
|
|
791
|
+
"""Send everything buffered by batch() as one message.
|
|
792
|
+
|
|
793
|
+
A browser older than this server won't know the ``batch``
|
|
794
|
+
message -- a page that was loaded before the server was
|
|
795
|
+
upgraded, most commonly. Rather than failing the caller's
|
|
796
|
+
update, fall back to sending the calls individually and stop
|
|
797
|
+
trying to batch for this connection. Reloading the page picks
|
|
798
|
+
up the current client and re-enables it.
|
|
799
|
+
"""
|
|
800
|
+
calls, self._batch_calls = self._batch_calls, []
|
|
801
|
+
if not calls:
|
|
802
|
+
return None
|
|
803
|
+
# _send handles the no-browser case (and is the single place
|
|
804
|
+
# that policy lives)
|
|
805
|
+
# Fold runs of colour calls into set_colors: one re-render in
|
|
806
|
+
# the browser instead of one per cell.
|
|
807
|
+
calls = coalesce_colour_calls(calls)
|
|
808
|
+
if self._batch_supported:
|
|
809
|
+
self._logger.debug("flushing a batch of %d call(s)", len(calls))
|
|
810
|
+
try:
|
|
811
|
+
return self._send({"type": "batch", "calls": calls})
|
|
812
|
+
except RuntimeError as e:
|
|
813
|
+
if "Unknown message type" not in str(e):
|
|
814
|
+
raise
|
|
815
|
+
self._batch_supported = False
|
|
816
|
+
self._logger.warning(
|
|
817
|
+
"browser does not understand batched updates (it is "
|
|
818
|
+
"running an older pgwidgets-js); sending calls "
|
|
819
|
+
"individually. Reload the page to re-enable batching.")
|
|
820
|
+
for call in calls:
|
|
821
|
+
self._send({"type": "call", **call})
|
|
822
|
+
return None
|
|
823
|
+
|
|
824
|
+
def _needs_result(self, method):
|
|
825
|
+
"""True if `method` has to reach the browser now to be useful.
|
|
826
|
+
|
|
827
|
+
Getters answer from local state where they can; the ones that
|
|
828
|
+
can't (and the browser-only queries) must not be deferred, or
|
|
829
|
+
the caller gets None where it expected a value.
|
|
830
|
+
"""
|
|
831
|
+
return method.startswith("get_") or method in JS_ONLY_METHODS
|
|
832
|
+
|
|
728
833
|
def _call(self, wid, method, *args):
|
|
729
834
|
"""Call a method on a JS widget.
|
|
730
835
|
|
|
731
836
|
Returns None if no browser is connected.
|
|
732
837
|
"""
|
|
838
|
+
if self._batch_depth > 0:
|
|
839
|
+
if not self._needs_result(method):
|
|
840
|
+
self._batch_calls.append({"wid": wid, "method": method,
|
|
841
|
+
"args": list(args)})
|
|
842
|
+
return None
|
|
843
|
+
# has to go now -- flush what's queued so it stays ordered
|
|
844
|
+
self._flush_batch()
|
|
845
|
+
|
|
733
846
|
result = self._send({
|
|
734
847
|
"type": "call",
|
|
735
848
|
"wid": wid,
|
|
@@ -1524,6 +1637,13 @@ class Session:
|
|
|
1524
1637
|
list(path))
|
|
1525
1638
|
continue
|
|
1526
1639
|
|
|
1640
|
+
# Tree/table colour overrides are replayed together as
|
|
1641
|
+
# batches once the whole state has been walked (see
|
|
1642
|
+
# below) -- one round-trip and one browser re-render per
|
|
1643
|
+
# batch, rather than per coloured cell.
|
|
1644
|
+
if key in TREE_OVERRIDE_REPLAY or key == "_table_style":
|
|
1645
|
+
continue
|
|
1646
|
+
|
|
1527
1647
|
# Tree/table sort
|
|
1528
1648
|
if key == "_sort":
|
|
1529
1649
|
col, asc = value
|
|
@@ -1539,6 +1659,10 @@ class Session:
|
|
|
1539
1659
|
else:
|
|
1540
1660
|
self._call(widget._wid, method_name, value)
|
|
1541
1661
|
|
|
1662
|
+
# Colour overrides, batched
|
|
1663
|
+
for spec in colour_batches(widget):
|
|
1664
|
+
self._call(widget._wid, "set_colors", spec)
|
|
1665
|
+
|
|
1542
1666
|
# Show/hide
|
|
1543
1667
|
for key, value in widget._state.items():
|
|
1544
1668
|
if key in self._FIXED_STATE_KEYS:
|