diff --git a/.gitignore b/.gitignore index 2c093ca3..dfd41b2e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ dist *.qasm .env *.zxr +.ipynb_checkpoints diff --git a/embed_zxlive_demo.ipynb b/embed_zxlive_demo.ipynb new file mode 100644 index 00000000..86792d0c --- /dev/null +++ b/embed_zxlive_demo.ipynb @@ -0,0 +1,80 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "dc0b153f-dc2f-447e-82de-6e71b9d389d2", + "metadata": {}, + "source": [ + "# Demo of embedded ZX Live running inside Jupyter Notebook\n", + "\n", + "First, run the cell below. An instance of ZX Live will open, with two identical graphs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e45e8da-f33b-4d13-8327-e394ae096ed9", + "metadata": {}, + "outputs": [], + "source": [ + "%gui qt6\n", + "from PySide6.QtWidgets import QApplication, QWidget\n", + "from zxlive import app\n", + "\n", + "import pyzx as zx\n", + "\n", + "g1 = zx.Graph()\n", + "g1.add_vertex(zx.VertexType.Z, 0, 0)\n", + "g1.add_vertex(zx.VertexType.X, 0, 1)\n", + "g1.add_edge((0, 1))\n", + "g2 = g1.clone()\n", + "zx.draw(g1)\n", + "zx.draw(g2)\n", + "\n", + "zxl = app.get_embedded_app()\n", + "zxl.edit_graph(g1, 'g1')\n", + "zxl.edit_graph(g2, 'g2')" + ] + }, + { + "cell_type": "markdown", + "id": "88f425a0-d50d-40e0-86cf-eecbc3a27277", + "metadata": {}, + "source": [ + "After making some edits and saving them from within ZX Live, run the following cell to see the changes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a0a8a17-1795-4b13-aedf-30b346388e23", + "metadata": {}, + "outputs": [], + "source": [ + "zx.draw(g1)\n", + "zx.draw(g2)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/zxlive/app.py b/zxlive/app.py index b586c052..cec34799 100644 --- a/zxlive/app.py +++ b/zxlive/app.py @@ -19,6 +19,8 @@ from PySide6.QtCore import QCommandLineParser import sys from .mainwindow import MainWindow +from .common import GraphT +from typing import Optional sys.path.insert(0, '../pyzx') # So that it can find a local copy of pyzx @@ -29,6 +31,9 @@ class ZXLive(QApplication): ... """ + main_window: Optional[MainWindow] = None + is_embedded: bool = False + def __init__(self) -> None: super().__init__(sys.argv) self.setApplicationName('ZX Live') @@ -47,9 +52,24 @@ def __init__(self) -> None: for f in parser.positionalArguments(): self.main_window.open_file_from_path(f) + def edit_graph(self, g: GraphT, name: Optional[str] = "embedded graph") -> None: + """Opens a ZX Live window from within a notebook to edit a graph.""" + assert self.is_embedded + if not self.main_window: + self.main_window = MainWindow(True) + self.main_window.show() + self.main_window.new_graph(g, name) -def main() -> None: - """Main entry point for ZX Live""" +def get_embedded_app() -> ZXLive: + """Main entry point for ZX Live as an embedded app inside a jupyter notebook.""" + app = QApplication.instance() or ZXLive() + app.__class__ = ZXLive + app.is_embedded = True + return app + + +def main() -> None: + """Main entry point for ZX Live as a standalone app.""" zxl = ZXLive() zxl.exec_() diff --git a/zxlive/mainwindow.py b/zxlive/mainwindow.py index 17ec7940..aaa12d11 100644 --- a/zxlive/mainwindow.py +++ b/zxlive/mainwindow.py @@ -55,12 +55,14 @@ class MainWindow(QMainWindow): rewrite_form: QFormLayout left_graph: Optional[GraphT] right_graph: Optional[GraphT] + embedded_graph: Optional[GraphT] - def __init__(self) -> None: + def __init__(self, is_embedded: bool = False) -> None: super().__init__() self.settings = QSettings("zxlive", "zxlive") self.setWindowTitle("zxlive") + self.is_embedded = is_embedded w = QWidget(self) w.setLayout(QVBoxLayout()) @@ -89,15 +91,21 @@ def __init__(self) -> None: menu = self.menuBar() new_graph = self._new_action("&New", self.new_graph, QKeySequence.StandardKey.New, - "Create a new tab with an empty graph", alt_shortcut = QKeySequence.StandardKey.AddTab) + "Create a new tab with an empty graph", alt_shortcut=QKeySequence.StandardKey.AddTab) open_file = self._new_action("&Open...", self.open_file, QKeySequence.StandardKey.Open, "Open a file-picker dialog to choose a new diagram") self.close_action = self._new_action("Close", self.handle_close_action, QKeySequence.StandardKey.Close, - "Closes the window", alt_shortcut = QKeySequence("Ctrl+W")) - # TODO: We should remember if we have saved the diagram before, - # and give an open to overwrite this file with a Save action - self.save_file = self._new_action("&Save", self.handle_save_file_action, QKeySequence.StandardKey.Save, - "Save the diagram by overwriting the previous loaded file.") + "Closes the window", alt_shortcut=QKeySequence("Ctrl+W")) + if not self.is_embedded: + # TODO: We should remember if we have saved the diagram before, + # and give an option to overwrite this file with a Save action. + self.save_diagram = self._new_action("&Save", self.handle_save_file_action, + QKeySequence.StandardKey.Save, + "Save the diagram by overwriting the previous loaded file.") + else: + self.save_diagram = self._new_action("&Save to notebook", self.handle_save_embedded_graph_action, + QKeySequence.StandardKey.Save, + "Save the diagram back to the notebook.") self.save_as = self._new_action("Save &as...", self.handle_save_as_action, QKeySequence.StandardKey.SaveAs, "Opens a file-picker dialog to save the diagram in a chosen file format") @@ -106,7 +114,7 @@ def __init__(self) -> None: file_menu.addAction(open_file) file_menu.addSeparator() file_menu.addAction(self.close_action) - file_menu.addAction(self.save_file) + file_menu.addAction(self.save_diagram) file_menu.addAction(self.save_as) self.undo_action = self._new_action("Undo", self.undo, QKeySequence.StandardKey.Undo, @@ -170,11 +178,12 @@ def __init__(self) -> None: self.simplify_menu.addAction(action) self.simplify_menu.menuAction().setVisible(False) - graph = construct_circuit() - self.new_graph(graph) + if not self.is_embedded: + graph = construct_circuit() + self.new_graph(graph) def _reset_menus(self, has_active_tab: bool) -> None: - self.save_file.setEnabled(has_active_tab) + self.save_diagram.setEnabled(has_active_tab) self.save_as.setEnabled(has_active_tab) self.cut_action.setEnabled(has_active_tab) self.copy_action.setEnabled(has_active_tab) @@ -376,6 +385,12 @@ def handle_save_as_action(self) -> bool: self.tab_widget.setTabText(i,name) return True + def handle_save_embedded_graph_action(self) -> bool: + assert self.is_embedded and self.embedded_graph is not None + assert self.active_panel is not None and isinstance(self.active_panel, GraphEditPanel) + self.embedded_graph.__dict__.update(self.active_panel.graph.__dict__) + self.active_panel.undo_stack.setClean() + return False def cut_graph(self) -> None: assert self.active_panel is not None @@ -415,6 +430,9 @@ def new_graph(self, graph: Optional[GraphT] = None, name: Optional[str] = None) panel = GraphEditPanel(_graph, self.undo_action, self.redo_action) panel.start_derivation_signal.connect(self.new_deriv) if name is None: name = "New Graph" + if self.is_embedded: + assert graph is not None + self.embedded_graph = graph self._new_panel(panel, name) def new_rule_editor(self, rule: Optional[CustomRule] = None, name: Optional[str] = None) -> None: