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.
Files changed (67) hide show
  1. pgwidgets_python-0.2.0/.github/workflows/tests.yml +28 -0
  2. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/PKG-INFO +35 -1
  3. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/README.md +34 -0
  4. pgwidgets_python-0.2.0/docs/WhatsNew.rst +162 -0
  5. pgwidgets_python-0.2.0/docs/architecture.rst +180 -0
  6. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/async.rst +27 -2
  7. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/callbacks.rst +10 -4
  8. pgwidgets_python-0.2.0/docs/extras.rst +273 -0
  9. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/getting-started.rst +12 -6
  10. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/index.rst +4 -0
  11. pgwidgets_python-0.2.0/docs/subclassing.rst +242 -0
  12. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/sync.rst +52 -4
  13. pgwidgets_python-0.2.0/docs/utilities.rst +40 -0
  14. pgwidgets_python-0.2.0/docs/widgets.rst +808 -0
  15. pgwidgets_python-0.2.0/examples/all_widgets.py +712 -0
  16. pgwidgets_python-0.2.0/examples/all_widgets_async.py +782 -0
  17. pgwidgets_python-0.2.0/examples/demo_treeview.py +98 -0
  18. pgwidgets_python-0.2.0/pgwidgets/async_/Widgets.py +22 -0
  19. pgwidgets_python-0.2.0/pgwidgets/async_/application.py +1633 -0
  20. pgwidgets_python-0.2.0/pgwidgets/async_/widget.py +935 -0
  21. pgwidgets_python-0.2.0/pgwidgets/callbacks.py +95 -0
  22. pgwidgets_python-0.2.0/pgwidgets/extras/__init__.py +11 -0
  23. pgwidgets_python-0.2.0/pgwidgets/extras/file_browser.py +479 -0
  24. pgwidgets_python-0.2.0/pgwidgets/method_types.py +447 -0
  25. pgwidgets_python-0.2.0/pgwidgets/sync/Widgets.py +22 -0
  26. pgwidgets_python-0.2.0/pgwidgets/sync/application.py +1879 -0
  27. pgwidgets_python-0.2.0/pgwidgets/sync/widget.py +919 -0
  28. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets_python.egg-info/PKG-INFO +35 -1
  29. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets_python.egg-info/SOURCES.txt +18 -0
  30. pgwidgets_python-0.2.0/test_pg2.py +45 -0
  31. pgwidgets_python-0.2.0/tests/test_extras.py +53 -0
  32. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/tests/test_protocol.py +11 -9
  33. pgwidgets_python-0.2.0/tests/test_reconstruct.py +513 -0
  34. pgwidgets_python-0.2.0/tests/test_session.py +377 -0
  35. pgwidgets_python-0.2.0/tests/test_stateful.py +454 -0
  36. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/tests/test_widget_classes.py +2 -1
  37. pgwidgets_python-0.1.3/docs/architecture.rst +0 -94
  38. pgwidgets_python-0.1.3/docs/widgets.rst +0 -579
  39. pgwidgets_python-0.1.3/examples/demo_treeview.py +0 -94
  40. pgwidgets_python-0.1.3/pgwidgets/async_/application.py +0 -660
  41. pgwidgets_python-0.1.3/pgwidgets/async_/widget.py +0 -171
  42. pgwidgets_python-0.1.3/pgwidgets/sync/application.py +0 -796
  43. pgwidgets_python-0.1.3/pgwidgets/sync/widget.py +0 -166
  44. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/.flake8 +0 -0
  45. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/.gitignore +0 -0
  46. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/.readthedocs.yaml +0 -0
  47. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/LICENSE.md +0 -0
  48. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/Makefile +0 -0
  49. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/api/async.rst +0 -0
  50. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/api/index.rst +0 -0
  51. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/api/sync.rst +0 -0
  52. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/conf.py +0 -0
  53. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/docs/web-servers.rst +0 -0
  54. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/examples/README.md +0 -0
  55. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/examples/demo_async.py +0 -0
  56. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/examples/demo_sync.py +0 -0
  57. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets/__init__.py +0 -0
  58. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets/async_/__init__.py +0 -0
  59. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets/defs.py +0 -0
  60. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets/sync/__init__.py +0 -0
  61. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets_python.egg-info/dependency_links.txt +0 -0
  62. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets_python.egg-info/requires.txt +0 -0
  63. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pgwidgets_python.egg-info/top_level.txt +0 -0
  64. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/pyproject.toml +0 -0
  65. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/setup.cfg +0 -0
  66. {pgwidgets_python-0.1.3 → pgwidgets_python-0.2.0}/tests/__init__.py +0 -0
  67. {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.1.3
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 methods
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
- - ``(index: int)``
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)``