pgwidgets-python 0.3.0__py3-none-any.whl → 0.3.2__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 +169 -5
- pgwidgets/async_/widget.py +9 -1
- pgwidgets/extras/file_browser.py +7 -2
- pgwidgets/method_types.py +12 -1
- pgwidgets/sync/application.py +236 -10
- pgwidgets/sync/widget.py +9 -1
- {pgwidgets_python-0.3.0.dist-info → pgwidgets_python-0.3.2.dist-info}/METADATA +1 -1
- {pgwidgets_python-0.3.0.dist-info → pgwidgets_python-0.3.2.dist-info}/RECORD +11 -11
- {pgwidgets_python-0.3.0.dist-info → pgwidgets_python-0.3.2.dist-info}/WHEEL +0 -0
- {pgwidgets_python-0.3.0.dist-info → pgwidgets_python-0.3.2.dist-info}/licenses/LICENSE.md +0 -0
- {pgwidgets_python-0.3.0.dist-info → pgwidgets_python-0.3.2.dist-info}/top_level.txt +0 -0
pgwidgets/async_/application.py
CHANGED
|
@@ -16,6 +16,7 @@ import logging
|
|
|
16
16
|
import mimetypes
|
|
17
17
|
import signal
|
|
18
18
|
import secrets
|
|
19
|
+
import threading
|
|
19
20
|
import traceback
|
|
20
21
|
from http.server import SimpleHTTPRequestHandler
|
|
21
22
|
from pathlib import Path
|
|
@@ -37,6 +38,15 @@ from pgwidgets.async_.widget import Widget, build_all_widget_classes
|
|
|
37
38
|
_CONCURRENCY_MODES = ("serialized", "per_session", "concurrent")
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
# MIME types for the font formats register_font accepts.
|
|
42
|
+
_FONT_MIME = {
|
|
43
|
+
".ttf": "font/ttf",
|
|
44
|
+
".otf": "font/otf",
|
|
45
|
+
".woff": "font/woff",
|
|
46
|
+
".woff2": "font/woff2",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
40
50
|
class _Namespace:
|
|
41
51
|
"""Holds widget factory methods as attributes (W.Button, W.Label, etc.)."""
|
|
42
52
|
pass
|
|
@@ -1428,6 +1438,13 @@ class Application:
|
|
|
1428
1438
|
self._session_semaphore = None # initialized in start()
|
|
1429
1439
|
self._cb_lock = None # for "serialized" mode
|
|
1430
1440
|
|
|
1441
|
+
# Custom-font registry — see sync.application for details.
|
|
1442
|
+
self._fonts = []
|
|
1443
|
+
self._fonts_by_id = {}
|
|
1444
|
+
self._next_font_id = 1
|
|
1445
|
+
self._default_font = None
|
|
1446
|
+
self._font_lock = threading.Lock()
|
|
1447
|
+
|
|
1431
1448
|
self._run_future = None # set in run(), cancelled by close()
|
|
1432
1449
|
self._httpd = None # HTTP server instance
|
|
1433
1450
|
|
|
@@ -1493,6 +1510,113 @@ class Application:
|
|
|
1493
1510
|
self._widget_classes[name] = cls
|
|
1494
1511
|
return cls
|
|
1495
1512
|
|
|
1513
|
+
# ----- Custom font registration ---------------------------
|
|
1514
|
+
#
|
|
1515
|
+
# API matches the sync backend; see ``sync.application`` for
|
|
1516
|
+
# full docstrings. The async variant schedules sends on the
|
|
1517
|
+
# event loop via ``asyncio.run_coroutine_threadsafe`` so the
|
|
1518
|
+
# method is safe to call from outside the loop (e.g. from the
|
|
1519
|
+
# ``on_connect`` callback running on a worker thread).
|
|
1520
|
+
|
|
1521
|
+
def register_font(self, family, source, *,
|
|
1522
|
+
weight="normal", style="normal"):
|
|
1523
|
+
if isinstance(source, (bytes, bytearray, memoryview)):
|
|
1524
|
+
data = bytes(source)
|
|
1525
|
+
mime = "font/ttf"
|
|
1526
|
+
else:
|
|
1527
|
+
p = Path(source)
|
|
1528
|
+
data = p.read_bytes()
|
|
1529
|
+
mime = _FONT_MIME.get(p.suffix.lower(), "font/ttf")
|
|
1530
|
+
with self._font_lock:
|
|
1531
|
+
font_id = self._next_font_id
|
|
1532
|
+
self._next_font_id += 1
|
|
1533
|
+
entry = {
|
|
1534
|
+
"id": font_id,
|
|
1535
|
+
"family": str(family),
|
|
1536
|
+
"weight": str(weight),
|
|
1537
|
+
"style": str(style),
|
|
1538
|
+
"bytes": data,
|
|
1539
|
+
"mime": mime,
|
|
1540
|
+
}
|
|
1541
|
+
self._fonts.append(entry)
|
|
1542
|
+
self._fonts_by_id[font_id] = entry
|
|
1543
|
+
msg = self._font_register_msg(entry)
|
|
1544
|
+
self._broadcast_font_msg(msg)
|
|
1545
|
+
return font_id
|
|
1546
|
+
|
|
1547
|
+
def set_default_font(self, family, *, size=None,
|
|
1548
|
+
weight=None, style=None):
|
|
1549
|
+
if family is None:
|
|
1550
|
+
self._default_font = None
|
|
1551
|
+
else:
|
|
1552
|
+
self._default_font = {
|
|
1553
|
+
"family": str(family),
|
|
1554
|
+
"size": None if size is None else float(size),
|
|
1555
|
+
"weight": None if weight is None else str(weight),
|
|
1556
|
+
"style": None if style is None else str(style),
|
|
1557
|
+
}
|
|
1558
|
+
self._broadcast_font_msg(self._font_default_msg())
|
|
1559
|
+
|
|
1560
|
+
def open_url(self, url):
|
|
1561
|
+
"""Ask the connected browser(s) to open *url* in a new tab/window.
|
|
1562
|
+
|
|
1563
|
+
Fire-and-forget app-level command; opens the link in the *user's*
|
|
1564
|
+
browser rather than on the host running Python. (Reuses the
|
|
1565
|
+
generic per-session broadcast helper.)
|
|
1566
|
+
"""
|
|
1567
|
+
self._broadcast_font_msg({"type": "open-url", "url": str(url)})
|
|
1568
|
+
|
|
1569
|
+
def _font_register_msg(self, entry):
|
|
1570
|
+
return {
|
|
1571
|
+
"type": "register-font",
|
|
1572
|
+
"id": entry["id"],
|
|
1573
|
+
"family": entry["family"],
|
|
1574
|
+
"weight": entry["weight"],
|
|
1575
|
+
"style": entry["style"],
|
|
1576
|
+
"url": f"/_pgwidgets/font/{entry['id']}",
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
def _font_default_msg(self):
|
|
1580
|
+
return {
|
|
1581
|
+
"type": "set-default-font",
|
|
1582
|
+
"font": self._default_font,
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
def _broadcast_font_msg(self, msg):
|
|
1586
|
+
loop = getattr(self, "_loop", None) or asyncio.get_event_loop()
|
|
1587
|
+
for session in list(self._sessions.values()):
|
|
1588
|
+
try:
|
|
1589
|
+
asyncio.run_coroutine_threadsafe(
|
|
1590
|
+
session._send(dict(msg)), loop)
|
|
1591
|
+
except Exception:
|
|
1592
|
+
pass
|
|
1593
|
+
|
|
1594
|
+
async def _replay_fonts_to_session(self, session):
|
|
1595
|
+
"""Push the registry + default font to a session before
|
|
1596
|
+
any user code runs. ``await``-ed from ``_on_session_open``
|
|
1597
|
+
so the JS side has loaded faces (or at least dispatched
|
|
1598
|
+
the load) before reconstruct / on_connect fires."""
|
|
1599
|
+
with self._font_lock:
|
|
1600
|
+
fonts = list(self._fonts)
|
|
1601
|
+
default = self._default_font
|
|
1602
|
+
for entry in fonts:
|
|
1603
|
+
try:
|
|
1604
|
+
await session._send(self._font_register_msg(entry))
|
|
1605
|
+
except Exception:
|
|
1606
|
+
pass
|
|
1607
|
+
if default is not None:
|
|
1608
|
+
try:
|
|
1609
|
+
await session._send(self._font_default_msg())
|
|
1610
|
+
except Exception:
|
|
1611
|
+
pass
|
|
1612
|
+
|
|
1613
|
+
def _get_font_bytes(self, font_id):
|
|
1614
|
+
with self._font_lock:
|
|
1615
|
+
entry = self._fonts_by_id.get(font_id)
|
|
1616
|
+
if entry is None:
|
|
1617
|
+
return None, None
|
|
1618
|
+
return entry["bytes"], entry["mime"]
|
|
1619
|
+
|
|
1496
1620
|
@property
|
|
1497
1621
|
def sessions(self):
|
|
1498
1622
|
"""Dict of active sessions (session_id -> Session)."""
|
|
@@ -1603,6 +1727,10 @@ class Application:
|
|
|
1603
1727
|
|
|
1604
1728
|
if is_reconnect:
|
|
1605
1729
|
async def do_reconstruct():
|
|
1730
|
+
# Replay the font registry before reconstruct() so
|
|
1731
|
+
# any widget reconstructed with ``set_font(...)``
|
|
1732
|
+
# finds the face already declared.
|
|
1733
|
+
await self._replay_fonts_to_session(session)
|
|
1606
1734
|
self._logger.info(
|
|
1607
1735
|
f"Session {session.id}: reconstructing UI.")
|
|
1608
1736
|
session._reconstructing = True
|
|
@@ -1615,10 +1743,16 @@ class Application:
|
|
|
1615
1743
|
asyncio.ensure_future(do_reconstruct())
|
|
1616
1744
|
else:
|
|
1617
1745
|
self._logger.info(f"Session {session.id} connected.")
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1746
|
+
async def do_connect():
|
|
1747
|
+
# Replay fonts before the user callback so any
|
|
1748
|
+
# widget the user builds with ``set_font(family,
|
|
1749
|
+
# ...)`` sees the face already declared.
|
|
1750
|
+
await self._replay_fonts_to_session(session)
|
|
1751
|
+
if self._on_connect:
|
|
1752
|
+
result = self._on_connect(session)
|
|
1753
|
+
if hasattr(result, "__await__"):
|
|
1754
|
+
await result
|
|
1755
|
+
asyncio.ensure_future(do_connect())
|
|
1622
1756
|
|
|
1623
1757
|
try:
|
|
1624
1758
|
async for message in ws:
|
|
@@ -1663,8 +1797,14 @@ class Application:
|
|
|
1663
1797
|
The newly created session.
|
|
1664
1798
|
"""
|
|
1665
1799
|
if session_id is None:
|
|
1800
|
+
# skip ids already taken by explicitly-created sessions
|
|
1801
|
+
# (e.g. a pre-created default session) so we never
|
|
1802
|
+
# silently overwrite an existing session
|
|
1666
1803
|
session_id = self._next_session_id
|
|
1667
1804
|
self._next_session_id += 1
|
|
1805
|
+
while session_id in self._sessions:
|
|
1806
|
+
session_id = self._next_session_id
|
|
1807
|
+
self._next_session_id += 1
|
|
1668
1808
|
elif session_id in self._sessions:
|
|
1669
1809
|
# If the existing session was auto-created by a browser
|
|
1670
1810
|
# reconnect with the same token, return it so the user's
|
|
@@ -1699,6 +1839,7 @@ class Application:
|
|
|
1699
1839
|
favicon_path = self._favicon_path
|
|
1700
1840
|
ws_host = self._host
|
|
1701
1841
|
ws_port = self._ws_port
|
|
1842
|
+
app = self
|
|
1702
1843
|
|
|
1703
1844
|
class Handler(SimpleHTTPRequestHandler):
|
|
1704
1845
|
def __init__(self, *a, **kw):
|
|
@@ -1707,6 +1848,27 @@ class Application:
|
|
|
1707
1848
|
def do_GET(self):
|
|
1708
1849
|
# Strip query string for path matching (e.g. /?session=1)
|
|
1709
1850
|
path = self.path.split("?")[0]
|
|
1851
|
+
# Custom-font registry: see sync.application for
|
|
1852
|
+
# the matching implementation.
|
|
1853
|
+
if path.startswith("/_pgwidgets/font/"):
|
|
1854
|
+
try:
|
|
1855
|
+
font_id = int(path.rsplit("/", 1)[-1])
|
|
1856
|
+
except ValueError:
|
|
1857
|
+
self.send_error(404)
|
|
1858
|
+
return
|
|
1859
|
+
data, mime = app._get_font_bytes(font_id)
|
|
1860
|
+
if data is None:
|
|
1861
|
+
self.send_error(404)
|
|
1862
|
+
return
|
|
1863
|
+
self.send_response(200)
|
|
1864
|
+
self.send_header("Content-Type", mime)
|
|
1865
|
+
self.send_header("Content-Length", str(len(data)))
|
|
1866
|
+
self.send_header(
|
|
1867
|
+
"Cache-Control", "public, max-age=31536000, immutable")
|
|
1868
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
1869
|
+
self.end_headers()
|
|
1870
|
+
self.wfile.write(data)
|
|
1871
|
+
return
|
|
1710
1872
|
if path == "/" or path == "/index.html":
|
|
1711
1873
|
html = remote_html.read_text(encoding="utf-8")
|
|
1712
1874
|
inject = (
|
|
@@ -1807,12 +1969,14 @@ class Application:
|
|
|
1807
1969
|
|
|
1808
1970
|
In the async API, callbacks are dispatched by the asyncio event
|
|
1809
1971
|
loop directly, so this simply yields control for the requested
|
|
1810
|
-
duration.
|
|
1972
|
+
duration. A *timeout* of ``0`` yields once (a non-blocking drain
|
|
1973
|
+
of ready callbacks), matching the sync API's contract.
|
|
1811
1974
|
|
|
1812
1975
|
Parameters
|
|
1813
1976
|
----------
|
|
1814
1977
|
timeout : float
|
|
1815
1978
|
Seconds to yield to the event loop (default 0.1).
|
|
1979
|
+
Use ``0`` for a single non-blocking yield.
|
|
1816
1980
|
"""
|
|
1817
1981
|
await asyncio.sleep(timeout)
|
|
1818
1982
|
|
pgwidgets/async_/widget.py
CHANGED
|
@@ -446,6 +446,11 @@ def _resolve_kwargs(method_name, param_names, args, kwargs):
|
|
|
446
446
|
are bundled into a dict for that parameter (e.g.
|
|
447
447
|
``add_widget(child, title="Tab 1")`` becomes
|
|
448
448
|
``add_widget(child, {"title": "Tab 1"})``).
|
|
449
|
+
|
|
450
|
+
Skipped-positional kwargs are supported: a call like
|
|
451
|
+
``set_color(fg='red')`` against ``param_names = ['bg', 'fg']``
|
|
452
|
+
fills the omitted ``bg`` slot with ``None`` (the JS-side
|
|
453
|
+
default) instead of erroring out.
|
|
449
454
|
"""
|
|
450
455
|
if not kwargs:
|
|
451
456
|
return args
|
|
@@ -456,7 +461,10 @@ def _resolve_kwargs(method_name, param_names, args, kwargs):
|
|
|
456
461
|
if name in kwargs:
|
|
457
462
|
merged.append(kwargs.pop(name))
|
|
458
463
|
else:
|
|
459
|
-
|
|
464
|
+
# Leave a placeholder so subsequent kwargs can land in
|
|
465
|
+
# later positions. The JS side reads omitted args as
|
|
466
|
+
# null / default, which matches ``None`` here.
|
|
467
|
+
merged.append(None)
|
|
460
468
|
if kwargs and param_names and param_names[-1] == "options":
|
|
461
469
|
# Bundle remaining kwargs into the options dict
|
|
462
470
|
opts_idx = len(param_names) - 1
|
pgwidgets/extras/file_browser.py
CHANGED
|
@@ -352,8 +352,13 @@ class FileBrowser(Callbacks):
|
|
|
352
352
|
self._navigate_to(d)
|
|
353
353
|
self._name_entry.set_text(os.path.basename(path))
|
|
354
354
|
|
|
355
|
-
def _on_row_activated(self, values, path):
|
|
356
|
-
"""Double-click on a row.
|
|
355
|
+
def _on_row_activated(self, values, path, col_key=None):
|
|
356
|
+
"""Double-click on a row.
|
|
357
|
+
|
|
358
|
+
``col_key`` (TableView ≥ this rev) reports which cell was
|
|
359
|
+
clicked; unused here since the browser only cares about
|
|
360
|
+
the row's filename, but the arg has to be in the signature
|
|
361
|
+
or the new 3-arg dispatch raises TypeError."""
|
|
357
362
|
name = values.get("name", "")
|
|
358
363
|
if name == "..":
|
|
359
364
|
self._go_up()
|
pgwidgets/method_types.py
CHANGED
|
@@ -56,7 +56,7 @@ JS_ONLY_METHODS = {
|
|
|
56
56
|
# ComboBox
|
|
57
57
|
"get_alpha",
|
|
58
58
|
# Container
|
|
59
|
-
"num_children",
|
|
59
|
+
"num_children", "is_container",
|
|
60
60
|
# TabWidget lookups
|
|
61
61
|
"get_tab_id", "get_child", "index_of",
|
|
62
62
|
# MDIWidget
|
|
@@ -82,6 +82,17 @@ ACTION_METHODS = {
|
|
|
82
82
|
"expand_all", "collapse_all", "expand_item", "collapse_item",
|
|
83
83
|
"sort_by_column", "set_optimal_column_widths",
|
|
84
84
|
"select_path", "select_paths", "select_all",
|
|
85
|
+
"select_cell", "select_cells", "clear_cell_selection",
|
|
86
|
+
# Per-cell / row / column / table colour overrides — the
|
|
87
|
+
# ``set_*`` naming would otherwise classify them as SETTERs,
|
|
88
|
+
# whose single-state-slot semantics can't represent the
|
|
89
|
+
# accumulated dict of overrides we actually keep. ACTION
|
|
90
|
+
# dispatch sends each call straight through to the JS side,
|
|
91
|
+
# which holds the canonical state in its own maps.
|
|
92
|
+
"set_cell_color", "set_row_color", "set_column_color",
|
|
93
|
+
"set_table_color",
|
|
94
|
+
"clear_cell_color", "clear_row_color", "clear_column_color",
|
|
95
|
+
"clear_all_colors",
|
|
85
96
|
# TextSource editing
|
|
86
97
|
"insert_text", "delete_range", "create_tag", "remove_tag_def",
|
|
87
98
|
"apply_tag", "remove_tag", "create_ref", "remove_ref",
|
pgwidgets/sync/application.py
CHANGED
|
@@ -130,6 +130,17 @@ def _run_queue_loop(cb_queue, stop_event, logger=None):
|
|
|
130
130
|
result_slot['event'].set()
|
|
131
131
|
|
|
132
132
|
|
|
133
|
+
# MIME types for the font formats register_font accepts. Used to
|
|
134
|
+
# set the ``Content-Type`` header when the HTTP server delivers a
|
|
135
|
+
# registered font to the browser.
|
|
136
|
+
_FONT_MIME = {
|
|
137
|
+
".ttf": "font/ttf",
|
|
138
|
+
".otf": "font/otf",
|
|
139
|
+
".woff": "font/woff",
|
|
140
|
+
".woff2": "font/woff2",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
133
144
|
class Session:
|
|
134
145
|
"""
|
|
135
146
|
A session that owns a widget tree and its associated state.
|
|
@@ -1642,6 +1653,17 @@ class Application:
|
|
|
1642
1653
|
self._on_disconnect = None # user callback: fn(session)
|
|
1643
1654
|
self._cb_queue = queue.Queue() # for "serialized" mode
|
|
1644
1655
|
|
|
1656
|
+
# Custom-font registry. Each entry is
|
|
1657
|
+
# ``{id, family, weight, style, bytes, mime}``; ``id`` is a
|
|
1658
|
+
# monotonic int used as the path component of the HTTP URL
|
|
1659
|
+
# the browser fetches the font from. ``_default_font`` is
|
|
1660
|
+
# ``{family, size, weight, style}`` or ``None``.
|
|
1661
|
+
self._fonts = []
|
|
1662
|
+
self._fonts_by_id = {}
|
|
1663
|
+
self._next_font_id = 1
|
|
1664
|
+
self._default_font = None
|
|
1665
|
+
self._font_lock = threading.Lock()
|
|
1666
|
+
|
|
1645
1667
|
self._loop = None
|
|
1646
1668
|
self._shutdown = threading.Event()
|
|
1647
1669
|
self._thread = None
|
|
@@ -1714,6 +1736,155 @@ class Application:
|
|
|
1714
1736
|
self._widget_classes[name] = cls
|
|
1715
1737
|
return cls
|
|
1716
1738
|
|
|
1739
|
+
# ----- Custom font registration ---------------------------
|
|
1740
|
+
|
|
1741
|
+
def register_font(self, family, source, *,
|
|
1742
|
+
weight="normal", style="normal"):
|
|
1743
|
+
"""Register a custom font with the application.
|
|
1744
|
+
|
|
1745
|
+
Browsers receive a ``@font-face``-equivalent declaration
|
|
1746
|
+
through the JS ``FontFace`` API, so any widget that does
|
|
1747
|
+
``set_font(family, ...)`` thereafter renders with this
|
|
1748
|
+
face. Multiple registrations for the same ``family`` but
|
|
1749
|
+
different ``weight`` / ``style`` combine into a single
|
|
1750
|
+
font family with multiple faces.
|
|
1751
|
+
|
|
1752
|
+
Parameters
|
|
1753
|
+
----------
|
|
1754
|
+
family : str
|
|
1755
|
+
CSS font-family name to expose to widgets.
|
|
1756
|
+
source : str | os.PathLike | bytes | bytearray | memoryview
|
|
1757
|
+
Path to a font file (``.ttf`` / ``.otf`` / ``.woff`` /
|
|
1758
|
+
``.woff2``) or raw font bytes.
|
|
1759
|
+
weight : str
|
|
1760
|
+
CSS ``font-weight`` -- ``'normal'``, ``'bold'``, or a
|
|
1761
|
+
numeric string (``'100'`` ... ``'900'``).
|
|
1762
|
+
style : str
|
|
1763
|
+
CSS ``font-style`` -- ``'normal'``, ``'italic'``, or
|
|
1764
|
+
``'oblique'``.
|
|
1765
|
+
|
|
1766
|
+
Returns
|
|
1767
|
+
-------
|
|
1768
|
+
int
|
|
1769
|
+
The registration id (rarely needed; callers usually
|
|
1770
|
+
just refer to the font by ``family``).
|
|
1771
|
+
"""
|
|
1772
|
+
if isinstance(source, (bytes, bytearray, memoryview)):
|
|
1773
|
+
data = bytes(source)
|
|
1774
|
+
mime = "font/ttf"
|
|
1775
|
+
else:
|
|
1776
|
+
p = Path(source)
|
|
1777
|
+
data = p.read_bytes()
|
|
1778
|
+
ext = p.suffix.lower()
|
|
1779
|
+
mime = _FONT_MIME.get(ext, "font/ttf")
|
|
1780
|
+
with self._font_lock:
|
|
1781
|
+
font_id = self._next_font_id
|
|
1782
|
+
self._next_font_id += 1
|
|
1783
|
+
entry = {
|
|
1784
|
+
"id": font_id,
|
|
1785
|
+
"family": str(family),
|
|
1786
|
+
"weight": str(weight),
|
|
1787
|
+
"style": str(style),
|
|
1788
|
+
"bytes": data,
|
|
1789
|
+
"mime": mime,
|
|
1790
|
+
}
|
|
1791
|
+
self._fonts.append(entry)
|
|
1792
|
+
self._fonts_by_id[font_id] = entry
|
|
1793
|
+
# Push to any already-connected sessions so live UIs pick
|
|
1794
|
+
# the font up without a reconnect.
|
|
1795
|
+
msg = self._font_register_msg(entry)
|
|
1796
|
+
for session in list(self._sessions.values()):
|
|
1797
|
+
try:
|
|
1798
|
+
session._send(msg)
|
|
1799
|
+
except Exception:
|
|
1800
|
+
pass
|
|
1801
|
+
return font_id
|
|
1802
|
+
|
|
1803
|
+
def set_default_font(self, family, *, size=None,
|
|
1804
|
+
weight=None, style=None):
|
|
1805
|
+
"""Set the document-level default font.
|
|
1806
|
+
|
|
1807
|
+
Writes ``--pg-default-font-family`` / ``--pg-default-font-size``
|
|
1808
|
+
/ ``--pg-default-font-weight`` / ``--pg-default-font-style``
|
|
1809
|
+
CSS variables on ``:root``; the base pgwidgets stylesheet
|
|
1810
|
+
consumes these so any widget that hasn't been given an
|
|
1811
|
+
explicit ``set_font(...)`` follows the default.
|
|
1812
|
+
|
|
1813
|
+
Pass ``family=None`` to clear the default and fall back to
|
|
1814
|
+
the built-in stylesheet."""
|
|
1815
|
+
if family is None:
|
|
1816
|
+
self._default_font = None
|
|
1817
|
+
else:
|
|
1818
|
+
self._default_font = {
|
|
1819
|
+
"family": str(family),
|
|
1820
|
+
"size": None if size is None else float(size),
|
|
1821
|
+
"weight": None if weight is None else str(weight),
|
|
1822
|
+
"style": None if style is None else str(style),
|
|
1823
|
+
}
|
|
1824
|
+
msg = self._font_default_msg()
|
|
1825
|
+
for session in list(self._sessions.values()):
|
|
1826
|
+
try:
|
|
1827
|
+
session._send(msg)
|
|
1828
|
+
except Exception:
|
|
1829
|
+
pass
|
|
1830
|
+
|
|
1831
|
+
def open_url(self, url):
|
|
1832
|
+
"""Ask the connected browser(s) to open *url* in a new tab/window.
|
|
1833
|
+
|
|
1834
|
+
Fire-and-forget app-level command (mirrors set_default_font's
|
|
1835
|
+
per-session send). This opens the link in the *user's* browser
|
|
1836
|
+
rather than on the host running Python.
|
|
1837
|
+
"""
|
|
1838
|
+
msg = {"type": "open-url", "url": str(url)}
|
|
1839
|
+
for session in list(self._sessions.values()):
|
|
1840
|
+
try:
|
|
1841
|
+
session._send(msg)
|
|
1842
|
+
except Exception:
|
|
1843
|
+
pass
|
|
1844
|
+
|
|
1845
|
+
def _font_register_msg(self, entry):
|
|
1846
|
+
return {
|
|
1847
|
+
"type": "register-font",
|
|
1848
|
+
"id": entry["id"],
|
|
1849
|
+
"family": entry["family"],
|
|
1850
|
+
"weight": entry["weight"],
|
|
1851
|
+
"style": entry["style"],
|
|
1852
|
+
"url": f"/_pgwidgets/font/{entry['id']}",
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
def _font_default_msg(self):
|
|
1856
|
+
return {
|
|
1857
|
+
"type": "set-default-font",
|
|
1858
|
+
"font": self._default_font,
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
def _replay_fonts_to_session(self, session):
|
|
1862
|
+
"""Push the full font registry + default font to a freshly
|
|
1863
|
+
connected (or reconnecting) session before any user code
|
|
1864
|
+
creates widgets. Called from ``_on_session_open``."""
|
|
1865
|
+
with self._font_lock:
|
|
1866
|
+
fonts = list(self._fonts)
|
|
1867
|
+
default = self._default_font
|
|
1868
|
+
for entry in fonts:
|
|
1869
|
+
try:
|
|
1870
|
+
session._send(self._font_register_msg(entry))
|
|
1871
|
+
except Exception:
|
|
1872
|
+
pass
|
|
1873
|
+
if default is not None:
|
|
1874
|
+
try:
|
|
1875
|
+
session._send(self._font_default_msg())
|
|
1876
|
+
except Exception:
|
|
1877
|
+
pass
|
|
1878
|
+
|
|
1879
|
+
def _get_font_bytes(self, font_id):
|
|
1880
|
+
"""Return ``(bytes, mime)`` for a registered font, or
|
|
1881
|
+
``(None, None)`` if unknown. Called by the HTTP handler."""
|
|
1882
|
+
with self._font_lock:
|
|
1883
|
+
entry = self._fonts_by_id.get(font_id)
|
|
1884
|
+
if entry is None:
|
|
1885
|
+
return None, None
|
|
1886
|
+
return entry["bytes"], entry["mime"]
|
|
1887
|
+
|
|
1717
1888
|
def start(self):
|
|
1718
1889
|
"""Start the WebSocket server (and HTTP server if enabled).
|
|
1719
1890
|
|
|
@@ -1836,6 +2007,10 @@ class Application:
|
|
|
1836
2007
|
# because _send() blocks with event.wait() while the event
|
|
1837
2008
|
# loop that must process ws.send() is also blocked.
|
|
1838
2009
|
def do_reconstruct():
|
|
2010
|
+
# Replay the font registry before reconstruct() so any
|
|
2011
|
+
# widget reconstructed with ``set_font(...)`` finds the
|
|
2012
|
+
# face already declared.
|
|
2013
|
+
self._replay_fonts_to_session(session)
|
|
1839
2014
|
self._logger.info(
|
|
1840
2015
|
f"Session {session.id}: reconstructing UI.")
|
|
1841
2016
|
session._reconstructing = True
|
|
@@ -1848,8 +2023,14 @@ class Application:
|
|
|
1848
2023
|
self._dispatch(session, do_reconstruct, ())
|
|
1849
2024
|
else:
|
|
1850
2025
|
self._logger.info(f"Session {session.id} connected.")
|
|
1851
|
-
|
|
1852
|
-
|
|
2026
|
+
def do_connect():
|
|
2027
|
+
# Replay fonts before the user callback so any widget
|
|
2028
|
+
# the user builds with ``set_font(family, ...)`` sees
|
|
2029
|
+
# the face already declared.
|
|
2030
|
+
self._replay_fonts_to_session(session)
|
|
2031
|
+
if self._on_connect:
|
|
2032
|
+
self._on_connect(session)
|
|
2033
|
+
self._dispatch(session, do_connect, ())
|
|
1853
2034
|
|
|
1854
2035
|
try:
|
|
1855
2036
|
async for message in ws:
|
|
@@ -1895,8 +2076,14 @@ class Application:
|
|
|
1895
2076
|
"""
|
|
1896
2077
|
with self._session_lock:
|
|
1897
2078
|
if session_id is None:
|
|
2079
|
+
# skip ids already taken by explicitly-created sessions
|
|
2080
|
+
# (e.g. a pre-created default session) so we never
|
|
2081
|
+
# silently overwrite an existing session
|
|
1898
2082
|
session_id = self._next_session_id
|
|
1899
2083
|
self._next_session_id += 1
|
|
2084
|
+
while session_id in self._sessions:
|
|
2085
|
+
session_id = self._next_session_id
|
|
2086
|
+
self._next_session_id += 1
|
|
1900
2087
|
elif session_id in self._sessions:
|
|
1901
2088
|
# If the existing session was auto-created by a browser
|
|
1902
2089
|
# reconnect with the same token, return it so the user's
|
|
@@ -1942,6 +2129,7 @@ class Application:
|
|
|
1942
2129
|
favicon_path = self._favicon_path
|
|
1943
2130
|
ws_host = self._host
|
|
1944
2131
|
ws_port = self._ws_port
|
|
2132
|
+
app = self
|
|
1945
2133
|
|
|
1946
2134
|
class Handler(http.server.SimpleHTTPRequestHandler):
|
|
1947
2135
|
def __init__(self, *args, **kwargs):
|
|
@@ -1951,6 +2139,31 @@ class Application:
|
|
|
1951
2139
|
# serve remote.html at the root, with WS URL injected
|
|
1952
2140
|
# Strip query string for path matching (e.g. /?session=1)
|
|
1953
2141
|
path = self.path.split("?")[0]
|
|
2142
|
+
# Custom-font registry: /_pgwidgets/font/<id> serves
|
|
2143
|
+
# the bytes the app registered via ``register_font``.
|
|
2144
|
+
# ``Cache-Control: immutable`` is safe because the
|
|
2145
|
+
# registry assigns a fresh id on every call -- the
|
|
2146
|
+
# URL is content-stable for the lifetime of the
|
|
2147
|
+
# registration.
|
|
2148
|
+
if path.startswith("/_pgwidgets/font/"):
|
|
2149
|
+
try:
|
|
2150
|
+
font_id = int(path.rsplit("/", 1)[-1])
|
|
2151
|
+
except ValueError:
|
|
2152
|
+
self.send_error(404)
|
|
2153
|
+
return
|
|
2154
|
+
data, mime = app._get_font_bytes(font_id)
|
|
2155
|
+
if data is None:
|
|
2156
|
+
self.send_error(404)
|
|
2157
|
+
return
|
|
2158
|
+
self.send_response(200)
|
|
2159
|
+
self.send_header("Content-Type", mime)
|
|
2160
|
+
self.send_header("Content-Length", str(len(data)))
|
|
2161
|
+
self.send_header(
|
|
2162
|
+
"Cache-Control", "public, max-age=31536000, immutable")
|
|
2163
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
2164
|
+
self.end_headers()
|
|
2165
|
+
self.wfile.write(data)
|
|
2166
|
+
return
|
|
1954
2167
|
if path == "/" or path == "/index.html":
|
|
1955
2168
|
html = remote_html.read_text(encoding="utf-8")
|
|
1956
2169
|
inject = (
|
|
@@ -2086,22 +2299,35 @@ class Application:
|
|
|
2086
2299
|
``concurrent`` modes, callbacks already run on their own threads
|
|
2087
2300
|
so this method simply yields control briefly.
|
|
2088
2301
|
|
|
2302
|
+
A *timeout* of ``0`` drains every callback currently queued and
|
|
2303
|
+
returns immediately without blocking -- useful when pumping from
|
|
2304
|
+
within another event loop (e.g. a Jupyter kernel's asyncio loop).
|
|
2305
|
+
|
|
2089
2306
|
Parameters
|
|
2090
2307
|
----------
|
|
2091
2308
|
timeout : float
|
|
2092
2309
|
Maximum seconds to spend processing events (default 0.1).
|
|
2310
|
+
Use ``0`` for a non-blocking drain of the pending queue.
|
|
2093
2311
|
"""
|
|
2094
2312
|
if self._concurrency == "serialized":
|
|
2313
|
+
non_blocking = (timeout == 0)
|
|
2095
2314
|
deadline = time.monotonic() + timeout
|
|
2096
2315
|
while not self._shutdown.is_set():
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2316
|
+
if non_blocking:
|
|
2317
|
+
try:
|
|
2318
|
+
handler, args, kwargs, result_slot = \
|
|
2319
|
+
self._cb_queue.get_nowait()
|
|
2320
|
+
except queue.Empty:
|
|
2321
|
+
break
|
|
2322
|
+
else:
|
|
2323
|
+
remaining = deadline - time.monotonic()
|
|
2324
|
+
if remaining <= 0:
|
|
2325
|
+
break
|
|
2326
|
+
try:
|
|
2327
|
+
handler, args, kwargs, result_slot = \
|
|
2328
|
+
self._cb_queue.get(timeout=min(remaining, 0.5))
|
|
2329
|
+
except queue.Empty:
|
|
2330
|
+
continue
|
|
2105
2331
|
try:
|
|
2106
2332
|
rv = handler(*args, **kwargs)
|
|
2107
2333
|
if result_slot is not None:
|
pgwidgets/sync/widget.py
CHANGED
|
@@ -420,6 +420,11 @@ def _resolve_kwargs(method_name, param_names, args, kwargs):
|
|
|
420
420
|
are bundled into a dict for that parameter (e.g.
|
|
421
421
|
``add_widget(child, title="Tab 1")`` becomes
|
|
422
422
|
``add_widget(child, {"title": "Tab 1"})``).
|
|
423
|
+
|
|
424
|
+
Skipped-positional kwargs are supported: a call like
|
|
425
|
+
``set_color(fg='red')`` against ``param_names = ['bg', 'fg']``
|
|
426
|
+
fills the omitted ``bg`` slot with ``None`` (the JS-side
|
|
427
|
+
default) instead of erroring out.
|
|
423
428
|
"""
|
|
424
429
|
if not kwargs:
|
|
425
430
|
return args
|
|
@@ -430,7 +435,10 @@ def _resolve_kwargs(method_name, param_names, args, kwargs):
|
|
|
430
435
|
if name in kwargs:
|
|
431
436
|
merged.append(kwargs.pop(name))
|
|
432
437
|
else:
|
|
433
|
-
|
|
438
|
+
# Leave a placeholder so subsequent kwargs can land in
|
|
439
|
+
# later positions. The JS side reads omitted args as
|
|
440
|
+
# null / default, which matches ``None`` here.
|
|
441
|
+
merged.append(None)
|
|
434
442
|
if kwargs and param_names and param_names[-1] == "options":
|
|
435
443
|
# Bundle remaining kwargs into the options dict
|
|
436
444
|
opts_idx = len(param_names) - 1
|
|
@@ -3,19 +3,19 @@ pgwidgets/_json.py,sha256=o21qywJ6yAldbqxTq3nLwgK9O67r3a8JxhvI_WAJnMY,2184
|
|
|
3
3
|
pgwidgets/buffer.py,sha256=BYj_nb6fuyGsBqUp3_Z1YrzUm4h5VmoWPGKvpTjWrYk,4048
|
|
4
4
|
pgwidgets/callbacks.py,sha256=gA2FnX0N5BmbmOMyEV35yHySgAfVjfAZcZhZbo7j4-c,3314
|
|
5
5
|
pgwidgets/defs.py,sha256=Q8qhvTeansFU1Av6j5ncdwF2xSNHrpXuKErdHWQ3HiA,436
|
|
6
|
-
pgwidgets/method_types.py,sha256=
|
|
6
|
+
pgwidgets/method_types.py,sha256=_-rdE5BOYLra8x3BMKv5-QS4Z8YB29DBhUCDsGiB-Gw,18314
|
|
7
7
|
pgwidgets/async_/Widgets.py,sha256=vld2xkvusBBEVbhbezaJsjBRRj3L7c17Up0pOVs8PGo,723
|
|
8
8
|
pgwidgets/async_/__init__.py,sha256=rXB-v9XRrYt1imYuYikhkzIyiRqaYhlTpQei2LHaQ18,564
|
|
9
|
-
pgwidgets/async_/application.py,sha256=
|
|
10
|
-
pgwidgets/async_/widget.py,sha256=
|
|
9
|
+
pgwidgets/async_/application.py,sha256=2RJNRerXu5LXG_UcMSi9HWOCAlEOu96TrRaoIcUZ86Q,80957
|
|
10
|
+
pgwidgets/async_/widget.py,sha256=gO7eJh9LV3UY7yRLdeml_0gqmHnFB5VU3NWlDURLtfc,38042
|
|
11
11
|
pgwidgets/extras/__init__.py,sha256=AXUmFtnn4RSlp6pJX6z55j-m_29VmfzpYIjQvAy7pO0,325
|
|
12
|
-
pgwidgets/extras/file_browser.py,sha256=
|
|
12
|
+
pgwidgets/extras/file_browser.py,sha256=CXQSfJx1VQ-fJfhKNrVyqrkKTJEQngmiP954y70Fjo0,17229
|
|
13
13
|
pgwidgets/sync/Widgets.py,sha256=7SaocMVMzFHhi51pjuEAr47Wez90b_a61kDbiv0jOfM,706
|
|
14
14
|
pgwidgets/sync/__init__.py,sha256=SF5RTAvtu8BbYBWzpiCPipy6DJzNXf6nqk9xdvwxUhQ,542
|
|
15
|
-
pgwidgets/sync/application.py,sha256=
|
|
16
|
-
pgwidgets/sync/widget.py,sha256=
|
|
17
|
-
pgwidgets_python-0.3.
|
|
18
|
-
pgwidgets_python-0.3.
|
|
19
|
-
pgwidgets_python-0.3.
|
|
20
|
-
pgwidgets_python-0.3.
|
|
21
|
-
pgwidgets_python-0.3.
|
|
15
|
+
pgwidgets/sync/application.py,sha256=tcAJvSVdijfT_KBP4llY1tyg2AAmQM-1qocw8Zf51nM,96240
|
|
16
|
+
pgwidgets/sync/widget.py,sha256=yrScNkZB34u0JXn3-kCB-1mPLxLysVbuaPsCjC8kq0E,38343
|
|
17
|
+
pgwidgets_python-0.3.2.dist-info/licenses/LICENSE.md,sha256=LoM3fMTiMnQuHRCJghdjOtjnCrL8soBpu2PFk24Xvyg,1528
|
|
18
|
+
pgwidgets_python-0.3.2.dist-info/METADATA,sha256=gzz5UD--Uq8I9azawdmXRE-G4KNDrTFDtkVhiNbTSzs,4568
|
|
19
|
+
pgwidgets_python-0.3.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
20
|
+
pgwidgets_python-0.3.2.dist-info/top_level.txt,sha256=wwL6fBq0gU-JwzlM6TdduY1qYpu39ysqnnbQT-1bqAs,10
|
|
21
|
+
pgwidgets_python-0.3.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|