pgwidgets-python 0.1.3__tar.gz → 0.2.0__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.2.0/.github/workflows/tests.yml +28 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/PKG-INFO +35 -1
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/README.md +34 -0
- pgwidgets_python-0.2.0/docs/WhatsNew.rst +162 -0
- pgwidgets_python-0.2.0/docs/architecture.rst +180 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/async.rst +27 -2
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/callbacks.rst +10 -4
- pgwidgets_python-0.2.0/docs/extras.rst +273 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/getting-started.rst +12 -6
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/index.rst +4 -0
- pgwidgets_python-0.2.0/docs/subclassing.rst +242 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/sync.rst +52 -4
- pgwidgets_python-0.2.0/docs/utilities.rst +40 -0
- pgwidgets_python-0.2.0/docs/widgets.rst +808 -0
- pgwidgets_python-0.2.0/examples/all_widgets.py +712 -0
- pgwidgets_python-0.2.0/examples/all_widgets_async.py +782 -0
- pgwidgets_python-0.2.0/examples/demo_treeview.py +98 -0
- pgwidgets_python-0.2.0/pgwidgets/async_/Widgets.py +22 -0
- pgwidgets_python-0.2.0/pgwidgets/async_/application.py +1633 -0
- pgwidgets_python-0.2.0/pgwidgets/async_/widget.py +935 -0
- pgwidgets_python-0.2.0/pgwidgets/callbacks.py +95 -0
- pgwidgets_python-0.2.0/pgwidgets/extras/__init__.py +11 -0
- pgwidgets_python-0.2.0/pgwidgets/extras/file_browser.py +479 -0
- pgwidgets_python-0.2.0/pgwidgets/method_types.py +447 -0
- pgwidgets_python-0.2.0/pgwidgets/sync/Widgets.py +22 -0
- pgwidgets_python-0.2.0/pgwidgets/sync/application.py +1879 -0
- pgwidgets_python-0.2.0/pgwidgets/sync/widget.py +919 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets_python.egg-info/PKG-INFO +35 -1
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets_python.egg-info/SOURCES.txt +18 -0
- pgwidgets_python-0.2.0/test_pg2.py +45 -0
- pgwidgets_python-0.2.0/tests/test_extras.py +53 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/tests/test_protocol.py +11 -9
- pgwidgets_python-0.2.0/tests/test_reconstruct.py +513 -0
- pgwidgets_python-0.2.0/tests/test_session.py +377 -0
- pgwidgets_python-0.2.0/tests/test_stateful.py +454 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/tests/test_widget_classes.py +2 -1
- pgwidgets_python-0.1.3/docs/architecture.rst +0 -94
- pgwidgets_python-0.1.3/docs/widgets.rst +0 -579
- pgwidgets_python-0.1.3/examples/demo_treeview.py +0 -94
- pgwidgets_python-0.1.3/pgwidgets/async_/application.py +0 -660
- pgwidgets_python-0.1.3/pgwidgets/async_/widget.py +0 -171
- pgwidgets_python-0.1.3/pgwidgets/sync/application.py +0 -796
- pgwidgets_python-0.1.3/pgwidgets/sync/widget.py +0 -166
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/.flake8 +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/.gitignore +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/.readthedocs.yaml +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/LICENSE.md +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/Makefile +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/api/async.rst +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/api/index.rst +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/api/sync.rst +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/conf.py +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/web-servers.rst +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/examples/README.md +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/examples/demo_async.py +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/examples/demo_sync.py +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets/__init__.py +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets/async_/__init__.py +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets/defs.py +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets/sync/__init__.py +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets_python.egg-info/dependency_links.txt +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets_python.egg-info/requires.txt +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets_python.egg-info/top_level.txt +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pyproject.toml +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/setup.cfg +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/tests/__init__.py +0 -0
- {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/tests/test_defs.py +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install package with test dependencies
|
|
25
|
+
run: pip install -e ".[test]"
|
|
26
|
+
|
|
27
|
+
- name: Run tests
|
|
28
|
+
run: pytest tests/ -v
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pgwidgets-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python bindings for the pgwidgets JavaScript widget library
|
|
5
5
|
Author: PGWidgets Developers
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -33,6 +33,11 @@ Python bindings for the [pgwidgets](https://github.com/naojsoft/pgwidgets-js)
|
|
|
33
33
|
JavaScript widget library. Build desktop-style browser UIs from Python
|
|
34
34
|
with a familiar Qt/GTK-style API.
|
|
35
35
|
|
|
36
|
+
## Documentation
|
|
37
|
+
|
|
38
|
+
Full documentation is available at
|
|
39
|
+
[pgwidgets-python.readthedocs.io](https://pgwidgets-python.readthedocs.io/en/latest/).
|
|
40
|
+
|
|
36
41
|
## Installation
|
|
37
42
|
|
|
38
43
|
```bash
|
|
@@ -116,6 +121,35 @@ back over WebSocket. Python widget constructors and method calls are
|
|
|
116
121
|
translated to JSON messages and executed in the browser. Callbacks are
|
|
117
122
|
forwarded back to Python.
|
|
118
123
|
|
|
124
|
+
## Sessions and Reconnection
|
|
125
|
+
|
|
126
|
+
Sessions persist independently of browser connections. When a browser
|
|
127
|
+
disconnects (page refresh, network drop, tab close), the session and its
|
|
128
|
+
widget tree remain alive on the Python side. When the browser reconnects,
|
|
129
|
+
the entire UI is automatically reconstructed.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
app = Application(max_sessions=4, logger=logger)
|
|
133
|
+
|
|
134
|
+
@app.on_connect
|
|
135
|
+
def setup(session):
|
|
136
|
+
Widgets = session.get_widgets()
|
|
137
|
+
# Build your UI...
|
|
138
|
+
# If the browser refreshes, this UI is reconstructed automatically.
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Key features:**
|
|
142
|
+
|
|
143
|
+
- **Automatic reconstruction** -- refresh the browser and the UI reappears
|
|
144
|
+
in its current state (widget positions, text, slider values, etc.).
|
|
145
|
+
- **Multi-browser support** -- open the same session URL in a second browser
|
|
146
|
+
tab or window. Both browsers show the same UI and stay synchronized.
|
|
147
|
+
Widget state changes (slider moves, tab switches, tree expand/collapse)
|
|
148
|
+
are pushed to all connected browsers in real time.
|
|
149
|
+
- **Headless sessions** -- create sessions without a browser using
|
|
150
|
+
`app.create_session()`, build the widget tree, then connect a browser
|
|
151
|
+
later to see the pre-built UI.
|
|
152
|
+
|
|
119
153
|
## License
|
|
120
154
|
|
|
121
155
|
BSD 3-Clause
|
|
@@ -4,6 +4,11 @@ Python bindings for the [pgwidgets](https://github.com/naojsoft/pgwidgets-js)
|
|
|
4
4
|
JavaScript widget library. Build desktop-style browser UIs from Python
|
|
5
5
|
with a familiar Qt/GTK-style API.
|
|
6
6
|
|
|
7
|
+
## Documentation
|
|
8
|
+
|
|
9
|
+
Full documentation is available at
|
|
10
|
+
[pgwidgets-python.readthedocs.io](https://pgwidgets-python.readthedocs.io/en/latest/).
|
|
11
|
+
|
|
7
12
|
## Installation
|
|
8
13
|
|
|
9
14
|
```bash
|
|
@@ -87,6 +92,35 @@ back over WebSocket. Python widget constructors and method calls are
|
|
|
87
92
|
translated to JSON messages and executed in the browser. Callbacks are
|
|
88
93
|
forwarded back to Python.
|
|
89
94
|
|
|
95
|
+
## Sessions and Reconnection
|
|
96
|
+
|
|
97
|
+
Sessions persist independently of browser connections. When a browser
|
|
98
|
+
disconnects (page refresh, network drop, tab close), the session and its
|
|
99
|
+
widget tree remain alive on the Python side. When the browser reconnects,
|
|
100
|
+
the entire UI is automatically reconstructed.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
app = Application(max_sessions=4, logger=logger)
|
|
104
|
+
|
|
105
|
+
@app.on_connect
|
|
106
|
+
def setup(session):
|
|
107
|
+
Widgets = session.get_widgets()
|
|
108
|
+
# Build your UI...
|
|
109
|
+
# If the browser refreshes, this UI is reconstructed automatically.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Key features:**
|
|
113
|
+
|
|
114
|
+
- **Automatic reconstruction** -- refresh the browser and the UI reappears
|
|
115
|
+
in its current state (widget positions, text, slider values, etc.).
|
|
116
|
+
- **Multi-browser support** -- open the same session URL in a second browser
|
|
117
|
+
tab or window. Both browsers show the same UI and stay synchronized.
|
|
118
|
+
Widget state changes (slider moves, tab switches, tree expand/collapse)
|
|
119
|
+
are pushed to all connected browsers in real time.
|
|
120
|
+
- **Headless sessions** -- create sessions without a browser using
|
|
121
|
+
`app.create_session()`, build the widget tree, then connect a browser
|
|
122
|
+
later to see the pre-built UI.
|
|
123
|
+
|
|
90
124
|
## License
|
|
91
125
|
|
|
92
126
|
BSD 3-Clause
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
What's New
|
|
2
|
+
==========
|
|
3
|
+
|
|
4
|
+
Significant changes since the last tagged release (``v0.1.3``).
|
|
5
|
+
|
|
6
|
+
Major changes
|
|
7
|
+
-------------
|
|
8
|
+
|
|
9
|
+
TreeView / TableView: dict-tree model
|
|
10
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
11
|
+
|
|
12
|
+
Mirroring the JS-side rewrite, ``TreeView`` and ``TableView`` now
|
|
13
|
+
work with hierarchies of dicts keyed by stable string identifiers.
|
|
14
|
+
Paths are arrays of those keys and stay valid no matter how the
|
|
15
|
+
visible tree is sorted.
|
|
16
|
+
|
|
17
|
+
.. code-block:: python
|
|
18
|
+
|
|
19
|
+
tree = W.TreeView(columns=[
|
|
20
|
+
{"label": "Name", "key": "NAME", "type": "string"},
|
|
21
|
+
{"label": "Type", "key": "TYPE", "type": "string"},
|
|
22
|
+
{"label": "Size", "key": "SIZE", "type": "integer"},
|
|
23
|
+
], sortable=True)
|
|
24
|
+
|
|
25
|
+
tree.set_tree({
|
|
26
|
+
"Documents": {
|
|
27
|
+
"report.pdf": {"TYPE": "PDF", "SIZE": 2400},
|
|
28
|
+
"notes.txt": {"TYPE": "Text", "SIZE": 12},
|
|
29
|
+
},
|
|
30
|
+
"Pictures": {
|
|
31
|
+
"photo.jpg": {"TYPE": "JPEG", "SIZE": 3200},
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
Highlights:
|
|
36
|
+
|
|
37
|
+
- New column-key-based API: ``set_column_width(col_key)``,
|
|
38
|
+
``sort_by_column(col_key, ascending)``,
|
|
39
|
+
``insert_column(column, before=None)``,
|
|
40
|
+
``delete_column(col_key)``,
|
|
41
|
+
``set_cell(path, col_key, value)``,
|
|
42
|
+
``set_column_editable(col_key, tf)``.
|
|
43
|
+
- New column types: ``"string"`` / ``"integer"`` / ``"float"`` /
|
|
44
|
+
``"boolean"`` (renders ✓ when truthy) / ``"icon"``.
|
|
45
|
+
``halign`` field with sensible per-type defaults.
|
|
46
|
+
- New tree methods:
|
|
47
|
+
|
|
48
|
+
- ``add_tree(tree, parent=None)`` -- merge a dict-tree under a
|
|
49
|
+
parent path (preserves selection by path).
|
|
50
|
+
- ``update_tree(tree)`` -- replace the tree, preserve selection.
|
|
51
|
+
- ``get_subtree(status='all')`` -- return a connected subset
|
|
52
|
+
(selected / expanded / collapsed nodes plus their descendants
|
|
53
|
+
and ancestors), round-trippable through ``set_tree``.
|
|
54
|
+
- ``clear_selection()`` -- explicit no-arg reset.
|
|
55
|
+
|
|
56
|
+
- Auto-spanning: a row whose value for a column is missing causes
|
|
57
|
+
the previous present cell to extend across it. Lets parent rows
|
|
58
|
+
be terse: ``{"NAME": "Documents"}`` (with the rest of the columns
|
|
59
|
+
omitted) renders as a single cell across the row.
|
|
60
|
+
- ``TableView.set_data`` accepts a list of dicts (preferred) or a
|
|
61
|
+
list of arrays.
|
|
62
|
+
|
|
63
|
+
See :doc:`widgets` for the full reference.
|
|
64
|
+
|
|
65
|
+
Window controls (TopLevel) and shade (MDISubWindow)
|
|
66
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
67
|
+
|
|
68
|
+
``TopLevel`` gains the same window controls that ``MDISubWindow``
|
|
69
|
+
has, plus a "shade" (roll up to title bar) state on both.
|
|
70
|
+
|
|
71
|
+
New ``TopLevel`` options (default ``False`` except ``shadeable``
|
|
72
|
+
which defaults ``True``):
|
|
73
|
+
|
|
74
|
+
- ``minimizable`` -- show minimize button. Minimized windows
|
|
75
|
+
auto-stack along the bottom of the viewport.
|
|
76
|
+
- ``maximizable`` -- show maximize button. Fills the browser
|
|
77
|
+
viewport (snapshot at click time).
|
|
78
|
+
- ``lowerable`` -- show send-to-back button.
|
|
79
|
+
- ``shadeable`` -- collapse to title bar in place. Available from
|
|
80
|
+
the right-click context menu and via double-click on the title
|
|
81
|
+
bar.
|
|
82
|
+
- ``icon`` -- title-bar icon (URL or ``data:`` URI).
|
|
83
|
+
|
|
84
|
+
New methods: ``set_icon(url)``, ``toggle_minimize()``,
|
|
85
|
+
``toggle_maximize()``, ``toggle_shade()``,
|
|
86
|
+
``set_window_state(state)``, ``get_window_state()``.
|
|
87
|
+
|
|
88
|
+
New callback ``window-state`` is auto-tracked, so the
|
|
89
|
+
minimize/maximize/shade state survives a browser reconnect.
|
|
90
|
+
|
|
91
|
+
``MDIWidget.add_widget`` accepts ``shadeable`` (default ``True``).
|
|
92
|
+
|
|
93
|
+
Right-click on the title bar of either widget opens a context menu
|
|
94
|
+
with the applicable actions (Raise, Lower, Shade, Minimize,
|
|
95
|
+
Maximize, Close). The menu supports both click-release and
|
|
96
|
+
press-drag-release, like a menubar.
|
|
97
|
+
|
|
98
|
+
Image: binary-frame protocol
|
|
99
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
100
|
+
|
|
101
|
+
New method ``Image.set_binary_image(data, format='jpeg')`` sends raw
|
|
102
|
+
bytes (``bytes`` / ``bytearray`` / ``memoryview``) via a WebSocket
|
|
103
|
+
binary frame, avoiding the ~33% base64 overhead of ``set_image``.
|
|
104
|
+
Useful for animation/streaming. ``format`` is one of ``"jpeg"``,
|
|
105
|
+
``"png"``, ``"webp"``, ``"gif"``. The latest frame is stored in
|
|
106
|
+
widget state and replayed on reconnect.
|
|
107
|
+
|
|
108
|
+
Callbacks base class
|
|
109
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
110
|
+
|
|
111
|
+
New module ``pgwidgets.callbacks`` exposes ``Callbacks``, a small
|
|
112
|
+
base class that provides the same callback API (``add_callback``,
|
|
113
|
+
``on``, ``make_callback``, ``enable_callback``, ...) as a real
|
|
114
|
+
``Widget`` without the widget machinery. Use it for Python-side
|
|
115
|
+
composite/utility classes that need to expose handler registration.
|
|
116
|
+
|
|
117
|
+
``FileBrowser`` (in ``pgwidgets.extras``) is now a subclass and so
|
|
118
|
+
supports both ``add_callback("activated", ...)`` and
|
|
119
|
+
``on("activated", ...)`` directly.
|
|
120
|
+
|
|
121
|
+
See :ref:`callbacks-base`.
|
|
122
|
+
|
|
123
|
+
Robustness improvements
|
|
124
|
+
-----------------------
|
|
125
|
+
|
|
126
|
+
- ``Session._send`` and ``_send_binary`` no longer hang when the
|
|
127
|
+
asyncio loop refuses a coroutine (loop closed mid-call,
|
|
128
|
+
``RecursionError`` from a deep callback chain, etc.). The
|
|
129
|
+
schedule failure is logged, the orphan coroutine is closed, and
|
|
130
|
+
``_send`` returns ``None`` so the caller continues.
|
|
131
|
+
- ``json.dumps`` errors in ``_send`` / ``_push`` are caught and
|
|
132
|
+
logged instead of crashing reconstruction with a non-JSON-
|
|
133
|
+
serialisable widget state.
|
|
134
|
+
- After every ``create``, ``Session._next_wid`` is advanced past
|
|
135
|
+
the JS-side ``next_wid`` so subsequent Python allocations skip
|
|
136
|
+
any auto-allocated sub-widget IDs (matching the JS-side
|
|
137
|
+
collision rescue). This fixes "callback fires on the wrong
|
|
138
|
+
widget" cases where a widget like ``TreeView`` allocates
|
|
139
|
+
internal ``ScrollBar`` widgets in its constructor.
|
|
140
|
+
|
|
141
|
+
Other notable changes
|
|
142
|
+
---------------------
|
|
143
|
+
|
|
144
|
+
- ``ColorDialog`` now exposes ``popup``, ``set_position``,
|
|
145
|
+
``set_modal``, and the ``move`` / ``close`` callbacks (inherited
|
|
146
|
+
from ``Dialog`` on the JS side). Its ``popup`` is wired through
|
|
147
|
+
the ``_dialog_popup`` custom method so visibility/position state
|
|
148
|
+
is tracked for reconstruction.
|
|
149
|
+
- ``MenuAction`` ``activated`` callback signature simplified. Old:
|
|
150
|
+
``handler(widget, text, checked)``. New: ``handler(widget)`` for
|
|
151
|
+
non-checkable actions; ``handler(widget, checked)`` for checkable
|
|
152
|
+
ones. *This is a breaking change for any handler that took the
|
|
153
|
+
text arg.*
|
|
154
|
+
- ``Widget`` base class now exposes
|
|
155
|
+
``set_allow_text_selection(tf)``; browser text selection is off
|
|
156
|
+
by default for most widgets (form controls and the cell editor
|
|
157
|
+
in ``TreeView`` always allow selection).
|
|
158
|
+
- ``clear()`` on ``TreeView`` / ``TableView`` no longer resets the
|
|
159
|
+
``columns`` state (it was popping ``_state["columns"]`` even
|
|
160
|
+
though the JS side preserves columns on clear).
|
|
161
|
+
- ``FileBrowser`` migrated to the new dict-tree ``TableView`` API
|
|
162
|
+
internally and now subclasses ``Callbacks``.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Architecture
|
|
2
|
+
============
|
|
3
|
+
|
|
4
|
+
Overview
|
|
5
|
+
--------
|
|
6
|
+
|
|
7
|
+
pgwidgets follows a client-server architecture. Python is the server; the
|
|
8
|
+
browser is the client. Widget constructors and method calls in Python are
|
|
9
|
+
translated to JSON messages and sent over WebSocket to the browser, where the
|
|
10
|
+
pgwidgets JavaScript library executes them. User interactions (clicks, input,
|
|
11
|
+
etc.) travel back as callback messages.
|
|
12
|
+
|
|
13
|
+
::
|
|
14
|
+
|
|
15
|
+
Python (server) Browser (client)
|
|
16
|
+
+-----------------+ +------------------+
|
|
17
|
+
| Application | WebSocket | pgwidgets JS |
|
|
18
|
+
| Session <----|----JSON------->| widget tree |
|
|
19
|
+
| widgets | | DOM rendering |
|
|
20
|
+
+-----------------+ +------------------+
|
|
21
|
+
|
|
|
22
|
+
| HTTP (static files)
|
|
23
|
+
v
|
|
24
|
+
JS/CSS assets served to browser
|
|
25
|
+
|
|
26
|
+
Servers
|
|
27
|
+
-------
|
|
28
|
+
|
|
29
|
+
The ``Application`` class starts two servers:
|
|
30
|
+
|
|
31
|
+
**HTTP server** (default port 9501)
|
|
32
|
+
Serves the pgwidgets JavaScript/CSS assets and a connector HTML page.
|
|
33
|
+
When a browser hits ``/``, it gets ``remote.html`` with the WebSocket URL
|
|
34
|
+
injected. Set ``http_server=False`` if you serve the static files from
|
|
35
|
+
your own web server (Flask, FastAPI, nginx, etc.).
|
|
36
|
+
|
|
37
|
+
**WebSocket server** (default port 9500)
|
|
38
|
+
Carries the JSON command protocol. Each browser tab opens one WebSocket
|
|
39
|
+
connection, which becomes one ``Session``.
|
|
40
|
+
|
|
41
|
+
JSON Protocol
|
|
42
|
+
-------------
|
|
43
|
+
|
|
44
|
+
All messages are JSON objects with a ``type`` field.
|
|
45
|
+
|
|
46
|
+
**Python -> Browser:**
|
|
47
|
+
|
|
48
|
+
- ``{"type": "init", "id": 0}`` -- handshake initiation.
|
|
49
|
+
- ``{"type": "session-info", "session_id": 1, "token": "..."}`` --
|
|
50
|
+
session credentials for reconnection.
|
|
51
|
+
- ``{"type": "create", "wid": 1, "class": "Button", "args": ["Click"]}`` --
|
|
52
|
+
create a widget.
|
|
53
|
+
- ``{"type": "call", "wid": 1, "method": "set_text", "args": ["New"]}`` --
|
|
54
|
+
call a method on a widget.
|
|
55
|
+
- ``{"type": "call", ..., "silent": true}`` -- call a method without
|
|
56
|
+
triggering callbacks (used for cross-browser sync).
|
|
57
|
+
- ``{"type": "listen", "wid": 1, "action": "activated"}`` -- subscribe to a
|
|
58
|
+
callback.
|
|
59
|
+
- ``{"type": "unlisten", "wid": 1, "action": "activated"}`` -- unsubscribe.
|
|
60
|
+
- ``{"type": "reconstruct-start", "next_wid": N}`` -- begin UI reconstruction.
|
|
61
|
+
- ``{"type": "reconstruct-end"}`` -- end UI reconstruction.
|
|
62
|
+
|
|
63
|
+
**Browser -> Python:**
|
|
64
|
+
|
|
65
|
+
- ``{"type": "ack", "session_id": 1, "token": "..."}`` -- handshake
|
|
66
|
+
acknowledgment (includes session credentials when reconnecting).
|
|
67
|
+
- ``{"type": "result", "id": 1, "value": ...}`` -- method return value.
|
|
68
|
+
- ``{"type": "error", "id": 1, "error": "..."}`` -- method error.
|
|
69
|
+
- ``{"type": "callback", "wid": 1, "action": "activated", "args": [...]}`` --
|
|
70
|
+
user interaction.
|
|
71
|
+
- ``{"type": "file-chunk", ...}`` -- chunked file data (see :doc:`callbacks`).
|
|
72
|
+
|
|
73
|
+
Session Model
|
|
74
|
+
-------------
|
|
75
|
+
|
|
76
|
+
Each browser connection gets a ``Session`` object. Sessions persist
|
|
77
|
+
independently of browser connections -- they survive page refreshes,
|
|
78
|
+
network drops, and tab closes. Python is the source of truth for all
|
|
79
|
+
widget state.
|
|
80
|
+
|
|
81
|
+
A session owns:
|
|
82
|
+
|
|
83
|
+
- A widget tree with full state tracking (text, colors, sizes, children, etc.)
|
|
84
|
+
- A callback registry (``"wid:action"`` -> handler function)
|
|
85
|
+
- A list of active browser connections
|
|
86
|
+
- A security token for reconnection
|
|
87
|
+
- A message-ID counter for request/response matching
|
|
88
|
+
|
|
89
|
+
Multiple sessions can be active concurrently (controlled by ``max_sessions``).
|
|
90
|
+
|
|
91
|
+
Lifecycle:
|
|
92
|
+
|
|
93
|
+
1. Browser opens the URL and loads ``remote.html``.
|
|
94
|
+
2. JavaScript connects to the WebSocket server.
|
|
95
|
+
3. Python sends ``init``; browser acknowledges with session info (if reconnecting).
|
|
96
|
+
4. For a **new** connection: ``on_connect`` fires with a new ``Session``.
|
|
97
|
+
5. For a **reconnection**: the existing session's UI is automatically
|
|
98
|
+
reconstructed in the browser.
|
|
99
|
+
6. User code creates widgets, registers callbacks.
|
|
100
|
+
7. When a browser disconnects, ``on_disconnect`` fires. The session
|
|
101
|
+
remains alive for reconnection.
|
|
102
|
+
8. Sessions are only destroyed when ``session.destroy()`` is called explicitly.
|
|
103
|
+
|
|
104
|
+
Reconnection and Reconstruction
|
|
105
|
+
--------------------------------
|
|
106
|
+
|
|
107
|
+
When a browser reconnects to an existing session (e.g. after a page refresh),
|
|
108
|
+
the framework walks the widget tree and replays every widget's creation,
|
|
109
|
+
state, children, and callbacks. The browser receives the full UI as if it
|
|
110
|
+
were being built for the first time.
|
|
111
|
+
|
|
112
|
+
The reconstruction process:
|
|
113
|
+
|
|
114
|
+
1. Clean up stale auto-wrapped widget references from the previous connection.
|
|
115
|
+
2. Send ``reconstruct-start`` so the browser suppresses callback echo.
|
|
116
|
+
3. For each widget (parents before children):
|
|
117
|
+
|
|
118
|
+
a. Create the widget with its original constructor arguments.
|
|
119
|
+
b. Replay item lists (e.g. ComboBox items).
|
|
120
|
+
c. Replay state changes (text, colors, size, position, etc.).
|
|
121
|
+
d. Attach to parent via the same child method used originally.
|
|
122
|
+
e. Replay factory calls (menu actions, toolbar actions, separators).
|
|
123
|
+
f. Re-register all callbacks.
|
|
124
|
+
g. Re-register auto-sync listeners.
|
|
125
|
+
|
|
126
|
+
4. Replay deferred state (splitter sizes, tab/stack index, tree collapse state).
|
|
127
|
+
5. Replay show/hide state.
|
|
128
|
+
6. Send ``reconstruct-end``.
|
|
129
|
+
|
|
130
|
+
Multi-Browser Synchronization
|
|
131
|
+
-----------------------------
|
|
132
|
+
|
|
133
|
+
Multiple browsers can connect to the same session simultaneously. When one
|
|
134
|
+
browser triggers a state change (slider move, tab switch, tree expand, etc.),
|
|
135
|
+
the change is pushed to all other connected browsers in real time.
|
|
136
|
+
|
|
137
|
+
::
|
|
138
|
+
|
|
139
|
+
Browser A Python (session) Browser B
|
|
140
|
+
+---------+ +---------------+ +---------+
|
|
141
|
+
| slider |---callback-->| update state |---push------>| slider |
|
|
142
|
+
| moved | | in _state | (silent) | updated |
|
|
143
|
+
+---------+ +---------------+ +---------+
|
|
144
|
+
|
|
145
|
+
The push uses a ``silent`` flag so the receiving browser suppresses callback
|
|
146
|
+
echo, preventing infinite feedback loops.
|
|
147
|
+
|
|
148
|
+
State changes that are synchronized include:
|
|
149
|
+
|
|
150
|
+
- Widget state: slider values, checkbox state, text content, tab index, etc.
|
|
151
|
+
- Layout: move and resize of windows and MDI subwindows.
|
|
152
|
+
- Tree/table: expand, collapse, and sort operations.
|
|
153
|
+
- Child management: closing MDI subwindows or tab pages.
|
|
154
|
+
|
|
155
|
+
Headless Sessions
|
|
156
|
+
-----------------
|
|
157
|
+
|
|
158
|
+
Sessions can be created without a browser using
|
|
159
|
+
``app.create_session()``. The widget tree can be built up before any
|
|
160
|
+
browser connects. When a browser navigates to the session URL, the
|
|
161
|
+
pre-built UI is reconstructed automatically.
|
|
162
|
+
|
|
163
|
+
.. code-block:: python
|
|
164
|
+
|
|
165
|
+
session = app.create_session()
|
|
166
|
+
Widgets = session.get_widgets()
|
|
167
|
+
top = Widgets.TopLevel(title="Pre-built UI")
|
|
168
|
+
top.show()
|
|
169
|
+
# ... build the full UI ...
|
|
170
|
+
# When a browser connects with this session's ID and token,
|
|
171
|
+
# the UI appears immediately.
|
|
172
|
+
|
|
173
|
+
Widget References
|
|
174
|
+
-----------------
|
|
175
|
+
|
|
176
|
+
Widgets are identified by integer IDs (``wid``). When a Python widget is
|
|
177
|
+
passed as an argument to another widget's method (e.g., ``vbox.add_widget(btn,
|
|
178
|
+
0)``), the framework automatically converts the Python ``Widget`` object to a
|
|
179
|
+
``{"__wid__": N}`` reference on the wire, and converts it back on return
|
|
180
|
+
values.
|
|
@@ -59,17 +59,42 @@ Running
|
|
|
59
59
|
Session
|
|
60
60
|
-------
|
|
61
61
|
|
|
62
|
-
The async ``Session`` has the same interface as the sync version, but
|
|
63
|
-
are coroutines
|
|
62
|
+
The async ``Session`` has the same interface as the sync version, but most
|
|
63
|
+
methods are coroutines. Sessions persist independently of browser connections,
|
|
64
|
+
support reconnection and multi-browser synchronization (see :doc:`sync` for
|
|
65
|
+
details on these features).
|
|
64
66
|
|
|
65
67
|
.. code-block:: python
|
|
66
68
|
|
|
67
69
|
Widgets = session.get_widgets() # sync -- returns namespace
|
|
68
70
|
btn = await Widgets.Button("Click me") # async -- creates widget
|
|
69
71
|
await btn.set_text("New text") # async -- calls method
|
|
72
|
+
text = btn.get_text() # sync -- returns from local state
|
|
70
73
|
await session.close() # async
|
|
71
74
|
timer = await session.make_timer(duration=1000) # async
|
|
72
75
|
|
|
76
|
+
**Properties:**
|
|
77
|
+
|
|
78
|
+
- ``session.id`` -- unique session identifier.
|
|
79
|
+
- ``session.app`` -- the owning ``Application``.
|
|
80
|
+
- ``session.token`` -- security token for reconnection.
|
|
81
|
+
- ``session.is_connected`` -- ``True`` if at least one browser is connected.
|
|
82
|
+
- ``session.connections`` -- list of active WebSocket connections.
|
|
83
|
+
|
|
84
|
+
**Getter methods** (``get_text()``, ``get_value()``, ``is_visible()``, etc.)
|
|
85
|
+
return from local state without a browser round-trip and do **not** need to
|
|
86
|
+
be awaited. All other widget methods (setters, actions, child methods) are
|
|
87
|
+
coroutines and must be awaited.
|
|
88
|
+
|
|
89
|
+
Creating Sessions Without a Browser
|
|
90
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
91
|
+
|
|
92
|
+
.. code-block:: python
|
|
93
|
+
|
|
94
|
+
session = app.create_session()
|
|
95
|
+
# Build widgets -- sends are queued; no browser needed.
|
|
96
|
+
# Connect a browser later to see the pre-built UI.
|
|
97
|
+
|
|
73
98
|
Concurrency Modes
|
|
74
99
|
-----------------
|
|
75
100
|
|
|
@@ -77,11 +77,17 @@ common patterns:
|
|
|
77
77
|
- Dialog
|
|
78
78
|
- ``(button_text: str)``
|
|
79
79
|
* - ``page-switch``
|
|
80
|
-
- TabWidget, StackWidget
|
|
81
|
-
- ``(index: int)``
|
|
80
|
+
- TabWidget, StackWidget, MDIWidget
|
|
81
|
+
- ``(child: Widget, index: int)``
|
|
82
82
|
* - ``page-close``
|
|
83
|
-
- TabWidget, MDIWidget
|
|
84
|
-
- ``(
|
|
83
|
+
- TabWidget, StackWidget, MDIWidget
|
|
84
|
+
- ``(child: Widget)``
|
|
85
|
+
* - ``child-added``
|
|
86
|
+
- All containers
|
|
87
|
+
- ``(child: Widget)``
|
|
88
|
+
* - ``child-removed``
|
|
89
|
+
- All containers
|
|
90
|
+
- ``(child: Widget)``
|
|
85
91
|
* - ``selected``
|
|
86
92
|
- TreeView, TableView
|
|
87
93
|
- ``(selected_items)``
|