pgwidgets-python 0.3.0__py3-none-any.whl → 0.3.1__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 +151 -4
- pgwidgets/async_/widget.py +9 -1
- pgwidgets/extras/file_browser.py +7 -2
- pgwidgets/method_types.py +11 -0
- pgwidgets/sync/application.py +195 -2
- pgwidgets/sync/widget.py +9 -1
- {pgwidgets_python-0.3.0.dist-info → pgwidgets_python-0.3.1.dist-info}/METADATA +1 -1
- {pgwidgets_python-0.3.0.dist-info → pgwidgets_python-0.3.1.dist-info}/RECORD +11 -11
- {pgwidgets_python-0.3.0.dist-info → pgwidgets_python-0.3.1.dist-info}/WHEEL +0 -0
- {pgwidgets_python-0.3.0.dist-info → pgwidgets_python-0.3.1.dist-info}/licenses/LICENSE.md +0 -0
- {pgwidgets_python-0.3.0.dist-info → pgwidgets_python-0.3.1.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,104 @@ 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 _font_register_msg(self, entry):
|
|
1561
|
+
return {
|
|
1562
|
+
"type": "register-font",
|
|
1563
|
+
"id": entry["id"],
|
|
1564
|
+
"family": entry["family"],
|
|
1565
|
+
"weight": entry["weight"],
|
|
1566
|
+
"style": entry["style"],
|
|
1567
|
+
"url": f"/_pgwidgets/font/{entry['id']}",
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
def _font_default_msg(self):
|
|
1571
|
+
return {
|
|
1572
|
+
"type": "set-default-font",
|
|
1573
|
+
"font": self._default_font,
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
def _broadcast_font_msg(self, msg):
|
|
1577
|
+
loop = getattr(self, "_loop", None) or asyncio.get_event_loop()
|
|
1578
|
+
for session in list(self._sessions.values()):
|
|
1579
|
+
try:
|
|
1580
|
+
asyncio.run_coroutine_threadsafe(
|
|
1581
|
+
session._send(dict(msg)), loop)
|
|
1582
|
+
except Exception:
|
|
1583
|
+
pass
|
|
1584
|
+
|
|
1585
|
+
async def _replay_fonts_to_session(self, session):
|
|
1586
|
+
"""Push the registry + default font to a session before
|
|
1587
|
+
any user code runs. ``await``-ed from ``_on_session_open``
|
|
1588
|
+
so the JS side has loaded faces (or at least dispatched
|
|
1589
|
+
the load) before reconstruct / on_connect fires."""
|
|
1590
|
+
with self._font_lock:
|
|
1591
|
+
fonts = list(self._fonts)
|
|
1592
|
+
default = self._default_font
|
|
1593
|
+
for entry in fonts:
|
|
1594
|
+
try:
|
|
1595
|
+
await session._send(self._font_register_msg(entry))
|
|
1596
|
+
except Exception:
|
|
1597
|
+
pass
|
|
1598
|
+
if default is not None:
|
|
1599
|
+
try:
|
|
1600
|
+
await session._send(self._font_default_msg())
|
|
1601
|
+
except Exception:
|
|
1602
|
+
pass
|
|
1603
|
+
|
|
1604
|
+
def _get_font_bytes(self, font_id):
|
|
1605
|
+
with self._font_lock:
|
|
1606
|
+
entry = self._fonts_by_id.get(font_id)
|
|
1607
|
+
if entry is None:
|
|
1608
|
+
return None, None
|
|
1609
|
+
return entry["bytes"], entry["mime"]
|
|
1610
|
+
|
|
1496
1611
|
@property
|
|
1497
1612
|
def sessions(self):
|
|
1498
1613
|
"""Dict of active sessions (session_id -> Session)."""
|
|
@@ -1603,6 +1718,10 @@ class Application:
|
|
|
1603
1718
|
|
|
1604
1719
|
if is_reconnect:
|
|
1605
1720
|
async def do_reconstruct():
|
|
1721
|
+
# Replay the font registry before reconstruct() so
|
|
1722
|
+
# any widget reconstructed with ``set_font(...)``
|
|
1723
|
+
# finds the face already declared.
|
|
1724
|
+
await self._replay_fonts_to_session(session)
|
|
1606
1725
|
self._logger.info(
|
|
1607
1726
|
f"Session {session.id}: reconstructing UI.")
|
|
1608
1727
|
session._reconstructing = True
|
|
@@ -1615,10 +1734,16 @@ class Application:
|
|
|
1615
1734
|
asyncio.ensure_future(do_reconstruct())
|
|
1616
1735
|
else:
|
|
1617
1736
|
self._logger.info(f"Session {session.id} connected.")
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1737
|
+
async def do_connect():
|
|
1738
|
+
# Replay fonts before the user callback so any
|
|
1739
|
+
# widget the user builds with ``set_font(family,
|
|
1740
|
+
# ...)`` sees the face already declared.
|
|
1741
|
+
await self._replay_fonts_to_session(session)
|
|
1742
|
+
if self._on_connect:
|
|
1743
|
+
result = self._on_connect(session)
|
|
1744
|
+
if hasattr(result, "__await__"):
|
|
1745
|
+
await result
|
|
1746
|
+
asyncio.ensure_future(do_connect())
|
|
1622
1747
|
|
|
1623
1748
|
try:
|
|
1624
1749
|
async for message in ws:
|
|
@@ -1699,6 +1824,7 @@ class Application:
|
|
|
1699
1824
|
favicon_path = self._favicon_path
|
|
1700
1825
|
ws_host = self._host
|
|
1701
1826
|
ws_port = self._ws_port
|
|
1827
|
+
app = self
|
|
1702
1828
|
|
|
1703
1829
|
class Handler(SimpleHTTPRequestHandler):
|
|
1704
1830
|
def __init__(self, *a, **kw):
|
|
@@ -1707,6 +1833,27 @@ class Application:
|
|
|
1707
1833
|
def do_GET(self):
|
|
1708
1834
|
# Strip query string for path matching (e.g. /?session=1)
|
|
1709
1835
|
path = self.path.split("?")[0]
|
|
1836
|
+
# Custom-font registry: see sync.application for
|
|
1837
|
+
# the matching implementation.
|
|
1838
|
+
if path.startswith("/_pgwidgets/font/"):
|
|
1839
|
+
try:
|
|
1840
|
+
font_id = int(path.rsplit("/", 1)[-1])
|
|
1841
|
+
except ValueError:
|
|
1842
|
+
self.send_error(404)
|
|
1843
|
+
return
|
|
1844
|
+
data, mime = app._get_font_bytes(font_id)
|
|
1845
|
+
if data is None:
|
|
1846
|
+
self.send_error(404)
|
|
1847
|
+
return
|
|
1848
|
+
self.send_response(200)
|
|
1849
|
+
self.send_header("Content-Type", mime)
|
|
1850
|
+
self.send_header("Content-Length", str(len(data)))
|
|
1851
|
+
self.send_header(
|
|
1852
|
+
"Cache-Control", "public, max-age=31536000, immutable")
|
|
1853
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
1854
|
+
self.end_headers()
|
|
1855
|
+
self.wfile.write(data)
|
|
1856
|
+
return
|
|
1710
1857
|
if path == "/" or path == "/index.html":
|
|
1711
1858
|
html = remote_html.read_text(encoding="utf-8")
|
|
1712
1859
|
inject = (
|
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
|
@@ -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,141 @@ 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 _font_register_msg(self, entry):
|
|
1832
|
+
return {
|
|
1833
|
+
"type": "register-font",
|
|
1834
|
+
"id": entry["id"],
|
|
1835
|
+
"family": entry["family"],
|
|
1836
|
+
"weight": entry["weight"],
|
|
1837
|
+
"style": entry["style"],
|
|
1838
|
+
"url": f"/_pgwidgets/font/{entry['id']}",
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
def _font_default_msg(self):
|
|
1842
|
+
return {
|
|
1843
|
+
"type": "set-default-font",
|
|
1844
|
+
"font": self._default_font,
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
def _replay_fonts_to_session(self, session):
|
|
1848
|
+
"""Push the full font registry + default font to a freshly
|
|
1849
|
+
connected (or reconnecting) session before any user code
|
|
1850
|
+
creates widgets. Called from ``_on_session_open``."""
|
|
1851
|
+
with self._font_lock:
|
|
1852
|
+
fonts = list(self._fonts)
|
|
1853
|
+
default = self._default_font
|
|
1854
|
+
for entry in fonts:
|
|
1855
|
+
try:
|
|
1856
|
+
session._send(self._font_register_msg(entry))
|
|
1857
|
+
except Exception:
|
|
1858
|
+
pass
|
|
1859
|
+
if default is not None:
|
|
1860
|
+
try:
|
|
1861
|
+
session._send(self._font_default_msg())
|
|
1862
|
+
except Exception:
|
|
1863
|
+
pass
|
|
1864
|
+
|
|
1865
|
+
def _get_font_bytes(self, font_id):
|
|
1866
|
+
"""Return ``(bytes, mime)`` for a registered font, or
|
|
1867
|
+
``(None, None)`` if unknown. Called by the HTTP handler."""
|
|
1868
|
+
with self._font_lock:
|
|
1869
|
+
entry = self._fonts_by_id.get(font_id)
|
|
1870
|
+
if entry is None:
|
|
1871
|
+
return None, None
|
|
1872
|
+
return entry["bytes"], entry["mime"]
|
|
1873
|
+
|
|
1717
1874
|
def start(self):
|
|
1718
1875
|
"""Start the WebSocket server (and HTTP server if enabled).
|
|
1719
1876
|
|
|
@@ -1836,6 +1993,10 @@ class Application:
|
|
|
1836
1993
|
# because _send() blocks with event.wait() while the event
|
|
1837
1994
|
# loop that must process ws.send() is also blocked.
|
|
1838
1995
|
def do_reconstruct():
|
|
1996
|
+
# Replay the font registry before reconstruct() so any
|
|
1997
|
+
# widget reconstructed with ``set_font(...)`` finds the
|
|
1998
|
+
# face already declared.
|
|
1999
|
+
self._replay_fonts_to_session(session)
|
|
1839
2000
|
self._logger.info(
|
|
1840
2001
|
f"Session {session.id}: reconstructing UI.")
|
|
1841
2002
|
session._reconstructing = True
|
|
@@ -1848,8 +2009,14 @@ class Application:
|
|
|
1848
2009
|
self._dispatch(session, do_reconstruct, ())
|
|
1849
2010
|
else:
|
|
1850
2011
|
self._logger.info(f"Session {session.id} connected.")
|
|
1851
|
-
|
|
1852
|
-
|
|
2012
|
+
def do_connect():
|
|
2013
|
+
# Replay fonts before the user callback so any widget
|
|
2014
|
+
# the user builds with ``set_font(family, ...)`` sees
|
|
2015
|
+
# the face already declared.
|
|
2016
|
+
self._replay_fonts_to_session(session)
|
|
2017
|
+
if self._on_connect:
|
|
2018
|
+
self._on_connect(session)
|
|
2019
|
+
self._dispatch(session, do_connect, ())
|
|
1853
2020
|
|
|
1854
2021
|
try:
|
|
1855
2022
|
async for message in ws:
|
|
@@ -1942,6 +2109,7 @@ class Application:
|
|
|
1942
2109
|
favicon_path = self._favicon_path
|
|
1943
2110
|
ws_host = self._host
|
|
1944
2111
|
ws_port = self._ws_port
|
|
2112
|
+
app = self
|
|
1945
2113
|
|
|
1946
2114
|
class Handler(http.server.SimpleHTTPRequestHandler):
|
|
1947
2115
|
def __init__(self, *args, **kwargs):
|
|
@@ -1951,6 +2119,31 @@ class Application:
|
|
|
1951
2119
|
# serve remote.html at the root, with WS URL injected
|
|
1952
2120
|
# Strip query string for path matching (e.g. /?session=1)
|
|
1953
2121
|
path = self.path.split("?")[0]
|
|
2122
|
+
# Custom-font registry: /_pgwidgets/font/<id> serves
|
|
2123
|
+
# the bytes the app registered via ``register_font``.
|
|
2124
|
+
# ``Cache-Control: immutable`` is safe because the
|
|
2125
|
+
# registry assigns a fresh id on every call -- the
|
|
2126
|
+
# URL is content-stable for the lifetime of the
|
|
2127
|
+
# registration.
|
|
2128
|
+
if path.startswith("/_pgwidgets/font/"):
|
|
2129
|
+
try:
|
|
2130
|
+
font_id = int(path.rsplit("/", 1)[-1])
|
|
2131
|
+
except ValueError:
|
|
2132
|
+
self.send_error(404)
|
|
2133
|
+
return
|
|
2134
|
+
data, mime = app._get_font_bytes(font_id)
|
|
2135
|
+
if data is None:
|
|
2136
|
+
self.send_error(404)
|
|
2137
|
+
return
|
|
2138
|
+
self.send_response(200)
|
|
2139
|
+
self.send_header("Content-Type", mime)
|
|
2140
|
+
self.send_header("Content-Length", str(len(data)))
|
|
2141
|
+
self.send_header(
|
|
2142
|
+
"Cache-Control", "public, max-age=31536000, immutable")
|
|
2143
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
2144
|
+
self.end_headers()
|
|
2145
|
+
self.wfile.write(data)
|
|
2146
|
+
return
|
|
1954
2147
|
if path == "/" or path == "/index.html":
|
|
1955
2148
|
html = remote_html.read_text(encoding="utf-8")
|
|
1956
2149
|
inject = (
|
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=1gOTR8pdX_t7E_UEyUy4rnWiFoh10tJSUAINA1mE-SU,18298
|
|
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=5tUlfyE9ie0aFPh2WAs2VRvgyuqQmdaGTX0d1IkSB7o,80077
|
|
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=Gfft77F66PfRnr_8_FBqU0v_gWe4YC9M-q5rww1N-2U,94741
|
|
16
|
+
pgwidgets/sync/widget.py,sha256=yrScNkZB34u0JXn3-kCB-1mPLxLysVbuaPsCjC8kq0E,38343
|
|
17
|
+
pgwidgets_python-0.3.1.dist-info/licenses/LICENSE.md,sha256=LoM3fMTiMnQuHRCJghdjOtjnCrL8soBpu2PFk24Xvyg,1528
|
|
18
|
+
pgwidgets_python-0.3.1.dist-info/METADATA,sha256=QKWuFclVrksOLKxHPqSM-6CoNVskbt_bw_rqaUa3WpI,4568
|
|
19
|
+
pgwidgets_python-0.3.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
20
|
+
pgwidgets_python-0.3.1.dist-info/top_level.txt,sha256=wwL6fBq0gU-JwzlM6TdduY1qYpu39ysqnnbQT-1bqAs,10
|
|
21
|
+
pgwidgets_python-0.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|