Skip to content

Commit

Permalink
Merge pull request #297 from ales-erjavec/svg-save
Browse files Browse the repository at this point in the history
[ENH] Save workflow image to SVG
  • Loading branch information
janezd committed May 13, 2024
2 parents 8b47de1 + db5216a commit 6967b60
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 13 deletions.
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

0 comments on commit 6967b60

Please sign in to comment.