pgwidgets-python 0.3.3__tar.gz → 0.3.5__tar.gz
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_python-0.3.3 → pgwidgets_python-0.3.5}/PKG-INFO +2 -2
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/async_/application.py +8 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/extras/file_browser.py +4 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/method_types.py +30 -4
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/sync/application.py +20 -0
- pgwidgets_python-0.3.5/pgwidgets/sync/text_source.py +200 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/sync/widget.py +5 -0
- pgwidgets_python-0.3.5/pgwidgets/text_model.py +792 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets_python.egg-info/PKG-INFO +2 -2
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets_python.egg-info/SOURCES.txt +3 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets_python.egg-info/requires.txt +1 -1
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets_python.egg-info/scm_file_list.json +3 -0
- pgwidgets_python-0.3.5/pgwidgets_python.egg-info/scm_version.json +8 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pyproject.toml +7 -5
- pgwidgets_python-0.3.5/tests/test_text_source.py +161 -0
- pgwidgets_python-0.3.3/pgwidgets_python.egg-info/scm_version.json +0 -8
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/.flake8 +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/.github/workflows/tests.yml +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/.gitignore +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/.readthedocs.yaml +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/LICENSE.md +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/README.md +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/Makefile +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/WhatsNew.rst +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/api/async.rst +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/api/index.rst +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/api/sync.rst +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/architecture.rst +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/async.rst +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/callbacks.rst +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/conf.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/extras.rst +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/getting-started.rst +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/index.rst +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/subclassing.rst +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/sync.rst +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/utilities.rst +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/web-servers.rst +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/widgets.rst +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/README.md +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/all_widgets.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/all_widgets_async.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/demo_async.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/demo_sync.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/demo_treeview.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/flask-multi-process/README.md +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/flask-multi-process/gunicorn.conf.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/flask-multi-process/nginx.conf +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/flask-multi-process/server.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/flask-multi-process/user_app.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/__init__.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/_json.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/async_/Widgets.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/async_/__init__.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/async_/widget.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/buffer.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/callbacks.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/defs.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/extras/__init__.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/icons/pgw-python-logo.png +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/sync/Widgets.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/sync/__init__.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets_python.egg-info/dependency_links.txt +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets_python.egg-info/top_level.txt +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/setup.cfg +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/test_pg2.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/__init__.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_async_session.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_defs.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_extras.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_fonts.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_protocol.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_reconstruct.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_resolve_kwargs.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_session.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_stateful.py +0 -0
- {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_widget_classes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pgwidgets-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: Python bindings for the pgwidgets JavaScript widget library
|
|
5
5
|
Author: PGWidgets Developers
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -17,7 +17,7 @@ Classifier: Topic :: Software Development :: User Interfaces
|
|
|
17
17
|
Requires-Python: >=3.12
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
License-File: LICENSE.md
|
|
20
|
-
Requires-Dist: pgwidgets-js>=0.
|
|
20
|
+
Requires-Dist: pgwidgets-js>=0.3.4
|
|
21
21
|
Requires-Dist: websockets>=12
|
|
22
22
|
Provides-Extra: dev
|
|
23
23
|
Requires-Dist: sphinx; extra == "dev"
|
|
@@ -434,6 +434,14 @@ class Session:
|
|
|
434
434
|
widget._state[spec[0]] = tuple(args)
|
|
435
435
|
else:
|
|
436
436
|
widget._state[spec] = args[0]
|
|
437
|
+
# ComboBox: a value committed with Enter in the browser may be
|
|
438
|
+
# a new entry the browser appended to its list; mirror that
|
|
439
|
+
# append so our _items (and thus get_index) stays consistent.
|
|
440
|
+
if (widget._js_class == "ComboBox" and action == "activated"
|
|
441
|
+
and len(args) >= 2):
|
|
442
|
+
items = widget._state.setdefault("_items", [])
|
|
443
|
+
if args[1] not in items:
|
|
444
|
+
items.append(args[1])
|
|
437
445
|
|
|
438
446
|
# Dialog autoclose: the JS side auto-hides when a button is
|
|
439
447
|
# clicked. Sync that to Python so reconstruction doesn't
|
|
@@ -209,6 +209,10 @@ class FileBrowser(Callbacks):
|
|
|
209
209
|
|
|
210
210
|
# ── Public API ──────────────────────────────────────────────
|
|
211
211
|
|
|
212
|
+
def set_title(self, title):
|
|
213
|
+
"""Set the dialog's title bar text."""
|
|
214
|
+
self._dialog.set_title(title)
|
|
215
|
+
|
|
212
216
|
def set_mode(self, mode):
|
|
213
217
|
"""Set the selection mode: 'file', 'files', 'directory', 'save'."""
|
|
214
218
|
self._mode = mode
|
|
@@ -62,7 +62,8 @@ JS_ONLY_METHODS = {
|
|
|
62
62
|
# MDIWidget
|
|
63
63
|
"get_subwin", "get_subwindows", "get_configuration",
|
|
64
64
|
"get_child_size", "get_child_position",
|
|
65
|
-
# Button icon
|
|
65
|
+
# Button icon: set_icon stores state (so it reconstructs); get_icon
|
|
66
|
+
# reads the live browser value, so it round-trips.
|
|
66
67
|
"get_icon",
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -107,7 +108,7 @@ ACTION_METHODS = {
|
|
|
107
108
|
# Timer
|
|
108
109
|
"start", "cancel", "set", "cond_set",
|
|
109
110
|
# Table/Tree row-level modifications (tracked via bulk set_data)
|
|
110
|
-
"add_item", "remove_item", "update_tree", "remove_items",
|
|
111
|
+
"add_item", "remove_item", "update_tree", "remove_items", "delete_tree",
|
|
111
112
|
"insert_row", "append_row", "delete_row",
|
|
112
113
|
"insert_column", "append_column", "delete_column",
|
|
113
114
|
"set_cell",
|
|
@@ -121,8 +122,6 @@ ACTION_METHODS = {
|
|
|
121
122
|
"set_minimum_size",
|
|
122
123
|
# TreeView per-column
|
|
123
124
|
"set_column_width", "set_column_editable",
|
|
124
|
-
# TextSource per-line
|
|
125
|
-
"set_icon",
|
|
126
125
|
}
|
|
127
126
|
|
|
128
127
|
# Setter methods that DON'T follow the set_* naming convention.
|
|
@@ -420,7 +419,34 @@ BINARY_STATE_KEYS = {
|
|
|
420
419
|
}
|
|
421
420
|
|
|
422
421
|
|
|
422
|
+
def _combobox_get_index(self):
|
|
423
|
+
"""Index of the item matching the current text, or -1 when the text is
|
|
424
|
+
not one of the offerings (e.g. a value typed into an editable combo box
|
|
425
|
+
but not yet committed with Enter). Mirrors the JS get_index(), which
|
|
426
|
+
is items.indexOf(input value), and stays consistent with get_text()
|
|
427
|
+
(the cached entry text, kept live by the 'modified' callback)."""
|
|
428
|
+
items = self._state.get("_items", [])
|
|
429
|
+
try:
|
|
430
|
+
return items.index(self._state.get("text", ""))
|
|
431
|
+
except ValueError:
|
|
432
|
+
return -1
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _combobox_set_index(self, idx):
|
|
436
|
+
"""Select the item at *idx*, keeping the cached text consistent with
|
|
437
|
+
the index so get_text() agrees with get_index() after a programmatic
|
|
438
|
+
change (the generic setter would leave 'text' stale)."""
|
|
439
|
+
items = self._state.get("_items", [])
|
|
440
|
+
if 0 <= idx < len(items):
|
|
441
|
+
self._state["text"] = items[idx]
|
|
442
|
+
self._state["index"] = idx
|
|
443
|
+
self._user_set_state.add("index")
|
|
444
|
+
return self._call("set_index", idx)
|
|
445
|
+
|
|
446
|
+
|
|
423
447
|
CUSTOM_METHODS = {
|
|
448
|
+
("ComboBox", "get_index"): _combobox_get_index,
|
|
449
|
+
("ComboBox", "set_index"): _combobox_set_index,
|
|
424
450
|
("TabWidget", "index_to_widget"): _index_to_widget,
|
|
425
451
|
("TabWidget", "index_of"): _index_of,
|
|
426
452
|
("StackWidget", "index_to_widget"): _index_to_widget,
|
|
@@ -524,6 +524,14 @@ class Session:
|
|
|
524
524
|
widget._state[spec[0]] = tuple(args)
|
|
525
525
|
else:
|
|
526
526
|
widget._state[spec] = args[0]
|
|
527
|
+
# ComboBox: a value committed with Enter in the browser may be
|
|
528
|
+
# a new entry the browser appended to its list; mirror that
|
|
529
|
+
# append so our _items (and thus get_index) stays consistent.
|
|
530
|
+
if (widget._js_class == "ComboBox" and action == "activated"
|
|
531
|
+
and len(args) >= 2):
|
|
532
|
+
items = widget._state.setdefault("_items", [])
|
|
533
|
+
if args[1] not in items:
|
|
534
|
+
items.append(args[1])
|
|
527
535
|
# Dialog autoclose: the JS side auto-hides when a button is
|
|
528
536
|
# clicked. Sync that to Python so reconstruction doesn't
|
|
529
537
|
# re-show a dialog that was autoclosed.
|
|
@@ -1399,6 +1407,18 @@ class Session:
|
|
|
1399
1407
|
# callbacks transferred by _transfer_proxy during step 3.
|
|
1400
1408
|
self._reregister_callbacks(widget)
|
|
1401
1409
|
|
|
1410
|
+
# 6. Widgets with a Python-authoritative model (TextSource) push
|
|
1411
|
+
# their full model state (text, tags, cursor) to the freshly
|
|
1412
|
+
# (re)connected browser. Guard it: a failure here must not abort
|
|
1413
|
+
# reconstruction of the whole widget tree.
|
|
1414
|
+
if hasattr(widget, "_reconstruct_model"):
|
|
1415
|
+
try:
|
|
1416
|
+
widget._reconstruct_model()
|
|
1417
|
+
except Exception:
|
|
1418
|
+
self._logger.error(
|
|
1419
|
+
"TextSource model reconstruction failed for wid=%s",
|
|
1420
|
+
getattr(widget, "_wid", "?"), exc_info=True)
|
|
1421
|
+
|
|
1402
1422
|
def _reregister_callbacks(self, widget):
|
|
1403
1423
|
"""Re-register user callbacks and auto-sync listeners on *widget*.
|
|
1404
1424
|
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Python-authoritative TextSource for the synchronous API.
|
|
2
|
+
|
|
3
|
+
Unlike most widgets (whose state is a handful of scalars), a text buffer
|
|
4
|
+
has rich structure -- content, live position refs, a tag table, cursor and
|
|
5
|
+
selection. We keep the *authoritative* copy of all of that on the Python
|
|
6
|
+
side in a :class:`~pgwidgets.text_model.TextModel`, so the whole API works
|
|
7
|
+
before a browser is ever connected (the UI can be built up headless and
|
|
8
|
+
replayed on connect). The browser is driven as a *view*: model changes are
|
|
9
|
+
pushed as offset-based operations through the render hooks, and are no-ops
|
|
10
|
+
(``_call`` returns None) until a browser attaches, at which point
|
|
11
|
+
``_reconstruct_model`` replays the full state.
|
|
12
|
+
|
|
13
|
+
Refs never cross the wire -- they are ordinary Python ``TextBufferRef``
|
|
14
|
+
objects owned by the local model. (The in-situ / JS-only case, where the
|
|
15
|
+
browser is authoritative, is handled by the separate pgwidgets-js/pyodide
|
|
16
|
+
wrapper, not here.)
|
|
17
|
+
|
|
18
|
+
The class subclasses the *generated* TextSource proxy so it inherits all the
|
|
19
|
+
base-widget methods (show/hide/set_tooltip/get_size/...) and the widget
|
|
20
|
+
option setters (set_editable/set_wrap/scrolling); it overrides only the
|
|
21
|
+
methods that manipulate the text model.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from pgwidgets.sync.widget import build_widget_class
|
|
25
|
+
from pgwidgets.defs import WIDGETS
|
|
26
|
+
from pgwidgets.text_model import TextModel
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_GeneratedTextSource = build_widget_class("TextSource", WIDGETS["TextSource"])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _BrowserTextModel(TextModel):
|
|
33
|
+
"""A TextModel whose render hooks push offset-based operations to the
|
|
34
|
+
browser via the owning widget. When no browser is connected the calls
|
|
35
|
+
are no-ops, so the model is fully usable headless."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, widget):
|
|
38
|
+
self._widget = widget
|
|
39
|
+
super().__init__()
|
|
40
|
+
|
|
41
|
+
def _push(self, method, *args):
|
|
42
|
+
# _call round-trips when a browser is connected and is a no-op
|
|
43
|
+
# returning None otherwise.
|
|
44
|
+
self._widget._call(method, *args)
|
|
45
|
+
|
|
46
|
+
# -- render hooks (see TextModel) --
|
|
47
|
+
def _set_editor_text(self, text):
|
|
48
|
+
self._push("set_text", text)
|
|
49
|
+
|
|
50
|
+
def _replace_editor_range(self, start, end, new_text):
|
|
51
|
+
# Drive the JS buffer by offset; pushUndo=False because the Python
|
|
52
|
+
# model owns undo/redo.
|
|
53
|
+
self._push("_replaceRange", start, end, new_text, {"pushUndo": False})
|
|
54
|
+
|
|
55
|
+
def _apply_all_formats(self, region=None):
|
|
56
|
+
# Push the current tag intervals; the JS side re-renders styling.
|
|
57
|
+
# (Tag *definitions* are pushed by TextSource.create_tag.)
|
|
58
|
+
self._push("_restoreTagIntervals", self._tag_intervals())
|
|
59
|
+
|
|
60
|
+
def _apply_selection_to_editor(self):
|
|
61
|
+
if self._sel_start == self._sel_end:
|
|
62
|
+
self._push("_setCursorOffset", self._cursor)
|
|
63
|
+
else:
|
|
64
|
+
self._push("_setSelectionOffsets", self._sel_start, self._sel_end)
|
|
65
|
+
|
|
66
|
+
def _refresh_icon_gutter(self):
|
|
67
|
+
# TODO: render ref-anchored gutter icons in the browser.
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
def _mark_clean(self):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
def _tag_intervals(self):
|
|
74
|
+
return [{"name": t["name"], "start": t["start"], "end": t["end"]}
|
|
75
|
+
for t in self._tags]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Model methods delegated verbatim to self._model. Refs in/out are the
|
|
79
|
+
# model's own TextBufferRefs; nothing here touches the wire. These override
|
|
80
|
+
# the generated round-trip proxies of the same name.
|
|
81
|
+
_DELEGATED = (
|
|
82
|
+
# content
|
|
83
|
+
"get_length", "get_text", "get_text_range", "clear", "set_text",
|
|
84
|
+
"insert_text", "delete_range",
|
|
85
|
+
# refs
|
|
86
|
+
"create_ref", "remove_ref", "create_named_ref", "get_named_ref",
|
|
87
|
+
"remove_named_ref", "get_ref_start", "get_ref_end", "get_ref_bounds",
|
|
88
|
+
"get_ref_line_start", "get_ref_line_end", "set_icon",
|
|
89
|
+
# tags (create_tag handled explicitly to also push the tag definition)
|
|
90
|
+
"remove_tag_def", "has_tag", "apply_tag", "remove_tag",
|
|
91
|
+
"get_tags_at", "get_tags_range", "get_tag_region", "get_tag_regions",
|
|
92
|
+
# find / replace
|
|
93
|
+
"find", "find_all", "replace",
|
|
94
|
+
# cursor / selection
|
|
95
|
+
"get_cursor", "set_cursor", "has_selection", "get_selection_range",
|
|
96
|
+
"get_selection_bounds", "set_selection_range",
|
|
97
|
+
# undo / redo / dirty flag
|
|
98
|
+
"can_undo", "can_redo", "undo", "redo", "get_modified", "mark_clean",
|
|
99
|
+
# misc convenience
|
|
100
|
+
"get_end_lineno", "clear_icons",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Callback actions that are model-authoritative (fire from the Python model)
|
|
104
|
+
# rather than being browser events.
|
|
105
|
+
_MODEL_CALLBACKS = {"changed", "cursor-moved", "cursor_moved"}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TextSource(_GeneratedTextSource):
|
|
109
|
+
|
|
110
|
+
def __init__(self, session, *args, **kwargs):
|
|
111
|
+
super().__init__(session, *args, **kwargs)
|
|
112
|
+
# The authoritative model. Seed its text from the initial value the
|
|
113
|
+
# generated __init__ recorded (the JS side already has it from the
|
|
114
|
+
# constructor, so no push). Text/tags/cursor are reconstructed by
|
|
115
|
+
# _reconstruct_model(), not by the generic state replay, so drop
|
|
116
|
+
# 'text' from _state.
|
|
117
|
+
self._model = _BrowserTextModel(self)
|
|
118
|
+
text = self._state.get("text")
|
|
119
|
+
self._model._text = "" if text is None else str(text)
|
|
120
|
+
self._state.pop("text", None)
|
|
121
|
+
|
|
122
|
+
def create_tag(self, name, attrs=None, **kwdargs):
|
|
123
|
+
"""Define a display tag locally and push its styling to the browser."""
|
|
124
|
+
merged = {} if attrs is None else dict(attrs)
|
|
125
|
+
merged.update(kwdargs)
|
|
126
|
+
self._model.create_tag(name, merged)
|
|
127
|
+
self._call("create_tag", name, merged)
|
|
128
|
+
|
|
129
|
+
def scroll_to_ref(self, ref):
|
|
130
|
+
"""Scroll the view to a ref, addressed by its current offset."""
|
|
131
|
+
self._call("_scrollToOffset", self._model._offset_of(ref))
|
|
132
|
+
|
|
133
|
+
def scroll_to_lineno(self, lineno):
|
|
134
|
+
"""Scroll so that line ``lineno`` is visible."""
|
|
135
|
+
self._call("_scrollToOffset",
|
|
136
|
+
self._model._offset_of_line_start(lineno))
|
|
137
|
+
|
|
138
|
+
def scroll_to_end(self):
|
|
139
|
+
"""Scroll to the end of the buffer."""
|
|
140
|
+
self._call("_scrollToOffset", self._model.get_length())
|
|
141
|
+
|
|
142
|
+
def show_tooltip(self, text, x, y):
|
|
143
|
+
"""Show a hover tooltip (``text``) near viewport point (x, y), or
|
|
144
|
+
hide it when ``text`` is empty. Normally called from a handler of
|
|
145
|
+
the 'tooltip' callback."""
|
|
146
|
+
self._call("_showTooltip", text, x, y)
|
|
147
|
+
|
|
148
|
+
# -- callbacks: 'changed'/'cursor_moved' are model-authoritative --
|
|
149
|
+
def add_callback(self, action, handler, *extra_args, **extra_kwargs):
|
|
150
|
+
if action in _MODEL_CALLBACKS:
|
|
151
|
+
self._model.add_callback(
|
|
152
|
+
self._model_cb_name(action),
|
|
153
|
+
lambda _m, *a: handler(self, *a, *extra_args, **extra_kwargs))
|
|
154
|
+
return
|
|
155
|
+
super().add_callback(action, handler, *extra_args, **extra_kwargs)
|
|
156
|
+
|
|
157
|
+
def on(self, action, handler, *extra_args, **extra_kwargs):
|
|
158
|
+
if action in _MODEL_CALLBACKS:
|
|
159
|
+
self._model.add_callback(
|
|
160
|
+
self._model_cb_name(action),
|
|
161
|
+
lambda _m, *a: handler(*a, *extra_args, **extra_kwargs))
|
|
162
|
+
return
|
|
163
|
+
super().on(action, handler, *extra_args, **extra_kwargs)
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def _model_cb_name(action):
|
|
167
|
+
return "cursor-moved" if action in ("cursor_moved",
|
|
168
|
+
"cursor-moved") else "changed"
|
|
169
|
+
|
|
170
|
+
# -- reconstruction: push the full model state to a freshly-connected
|
|
171
|
+
# browser (called by Session._reconstruct_widget after the generic
|
|
172
|
+
# create/state/callback replay) --
|
|
173
|
+
def _reconstruct_model(self):
|
|
174
|
+
m = self._model
|
|
175
|
+
self._call("set_text", m.get_text())
|
|
176
|
+
for name, attrs in m._tag_defs.items():
|
|
177
|
+
self._call("create_tag", name, attrs)
|
|
178
|
+
intervals = m._tag_intervals()
|
|
179
|
+
if intervals:
|
|
180
|
+
self._call("_restoreTagIntervals", intervals)
|
|
181
|
+
if m._sel_start != m._sel_end:
|
|
182
|
+
self._call("_setSelectionOffsets", m._sel_start, m._sel_end)
|
|
183
|
+
else:
|
|
184
|
+
self._call("_setCursorOffset", m._cursor)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _install_delegators():
|
|
188
|
+
"""Override the generated round-trip proxies with model-delegating
|
|
189
|
+
methods (defined at class scope so ``hasattr(TextSource, name)`` holds)."""
|
|
190
|
+
for _name in _DELEGATED:
|
|
191
|
+
def _make(n):
|
|
192
|
+
def method(self, *args, **kwargs):
|
|
193
|
+
return getattr(self._model, n)(*args, **kwargs)
|
|
194
|
+
method.__name__ = n
|
|
195
|
+
method.__qualname__ = "TextSource." + n
|
|
196
|
+
return method
|
|
197
|
+
setattr(TextSource, _name, _make(_name))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
_install_delegators()
|
|
@@ -965,4 +965,9 @@ def build_all_widget_classes():
|
|
|
965
965
|
classes = {}
|
|
966
966
|
for js_class, defn in WIDGETS.items():
|
|
967
967
|
classes[js_class] = build_widget_class(js_class, defn)
|
|
968
|
+
# TextSource is hand-written (Python-authoritative local text model)
|
|
969
|
+
# rather than generated -- see pgwidgets.sync.text_source. Deferred
|
|
970
|
+
# import avoids a circular import at module load.
|
|
971
|
+
from pgwidgets.sync.text_source import TextSource as _TextSource
|
|
972
|
+
classes['TextSource'] = _TextSource
|
|
968
973
|
return classes
|