From 73193373d6cf96aba413faaa9c422318d79d2d88 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Fri, 12 Apr 2024 12:31:19 +0200 Subject: [PATCH 1/3] canvasmain: Save/Export workflow image to SVG --- orangecanvas/application/canvasmain.py | 82 +++++++++++++++++++ .../application/tests/test_mainwindow.py | 20 ++++- 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/orangecanvas/application/canvasmain.py b/orangecanvas/application/canvasmain.py index 4ca6dad0..cc516d93 100644 --- a/orangecanvas/application/canvasmain.py +++ b/orangecanvas/application/canvasmain.py @@ -9,6 +9,7 @@ import io import traceback from concurrent import futures +from contextlib import contextmanager from xml.sax.saxutils import escape from functools import partial, reduce @@ -86,6 +87,7 @@ from .. import config from . import examples from ..resources import load_styled_svg_icon +from ..canvas import scene log = logging.getLogger(__name__) @@ -427,6 +429,12 @@ def setup_actions(self): triggered=self.save_scheme_as, shortcut=QKeySequence.SaveAs, ) + self.save_as_svg_action = QAction( + self.tr("Save Workflow Image as SVG ..."), self, + objectName="action-save-to-svg.", + toolTip=self.tr("Save workflow image as SVG."), + triggered=self.save_as_svg, + ) self.quit_action = QAction( self.tr("Quit"), self, objectName="quit-action", @@ -646,6 +654,7 @@ def setup_menu(self): sep.setObjectName("close-window-actions-separator") file_menu.addAction(self.save_action) file_menu.addAction(self.save_as_action) + file_menu.addAction(self.save_as_svg_action) sep = file_menu.addSeparator() sep.setObjectName("save-actions-separator") file_menu.addAction(self.show_properties_action) @@ -1798,6 +1807,79 @@ def load_diff(self, properties_and_commands): properties = properties_and_commands[0] document.restoreProperties(properties) + def _settings(self) -> QSettings: + s = QSettings() + s.beginGroup("mainwindow") + return s + + def save_as_svg(self): + settings = self._settings() + settings.beginGroup("save-as-svg-filedialog") + path = settings.value("path", defaultValue="", type=str) + if path: + directory = os.path.dirname(path) + else: + directory = user_documents_path() + document_path = self.current_document().path() + if document_path: + document_basename = os.path.basename(document_path) + basename, _ = os.path.splitext(document_basename) + basename = basename + ".svg" + else: + basename = self.tr("untitled.svg") + dialog = QFileDialog( + self, + acceptMode=QFileDialog.AcceptSave, + fileMode=QFileDialog.AnyFile, + directory=directory, + windowModality=Qt.WindowModal, + objectName="save-as-svg-filedialog", + ) + dialog.setAttribute(Qt.WA_DeleteOnClose) + dialog.setNameFilter(self.tr("Scalable Vector Graphics (*.svg)")) + dialog.selectFile(os.path.join(directory, basename)) + + def save(): + files = dialog.selectedFiles() + if files: + self.__save_as_svg(files[0]) + settings.setValue("path", files[0]) + + dialog.accepted.connect(save) + dialog.exec() + + def __save_as_svg(self, path): + doc = self.current_document() + content = scene.grab_svg(doc.scene()) + with self._handle_os_write_error(): + with open(path, "wt", encoding="utf-8") as f: + f.write(content) + + @contextmanager + def _handle_os_write_error(self): + try: + yield + except PermissionError as ex: + log.error("Write error", exc_info=True) + message_warning( + self.tr('"%(path)s" could not be saved. You do not ' + 'have write permissions (%(strerror)s).') % + {"path": ex.filename, "strerror": ex.strerror}, + title="", + informative_text=self.tr( + "Change the file system permissions or choose " + "another location."), + parent=self + ) + except OSError as ex: + log.error("Write error", exc_info=True) + message_warning( + self.tr('"%(path)s" could not be saved.') % + {"path": ex.filename}, + title="", + informative_text=ex.strerror + ) + def recent_scheme(self): # type: () -> int """ diff --git a/orangecanvas/application/tests/test_mainwindow.py b/orangecanvas/application/tests/test_mainwindow.py index 5082ac03..89472edb 100644 --- a/orangecanvas/application/tests/test_mainwindow.py +++ b/orangecanvas/application/tests/test_mainwindow.py @@ -1,9 +1,12 @@ +import io import os import tempfile from unittest.mock import patch from AnyQt.QtGui import QWhatsThisClickedEvent -from AnyQt.QtWidgets import QToolButton, QDialog, QMessageBox, QApplication +from AnyQt.QtWidgets import ( + QToolButton, QDialog, QMessageBox, QApplication, QFileDialog +) from .. import addons from ..outputview import TextStream @@ -178,6 +181,21 @@ def test_save(self): w.save_scheme() self.assertEqual(w.current_document().path(), self.filename) + def test_save_svg_image(self): + w = self.w + scheme = w.current_document().scheme() + scheme.load_from(io.BytesIO(TEST_OWS), registry=w.widget_registry) + with patch("AnyQt.QtWidgets.QFileDialog.exec"): + w.save_as_svg() + dialog = w.findChild(QFileDialog, "save-as-svg-filedialog") + dialog.setOption(QFileDialog.DontUseNativeDialog) + dialog.setOption(QFileDialog.DontConfirmOverwrite) + dialog.selectFile(self.filename) + dialog.accept() + with open(self.filename, "rb") as f: + contents = f.read() + self.assertIn(b" Date: Thu, 18 Apr 2024 19:54:15 +0200 Subject: [PATCH 2/3] scene: Match screen resolution in grab_svg if possible --- orangecanvas/canvas/scene.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/orangecanvas/canvas/scene.py b/orangecanvas/canvas/scene.py index a528516c..0c5a5aec 100644 --- a/orangecanvas/canvas/scene.py +++ b/orangecanvas/canvas/scene.py @@ -950,8 +950,7 @@ def metric(self, metric): _QSvgGenerator = QSvgGenerator # type: ignore -def grab_svg(scene): - # type: (QGraphicsScene) -> str +def grab_svg(scene: QGraphicsScene) -> str: """ Return a SVG rendering of the scene contents. @@ -962,6 +961,12 @@ def grab_svg(scene): """ svg_buffer = QBuffer() gen = _QSvgGenerator() + views = scene.views() + if views: + screen = views[0].screen() + if screen is not None: + res = screen.physicalDotsPerInch() + gen.setResolution(int(res)) gen.setOutputDevice(svg_buffer) items_rect = scene.itemsBoundingRect().adjusted(-10, -10, 10, 10) From db5216a2b3a7049d4b5642a9ee7ac9d5e70d86b8 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Fri, 19 Apr 2024 14:07:45 +0200 Subject: [PATCH 3/3] canvasmain: Use window modal file dialog for save workflow --- orangecanvas/application/canvasmain.py | 22 +++++++++++++------ .../application/tests/test_mainwindow.py | 12 +++++++--- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/orangecanvas/application/canvasmain.py b/orangecanvas/application/canvasmain.py index cc516d93..6f781b18 100644 --- a/orangecanvas/application/canvasmain.py +++ b/orangecanvas/application/canvasmain.py @@ -1500,8 +1500,7 @@ def save_scheme_as(self): curr_scheme = document.scheme() assert curr_scheme is not None title = self.__title_for_scheme(curr_scheme) - settings = QSettings() - settings.beginGroup("mainwindow") + settings = self._settings() if document.path(): start_dir = document.path() @@ -1512,12 +1511,21 @@ def save_scheme_as(self): start_dir = os.path.join(start_dir, title + ".ows") - filename, _ = QFileDialog.getSaveFileName( - self, self.tr("Save Orange Workflow File"), - start_dir, self.tr("Orange Workflow (*.ows)") + dialog = QFileDialog( + self, + windowTitle=self.tr("Save Orange Workflow File"), + directory=start_dir, + fileMode=QFileDialog.AnyFile, + acceptMode=QFileDialog.AcceptSave, + windowModality=Qt.WindowModal, + objectName="save-as-ows-filedialog", ) - - if filename: + dialog.setNameFilter(self.tr("Orange Workflow (*.ows)")) + dialog.exec() + files = dialog.selectedFiles() + dialog.deleteLater() + if files: + filename = files[0] settings.setValue("last-scheme-dir", os.path.dirname(filename)) if self.save_scheme_to(curr_scheme, filename): document.setPath(filename) diff --git a/orangecanvas/application/tests/test_mainwindow.py b/orangecanvas/application/tests/test_mainwindow.py index 89472edb..927257c4 100644 --- a/orangecanvas/application/tests/test_mainwindow.py +++ b/orangecanvas/application/tests/test_mainwindow.py @@ -176,10 +176,16 @@ def test_save(self): f.assert_not_called() w.current_document().setPath("") - with patch("AnyQt.QtWidgets.QFileDialog.getSaveFileName", - return_value=(self.filename, "")) as f: + + def exec(myself): + myself.setOption(QFileDialog.DontUseNativeDialog) + myself.setOption(QFileDialog.DontConfirmOverwrite) + myself.selectFile(self.filename) + myself.accept() + + with patch("AnyQt.QtWidgets.QFileDialog.exec", exec): w.save_scheme() - self.assertEqual(w.current_document().path(), self.filename) + self.assertTrue(os.path.samefile(w.current_document().path(), self.filename)) def test_save_svg_image(self): w = self.w