Skip to content
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
15 changes: 15 additions & 0 deletions openmc_plotter/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ def createMenuBar(self):
save_image_connector = partial(self.saveImage, filename=None)
self.saveImageAction.triggered.connect(save_image_connector)

self.copyImageAction = QAction("&Copy Image", self)
self.copyImageAction.setShortcut("Ctrl+Shift+C")
self.copyImageAction.setToolTip('Copy plot image to clipboard')
self.copyImageAction.setStatusTip('Copy plot image to clipboard')
self.copyImageAction.triggered.connect(self.copyImageToClipboard)

self.saveViewAction = QAction("Save &View...", self)
self.saveViewAction.setShortcut(QtGui.QKeySequence.Save)
self.saveViewAction.setStatusTip('Save current view settings')
Expand Down Expand Up @@ -199,6 +205,7 @@ def createMenuBar(self):

self.fileMenu = self.mainMenu.addMenu('&File')
self.fileMenu.addAction(self.reloadModelAction)
self.fileMenu.addAction(self.copyImageAction)
self.fileMenu.addAction(self.saveImageAction)
self.fileMenu.addAction(self.exportDataAction)
self.fileMenu.addSeparator()
Expand Down Expand Up @@ -519,6 +526,14 @@ def saveImage(self, filename=None):
self.plotIm.saveImage(filename)
self.statusBar().showMessage('Plot Image Saved', 5000)

def copyImageToClipboard(self):
if self.plotIm.copyImageToClipboard():
self.statusBar().showMessage('Plot Image Copied', 5000)
return True

self.statusBar().showMessage('No Plot Image Available', 5000)
return False

