diff --git a/MANIFEST.in b/MANIFEST.in index 76c2ca18..c18e721a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ -include zxlive/icons/*.svg \ No newline at end of file +include zxlive/icons/*.svg +include zxlive/tooltips/*.png \ No newline at end of file diff --git a/zxlive/app.py b/zxlive/app.py index 051c4806..11a423a9 100644 --- a/zxlive/app.py +++ b/zxlive/app.py @@ -35,7 +35,6 @@ myappid = 'zxcalc.zxlive.zxlive.1.0.0' # arbitrary string ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) # type: ignore - class ZXLive(QApplication): """The main ZXLive application diff --git a/zxlive/custom_rule.py b/zxlive/custom_rule.py index 35d43921..14f68e7b 100644 --- a/zxlive/custom_rule.py +++ b/zxlive/custom_rule.py @@ -186,7 +186,9 @@ def from_json(cls, json_str: str) -> "CustomRule": def to_rewrite_data(self) -> "RewriteData": from .rewrite_data import MATCHES_VERTICES return {"text": self.name, "matcher": self.matcher, "rule": self, "type": MATCHES_VERTICES, - "tooltip": self.description, 'copy_first': False, 'returns_new_graph': False} + "tooltip": self.description, 'copy_first': False, 'returns_new_graph': False, + "custom_rule": True, "lhs": self.lhs_graph, "rhs": self.rhs_graph} + def is_rewrite_unfusable(lhs_graph: GraphT) -> bool: diff --git a/zxlive/graphview.py b/zxlive/graphview.py index 881252fa..3ae6fbb7 100644 --- a/zxlive/graphview.py +++ b/zxlive/graphview.py @@ -65,11 +65,12 @@ def __init__(self, start: QPointF, shift: bool = False) -> None: class GraphView(QGraphicsView): """QtWidget containing a graph - This widget is view associated with a graph. However, most of the + This widget is the view associated with a graph. However, most of the interesting stuff happens in `GraphScene`. """ wand_trace_finished = Signal(object) + draw_background_lines = True def __init__(self, graph_scene: GraphScene) -> None: self.graph_scene = graph_scene @@ -267,6 +268,7 @@ def drawBackground(self, painter: QPainter, rect: QRectF | QRect) -> None: painter.setBrush(QColor(255, 255, 255, 255)) painter.setPen(QPen(Qt.PenStyle.NoPen)) painter.drawRect(rect) + if not self.draw_background_lines: return # Calculate grid lines lines, thick_lines = [], [] diff --git a/zxlive/rewrite_action.py b/zxlive/rewrite_action.py index 183a9373..4ca0c0d4 100644 --- a/zxlive/rewrite_action.py +++ b/zxlive/rewrite_action.py @@ -3,16 +3,24 @@ import copy from dataclasses import dataclass, field from typing import Callable, TYPE_CHECKING, Any, cast, Union +from concurrent.futures import ThreadPoolExecutor import pyzx -from PySide6.QtCore import Qt, QAbstractItemModel, QModelIndex, QPersistentModelIndex, Signal, QObject, QMetaObject -from concurrent.futures import ThreadPoolExecutor + +from PySide6.QtCore import (Qt, QAbstractItemModel, QModelIndex, QPersistentModelIndex, + Signal, QObject, QMetaObject, QIODevice, QBuffer, QPoint, QPointF, QLineF) +from PySide6.QtGui import QPixmap, QColor, QPen +from PySide6.QtWidgets import QGraphicsView, QGraphicsScene + from .animations import make_animation from .commands import AddRewriteStep -from .common import ET, GraphT, VT +from .common import ET, GraphT, VT, get_data from .dialogs import show_error_msg from .rewrite_data import is_rewrite_data, RewriteData, MatchType, MATCHES_VERTICES +from .settings import display_setting +from .graphscene import GraphScene +from .graphview import GraphView if TYPE_CHECKING: from .proof_panel import ProofPanel @@ -36,12 +44,57 @@ class RewriteAction: @classmethod def from_rewrite_data(cls, d: RewriteData) -> RewriteAction: + if display_setting.PREVIEWS_SHOW and ('picture' in d or 'custom_rule' in d): + if 'custom_rule' in d: + # We will create a custom tooltip picture representing the custom rewrite + graph_scene_left = GraphScene() + graph_scene_right = GraphScene() + graph_view_left = GraphView(graph_scene_left) + graph_view_left.draw_background_lines = False + graph_view_left.set_graph(d['lhs']) + graph_view_right = GraphView(graph_scene_right) + graph_view_right.draw_background_lines = False + graph_view_right.set_graph(d['rhs']) + graph_view_left.fit_view() + graph_view_right.fit_view() + graph_view_left.setSceneRect(graph_scene_left.itemsBoundingRect()) + graph_view_right.setSceneRect(graph_scene_right.itemsBoundingRect()) + lhs_size = graph_view_left.viewport().size() + rhs_size = graph_view_right.viewport().size() + # The picture needs to be wide enough to fit both of them and have some space for the = sign + pixmap = QPixmap(lhs_size.width()+rhs_size.width()+160,max(lhs_size.height(),rhs_size.height())) + pixmap.fill(QColor("#ffffff")) + graph_view_left.viewport().render(pixmap) + graph_view_right.viewport().render(pixmap,QPoint(lhs_size.width()+160,0)) + # We create a new scene to render the = sign + new_scene = GraphScene() + new_view = GraphView(new_scene) + new_view.draw_background_lines = False + new_scene.addLine(QLineF(QPointF(10,40),QPointF(80,40)),QPen(QColor("#000000"),8)) + new_scene.addLine(QLineF(QPointF(10,10),QPointF(80,10)),QPen(QColor("#000000"),8)) + new_view.setSceneRect(new_scene.itemsBoundingRect()) + new_view.viewport().render(pixmap,QPoint(lhs_size.width(),max(lhs_size.height(),rhs_size.height())/2-20)) + + buffer = QBuffer() + buffer.open(QIODevice.WriteOnly) + pixmap.save(buffer, "PNG", quality=100) + image = bytes(buffer.data().toBase64()).decode() + else: + pixmap = QPixmap() + pixmap.load(get_data("tooltips/"+d['picture'])) + buffer = QBuffer() + buffer.open(QIODevice.WriteOnly) + pixmap.save(buffer, "PNG", quality=100) + image = bytes(buffer.data().toBase64()).decode() + tooltip = ''.format(image) + d['tooltip'] + else: + tooltip = d['tooltip'] return cls( name=d['text'], matcher=d['matcher'], rule=d['rule'], match_type=d['type'], - tooltip=d['tooltip'], + tooltip=tooltip, copy_first=d.get('copy_first', False), returns_new_graph=d.get('returns_new_graph', False), ) diff --git a/zxlive/rewrite_data.py b/zxlive/rewrite_data.py index 2c528a8f..fcbc8113 100644 --- a/zxlive/rewrite_data.py +++ b/zxlive/rewrite_data.py @@ -28,6 +28,10 @@ class RewriteData(TypedDict): tooltip: str copy_first: NotRequired[bool] returns_new_graph: NotRequired[bool] + picture: NotRequired[str] + custom_rule: NotRequired[bool] + lhs: NotRequired[GraphT] + rhs: NotRequired[GraphT] def is_rewrite_data(d: dict) -> bool: @@ -46,7 +50,6 @@ def read_custom_rules() -> list[RewriteData]: custom_rules.append(rule) return custom_rules - # We want additional actions that are not part of the original PyZX editor # So we add them to operations @@ -57,7 +60,8 @@ def read_custom_rules() -> list[RewriteData]: "matcher": pyzx.rules.match_lcomp_parallel, "rule": pyzx.rules.lcomp, "type": MATCHES_VERTICES, - "copy_first": True + "copy_first": True, + "picture": "lcomp.png" }, "pivot": { "text": "pivot", @@ -65,7 +69,8 @@ def read_custom_rules() -> list[RewriteData]: "matcher": lambda g, matchf: pyzx.rules.match_pivot_parallel(g, matchf, check_edge_types=True), "rule": pyzx.rules.pivot, "type": MATCHES_EDGES, - "copy_first": True + "copy_first": True, + "picture": "pivot_regular.png" }, "pivot_boundary": { "text": "boundary pivot", @@ -81,7 +86,8 @@ def read_custom_rules() -> list[RewriteData]: "matcher": pyzx.rules.match_pivot_gadget, "rule": pyzx.rules.pivot, "type": MATCHES_EDGES, - "copy_first": True + "copy_first": True, + "picture": "pivot_gadget.png" }, "phase_gadget_fuse": { "text": "Fuse phase gadgets", @@ -89,7 +95,8 @@ def read_custom_rules() -> list[RewriteData]: "matcher": pyzx.rules.match_phase_gadgets, "rule": pyzx.rules.merge_phase_gadgets, "type": MATCHES_VERTICES, - "copy_first": True + "copy_first": True, + "picture": "gadget_fuse.png" }, "supplementarity": { "text": "Supplementarity", @@ -127,7 +134,7 @@ def ocm_rule(_graph: GraphT, _matches: list) -> pyzx.rules.RewriteOutputType[VT, ocm_action: RewriteData = { "text": "OCM", - "tooltip": "Saves the graph with the current vertex positions", + "tooltip": "Only Connectivity Matters. Saves the graph with the current vertex positions", "matcher": const_true, "rule": ocm_rule, "type": MATCHES_VERTICES, @@ -271,6 +278,9 @@ def ocm_rule(_graph: GraphT, _matches: list) -> pyzx.rules.RewriteOutputType[VT, } rules_basic = {"spider", "to_z", "to_x", "rem_id", "copy", "pauli", "bialgebra", "euler"} +operations["pauli"]["picture"] = "push_pauli.png" +operations["copy"]["picture"] = "copy_pi.png" +operations["bialgebra"]["picture"] = "bialgebra.png" rules_zxw = {"spider", "fuse_w", "z_to_z_box"} diff --git a/zxlive/settings.py b/zxlive/settings.py index e6c749a6..ec0e2d5b 100644 --- a/zxlive/settings.py +++ b/zxlive/settings.py @@ -34,6 +34,7 @@ class ColorScheme(TypedDict): "tab-bar-location": QTabWidget.TabPosition.North, "snap-granularity": '4', "input-circuit-format": 'openqasm', + "previews-show": True, 'sound-effects': False, } @@ -185,6 +186,7 @@ def _get_synonyms(key: str, default: list[str]) -> list[str]: class DisplaySettings: SNAP_DIVISION = 4 # Should be an integer dividing SCALE + PREVIEWS_SHOW = True def __init__(self) -> None: self.colors = color_schemes[str(settings.value("color-scheme"))] @@ -200,6 +202,8 @@ def update(self) -> None: get_settings_value("font/size", int) ) self.SNAP = SCALE / self.SNAP_DIVISION + self.PREVIEWS_SHOW = get_settings_value("previews-show",bool) + self.PREVIEWS_SHOW = True # Initialise settings diff --git a/zxlive/settings_dialog.py b/zxlive/settings_dialog.py index dc1008d2..de12a9f9 100644 --- a/zxlive/settings_dialog.py +++ b/zxlive/settings_dialog.py @@ -72,13 +72,13 @@ class SettingsData(TypedDict): 'sqasm-no-simplification': "Spider QASM (no simplification)", } - general_settings: list[SettingsData] = [ {"id": "path/custom-rules", "label": "Custom rules path", "type": FormInputType.Folder}, {"id": "color-scheme", "label": "Color scheme", "type": FormInputType.Combo, "data": color_scheme_data}, {"id": "tab-bar-location", "label": "Tab bar location", "type": FormInputType.Combo, "data": tab_positioning_data}, {"id": "snap-granularity", "label": "Snap-to-grid granularity", "type": FormInputType.Combo, "data": snap_to_grid_data}, {"id": "input-circuit-format", "label": "Input Circuit as", "type": FormInputType.Combo, "data": input_circuit_formats}, + {"id": "previews-show", "label": "Show rewrite previews","type": FormInputType.Bool}, {"id": "sound-effects", "label": "Sound Effects", "type": FormInputType.Bool}, ] @@ -277,6 +277,8 @@ def update_global_settings(self) -> None: self.settings.setValue(name, widget.value()) elif isinstance(widget, QComboBox): self.settings.setValue(name, widget.currentData()) + elif isinstance(widget, QCheckBox): + self.settings.setValue(name, widget.isChecked()) display_setting.update() def apply_global_settings(self) -> None: diff --git a/zxlive/tooltips/bialgebra.png b/zxlive/tooltips/bialgebra.png new file mode 100644 index 00000000..67593921 Binary files /dev/null and b/zxlive/tooltips/bialgebra.png differ diff --git a/zxlive/tooltips/copy_pi.png b/zxlive/tooltips/copy_pi.png new file mode 100644 index 00000000..f7dc8f96 Binary files /dev/null and b/zxlive/tooltips/copy_pi.png differ diff --git a/zxlive/tooltips/gadget_fuse.png b/zxlive/tooltips/gadget_fuse.png new file mode 100644 index 00000000..45471803 Binary files /dev/null and b/zxlive/tooltips/gadget_fuse.png differ diff --git a/zxlive/tooltips/lcomp.png b/zxlive/tooltips/lcomp.png new file mode 100644 index 00000000..91c31f43 Binary files /dev/null and b/zxlive/tooltips/lcomp.png differ diff --git a/zxlive/tooltips/pivot_gadget.png b/zxlive/tooltips/pivot_gadget.png new file mode 100644 index 00000000..d6193921 Binary files /dev/null and b/zxlive/tooltips/pivot_gadget.png differ diff --git a/zxlive/tooltips/pivot_regular.png b/zxlive/tooltips/pivot_regular.png new file mode 100644 index 00000000..65f47f08 Binary files /dev/null and b/zxlive/tooltips/pivot_regular.png differ diff --git a/zxlive/tooltips/push_pauli.png b/zxlive/tooltips/push_pauli.png new file mode 100644 index 00000000..1ff7dc08 Binary files /dev/null and b/zxlive/tooltips/push_pauli.png differ