Source code for qucat._web_gui

import json
import os
import uuid

# jscircuit: a browser-based circuit editor producing netlists in the
# same format as qucat's native tkinter GUI (see qucat.GUI).
# https://github.com/qucat/jscircuit
JSCIRCUIT_URL = "https://qucat.github.io/jscircuit/app/jscircuit.html"


[docs] def web_gui(filename=None, width=700, height=500): r'''Opens a browser-based circuit editor inside a Jupyter notebook cell. This is an alternative to :class:`qucat.GUI` for situations where the native tkinter interface is unavailable or impractical, for example when working on a remote server or inside JupyterHub. It embeds `jscircuit <https://github.com/qucat/jscircuit>`_, a browser-based circuit editor, together with a small text editor used to save the resulting netlist to a file. Parameters ---------- filename: string, optional Path to the file the netlist will be saved to. If the file already exists, its contents are loaded into the text editor, and the circuit is also loaded into the jscircuit editor itself. If not given, the file name can instead be typed into the text editor once the cell has been run. width: int, optional Width in pixels of the jscircuit editor (default 700). The editor can also be resized by dragging its bottom-right corner. height: int, optional Height in pixels of the jscircuit editor (default 500). Returns ------- None Notes ----- This function does not return a :class:`qucat.Qcircuit`: since the circuit is drawn in the browser, the netlist only becomes available once the user has saved it, which happens after this function returns. The typical workflow is thus split over two cells: 1. Call ``qucat.web_gui(filename)`` in one cell, draw the circuit in the jscircuit editor, copy the resulting netlist, paste it into the text box below the editor, then click "Save". 2. In a later cell, load the saved netlist with ``circuit = qucat.GUI(filename, edit=False)``. Loading an existing netlist into the jscircuit editor relies on jscircuit only accepting such requests from a same-origin, ``localhost``, or ``*.github.io`` parent page (a restriction of jscircuit itself, for security). This holds for a notebook running on your own machine, but on a remote JupyterHub reached through a different domain, the circuit will not auto-load: the netlist is still pre-filled in the text box below the editor, from where it can be pasted into jscircuit manually (Ctrl+V). Requires the optional ``ipywidgets`` dependency, install it with ``pip install ipywidgets`` or ``pip install qucat[notebook]``. ''' try: import ipywidgets as widgets from IPython.display import display, HTML except ImportError as e: raise ImportError( "qucat.web_gui requires the 'ipywidgets' package, which is " "not installed.\nInstall it with:\n\n" " pip install ipywidgets\n\n" "or install qucat with the 'notebook' extra:\n\n" " pip install qucat[notebook]" ) from e # Disable the native drag-to-resize handle on the textarea, it is # redundant with the resize handle of the jscircuit editor below. display(HTML("<style>textarea { resize: none !important; }</style>")) initial_text = "" if filename is not None and os.path.isfile(filename): with open(filename, "r", encoding="utf-8") as f: initial_text = f.read() container_id = "qucat-web-gui-%s" % uuid.uuid4().hex # jscircuit posts {type: 'appReady'} to its parent window once loaded, # and after that accepts {type: 'loadCircuit', netlist: <string>} to # populate the editor. We instead trigger on the iframe's own 'load' # event, which (since main.js is a module script) only fires after # jscircuit has already sent 'appReady', so the app is guaranteed to # be ready to receive the netlist by then. autoload_script = "" if initial_text: # Escape "</" so the embedded JSON can't prematurely close the # surrounding <script> tag if the netlist ever contained it. netlist_json = json.dumps(initial_text).replace("</", "<\\/") autoload_script = ''' <script> (function() { var iframe = document.getElementById("%s").querySelector("iframe"); iframe.addEventListener("load", function() { iframe.contentWindow.postMessage( {type: "loadCircuit", netlist: %s}, "*"); }); })(); </script> ''' % (container_id, netlist_json) display(HTML(''' <div id="{container_id}" style=" resize: both; overflow: hidden; width: {width}px; height: {height}px; min-width: 200px; min-height: 150px; border: 1px solid #ccc; box-sizing: border-box; "> <iframe src="{url}" allow="clipboard-write; clipboard-read" width="100%" height="100%" style="border: none; display: block;"> </iframe> </div> {autoload_script} '''.format(container_id=container_id, width=width, height=height, url=JSCIRCUIT_URL, autoload_script=autoload_script))) text_area = widgets.Textarea( value=initial_text, placeholder="Paste the netlist copied from jscircuit here...", layout=widgets.Layout(width="500px", height="200px"), ) filename_input = widgets.Text( value=filename or "", placeholder="e.g. netlist.txt", description="File name:", layout=widgets.Layout(width="300px"), ) save_button = widgets.Button( description="Save", button_style="primary", icon="save", ) status_label = widgets.Label(value="") def on_save_clicked(b): fname = filename_input.value.strip() content = text_area.value if not fname: status_label.value = "Please enter a file name." return try: directory = os.path.dirname(fname) if directory: os.makedirs(directory, exist_ok=True) with open(fname, "w", encoding="utf-8") as f: f.write(content) status_label.value = "Saved to '%s' (%d chars)" % (fname, len(content)) except Exception as e: status_label.value = "Error: %s" % e save_button.on_click(on_save_clicked) ui = widgets.VBox([ widgets.Label("Netlist"), text_area, widgets.HBox([filename_input, save_button]), status_label, ]) display(ui)