Skip to content

Commit 5b0d1f6

Browse files
committed
feat: wire TikZ export button through anywidget comm channel
- Add Python-side _on_custom_msg handler that receives export_tikz request from JS, generates .tex content, and sends it back as tikz_data message (avoids server-side file paths) - Standalone mode: disable TikZ button with tooltip; SVG still works - Jupyter mode: clicking Export TikZ triggers model.send() → Python generates content → JS receives blob → browser downloads .tex - 139 passed, 5 skipped; build + twine check pass
1 parent e8fff92 commit 5b0d1f6

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

src/tvb_phaseplane/static/widget.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15843,10 +15843,31 @@ export function render({ model, el }) {
1584315843
editorApplyBtn.addEventListener("click", applyEditor);
1584415844
editorCopyBtn.addEventListener("click", copySpec);
1584515845
el.querySelector(".ppw-export-svg").addEventListener("click", exportToSVG);
15846-
el.querySelector(".ppw-export-tikz").addEventListener("click", () => {
15846+
15847+
// TikZ export requires a live Python kernel; disabled in standalone.
15848+
const tikzBtn = el.querySelector(".ppw-export-tikz");
15849+
if (isStandalone) {
15850+
tikzBtn.disabled = true;
15851+
tikzBtn.title = "TikZ export requires a Jupyter kernel";
15852+
}
15853+
tikzBtn.addEventListener("click", () => {
15854+
if (isStandalone) {
15855+
alert("TikZ export requires a Jupyter kernel. Use widget.export_tikz('file.tex') in a notebook cell instead.");
15856+
return;
15857+
}
1584715858
model.send({ type: "export_tikz" });
1584815859
});
1584915860

15861+
// Listen for TikZ data coming back from the Python kernel
15862+
if (!isStandalone) {
15863+
model.on('msg:custom', (msg) => {
15864+
if (msg && msg.type === 'tikz_data') {
15865+
const blob = new Blob([msg.content], { type: 'application/x-tex' });
15866+
downloadBlob(blob, msg.filename || 'phase_plane.tex');
15867+
}
15868+
});
15869+
}
15870+
1585015871
// ═════════════════════════════════════════════════════════════
1585115872
// INITIALISATION
1585215873
// ═════════════════════════════════════════════════════════════

src/tvb_phaseplane/widget.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
"""
88

99
import json
10+
import os
1011
import pathlib
12+
import tempfile
1113

1214
import anywidget
1315
import numpy as np
@@ -98,6 +100,8 @@ def __init__(self, model=None, **kwargs):
98100
kwargs.setdefault("model_name", model.name)
99101
super().__init__(**kwargs)
100102
self._update_model()
103+
# Register handler for custom JS → Python messages (e.g. TikZ export)
104+
self.on_msg(self._on_custom_msg)
101105

102106
def _get_model(self):
103107
from .models import MODEL_REGISTRY
@@ -481,3 +485,24 @@ def export_tikz(self, filename: str | pathlib.Path = "phase_plane.tex"):
481485
)
482486
filename.write_text(tex, encoding="utf-8")
483487
return str(filename)
488+
489+
# ── Custom message handler (JS → Python) ──
490+
491+
def _on_custom_msg(self, _widget, content, buffers):
492+
"""Handle messages from the JS front-end."""
493+
msg_type = content.get("type")
494+
if msg_type == "export_tikz":
495+
fd, tmppath = tempfile.mkstemp(suffix=".tex", prefix="phase_plane_")
496+
os.close(fd)
497+
try:
498+
self.export_tikz(tmppath)
499+
tex_content = pathlib.Path(tmppath).read_text(encoding="utf-8")
500+
self.send(
501+
{
502+
"type": "tikz_data",
503+
"content": tex_content,
504+
"filename": "phase_plane.tex",
505+
}
506+
)
507+
finally:
508+
os.unlink(tmppath)

0 commit comments

Comments
 (0)