def saveView(self):
filename, ext = QFileDialog.getSaveFileName(self,
"Save View Settings",
Expand Down
56 changes: 56 additions & 0 deletions openmc_plotter/plotgui.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import io
from functools import partial

from PySide6 import QtCore, QtGui
Expand Down Expand Up @@ -180,6 +181,60 @@ def saveImage(self, filename):
filename += ".png"
self.figure.savefig(filename, transparent=True)

def copyImageToClipboard(self):
"""Copy the current canvas image to the clipboard."""
image = self._export_plot_image()
if image is None:
return False

clipboard = QtGui.QGuiApplication.clipboard()
if clipboard is None:
return False

clipboard.setImage(image)
return True

def _export_plot_image(self):
self.draw()
width, height = self.get_width_height()
if width <= 0 or height <= 0:
return None

buffer = io.BytesIO()
self.figure.savefig(buffer, format='png', transparent=True)
image = QtGui.QImage.fromData(buffer.getvalue(), 'PNG')
if image.isNull():
return None

crop_rect = self._visible_canvas_rect(image.width(), image.height())
if crop_rect.isEmpty():
return None

return image.copy(crop_rect)

def _visible_canvas_rect(self, image_width, image_height):
if self.width() <= 0 or self.height() <= 0:
return QtCore.QRect()

if self.parent is None or not hasattr(self.parent, 'viewport'):
return QtCore.QRect(0, 0, image_width, image_height)

viewport = self.parent.viewport()
visible_width = min(viewport.width(), self.width())
visible_height = min(viewport.height(), self.height())
x_offset = self.parent.horizontalScrollBar().value()
y_offset = self.parent.verticalScrollBar().value()

scale_x = image_width / self.width()
scale_y = image_height / self.height()

return QtCore.QRect(round(x_offset * scale_x),
round(y_offset * scale_y),
round(visible_width * scale_x),
round(visible_height * scale_y)).intersected(
QtCore.QRect(0, 0, image_width, image_height)
)

def getDataIndices(self, event):
cv = self.model.currentView

Expand Down Expand Up @@ -506,6 +561,7 @@ def contextMenuEvent(self, event):
olapColorAction.triggered.connect(connector)

self.menu.addSeparator()
self.menu.addAction(self.main_window.copyImageAction)
self.menu.addAction(self.main_window.saveImageAction)
self.menu.addAction(self.main_window.saveViewAction)
self.menu.addAction(self.main_window.openAction)
Expand Down
54 changes: 54 additions & 0 deletions tests/setup_test/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import shutil

import pytest
from PySide6 import QtGui, QtWidgets

from openmc_plotter.main_window import MainWindow, _openmcReload

Expand Down Expand Up @@ -56,3 +57,56 @@ def test_batch_image(tmpdir, qtbot):
filecmp.cmp(orig / 'ref1.png', tmpdir / 'test1.png')

mw.close()

def test_copy_image_to_clipboard(tmpdir, monkeypatch, qtbot):
orig = tmpdir.chdir()
QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
mw = MainWindow(model_path=orig)
_openmcReload(model_path=orig)
mw.loadGui()
qtbot.addWidget(mw)
mw.show()

class FakeClipboard:
def __init__(self):
self.image = None

def setImage(self, image):
self.image = image

fake_clipboard = FakeClipboard()
monkeypatch.setattr(QtGui.QGuiApplication,
'clipboard',
staticmethod(lambda: fake_clipboard))

try:
assert mw.waitForPlotIdle(60000)
mw.model.currentView.domainVisible = False
mw.plotIm.updatePixmap()
assert mw.copyImageToClipboard()
finally:
orig.chdir()

assert fake_clipboard.image is not None
assert not fake_clipboard.image.isNull()
assert fake_clipboard.image.hasAlphaChannel()

canvas_width, canvas_height = mw.plotIm.get_width_height()
expected_width = round(
min(mw.frame.viewport().width(), mw.plotIm.width())
* canvas_width
/ mw.plotIm.width()
)
expected_height = round(
min(mw.frame.viewport().height(), mw.plotIm.height())
* canvas_height
/ mw.plotIm.height()
)
assert fake_clipboard.image.width() == pytest.approx(expected_width, abs=1)
assert fake_clipboard.image.height() == pytest.approx(expected_height, abs=1)

center = fake_clipboard.image.pixelColor(fake_clipboard.image.width() // 2,
fake_clipboard.image.height() // 2)
assert center.alpha() == 0

mw.close()
80 changes: 80 additions & 0 deletions tests/test_plotgui_clipboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from types import SimpleNamespace

import numpy as np
import pytest
from PySide6 import QtGui, QtWidgets

from openmc_plotter.plotgui import PlotImage


class FakeClipboard:

def __init__(self):
self.image = None

def setImage(self, image):
self.image = image


@pytest.fixture
def qapp():
return QtWidgets.QApplication.instance() or QtWidgets.QApplication([])


def test_copy_image_to_clipboard_crops_to_plot_and_preserves_alpha(
qapp, monkeypatch
):
scroll = QtWidgets.QScrollArea()
scroll.resize(220, 160)
main_window = SimpleNamespace(
logicalDpiX=lambda: 100,
zoom=100,
coord_label=SimpleNamespace(show=lambda: None, hide=lambda: None),
statusBar=lambda: SimpleNamespace(showMessage=lambda *args, **kwargs: None),
)
plot = PlotImage(model=None, parent=scroll, main_window=main_window)
scroll.setWidget(plot)
plot.resize(400, 300)
plot.figure.clear()
plot.ax = plot.figure.subplots()
plot.ax.imshow(np.zeros((10, 10, 4)))
scroll.show()
qapp.processEvents()
scroll.horizontalScrollBar().setValue(40)
scroll.verticalScrollBar().setValue(30)
qapp.processEvents()

fake_clipboard = FakeClipboard()
monkeypatch.setattr(
QtGui.QGuiApplication,
"clipboard",
staticmethod(lambda: fake_clipboard),
)

try:
assert plot.copyImageToClipboard()
finally:
plot.close()
scroll.close()

assert fake_clipboard.image is not None
assert not fake_clipboard.image.isNull()
assert fake_clipboard.image.hasAlphaChannel()

canvas_width, canvas_height = plot.get_width_height()
expected_width = round(
min(scroll.viewport().width(), plot.width()) * canvas_width / plot.width()
)
expected_height = round(
min(scroll.viewport().height(), plot.height()) * canvas_height / plot.height()
)
assert fake_clipboard.image.width() == pytest.approx(expected_width, abs=1)
assert fake_clipboard.image.height() == pytest.approx(expected_height, abs=1)
assert fake_clipboard.image.width() < canvas_width
assert fake_clipboard.image.height() < canvas_height

center = fake_clipboard.image.pixelColor(
fake_clipboard.image.width() // 2,
fake_clipboard.image.height() // 2,
)
assert center.alpha() == 0