Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH] Save workflow image to SVG #297

Merged
merged 3 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 97 additions & 7 deletions orangecanvas/application/canvasmain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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__)

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1491,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()
Expand All @@ -1503,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)
Expand Down Expand Up @@ -1798,6 +1815,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
"""
Expand Down
32 changes: 28 additions & 4 deletions orangecanvas/application/tests/test_mainwindow.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -173,10 +176,31 @@ 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
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"<svg", contents)

def test_save_swp(self):
w = self.w
Expand Down
9 changes: 7 additions & 2 deletions orangecanvas/canvas/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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)
Expand Down
Loading