diff --git a/CHANGELOG.md b/CHANGELOG.md index 3da4bb28..37802fdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ New features and improvements: * Relative coordinates are now used by default to reduce file size. If absolute coordinates are needed, they a new `--absolute` option for the `write` command. * A homing command (as defined by the `final_pu_params` configuration parameter) is no longer emitted between layers. +* The viewer (`show` command) now catches interruptions from the terminal (ctrl-C) and closes itself (#321) Bug fixes: * ... diff --git a/vpype_viewer/qtviewer/utils.py b/vpype_viewer/qtviewer/utils.py index 72f7fa9c..dec88dbc 100644 --- a/vpype_viewer/qtviewer/utils.py +++ b/vpype_viewer/qtviewer/utils.py @@ -1,5 +1,10 @@ import os +import signal +import socket +from contextlib import contextmanager +from typing import Callable +from PySide2 import QtNetwork from PySide2.QtCore import QCoreApplication from PySide2.QtGui import QIcon, QPalette from PySide2.QtWidgets import QAction, QActionGroup @@ -73,3 +78,38 @@ def __init__(self, current: float = 0.8, parent=None): act.setCheckable(True) act.setChecked(w == current) act.setData(w) + + +class SignalWatchdog(QtNetwork.QAbstractSocket): + """This object notify PySide2's event loop of an incoming signal and makes it process it. + + The python interpreter flags incoming signals and triggers the handler only upon the next + bytecode is processed. Since PySide2's C++ event loop function never/rarely returns when + the UX is in the background, the Python interpreter doesn't have a chance to run and call + the handler. + + From: https://stackoverflow.com/a/65802260/229511 and + https://stackoverflow.com/a/37229299/229511 + """ + + def __init__(self): + # noinspection PyTypeChecker + super().__init__(QtNetwork.QAbstractSocket.SctpSocket, None) # type: ignore + self.writer, self.reader = socket.socketpair() + self.writer.setblocking(False) + signal.set_wakeup_fd(self.writer.fileno()) + self.setSocketDescriptor(self.reader.fileno()) + self.readyRead.connect(lambda: None) + + +@contextmanager +def set_sigint_handler(handler: Callable): + original_handler = signal.getsignal(signal.SIGINT) + signal.signal(signal.SIGINT, handler) + # noinspection PyUnusedLocal + watchdog = SignalWatchdog() + try: + yield + finally: + signal.signal(signal.SIGINT, original_handler) + del watchdog diff --git a/vpype_viewer/qtviewer/viewer.py b/vpype_viewer/qtviewer/viewer.py index 73818f43..e2109da7 100644 --- a/vpype_viewer/qtviewer/viewer.py +++ b/vpype_viewer/qtviewer/viewer.py @@ -30,7 +30,7 @@ from .._scales import UnitType from ..engine import Engine, ViewMode -from .utils import PenOpacityActionGroup, PenWidthActionGroup, load_icon +from .utils import PenOpacityActionGroup, PenWidthActionGroup, load_icon, set_sigint_handler __all__ = ["QtViewerWidget", "QtViewer", "show"] @@ -480,4 +480,11 @@ def show( widget.show() - return app.exec_() + # noinspection PyUnusedLocal + def sigint_handler(signum, frame): + QApplication.quit() + + with set_sigint_handler(sigint_handler): + res = app.exec_() + + return res