Source code for secv_guis.dialogs

# -*- coding:utf-8 -*-


"""
This module defines several reusable dialog types.
"""


import traceback
from PySide2 import QtCore, QtWidgets


# #############################################################################
# # DIALOGS
# #############################################################################
[docs]class FlexibleDialog(QtWidgets.QDialog): """ Dialog class that allows for OK, Yes/No, and timeout interactions. To extend this dialog class, override ``setup_ui_body``, ``on_accept`` and ``on_reject``, store the instance and call it with ``show`` or ``exec_``. Note that ``setup_ui_body`` is being called IN the constructor, so any variables that it may need when extending the class need to be set before ``super().__init__`` is called. As it can be seen here, https://stackoverflow.com/questions/56449605/pyside2-qdialog-possible-bug implementing a Dialog in PySide2 is a little tricky. These are some things to consider: * Do not implement ``accept, reject`` directly. Rather, connect the buttons to ``accept, reject``, and then connect the ``accepted, rejected`` signals to custom methods (in this case ``on_accept, on_reject``). * When calling the Dialog from the main window, the dialog must be persistently stored as a field of the main window ``i.e. self.d = ...``. Otherwise it will not show up. Then it can be called in modal or modeless way, as follows: ``XXX.connect(self.d.show), ...(self.d.exec_)``. """ TIMEOUT_LBL_TXT = "Closing in {} seconds..." def __init__(self, accept_button_name=None, reject_button_name=None, timeout_ms=None, parent=None): """ :param str accept_button_name: Text to be shown in the accept button. :param str reject_button_name: Text to be shown in the reject button. :param int timeout_ms: If given, time that the dialog takes to close automatically, in milliseconds. """ super().__init__(parent) # The body goes into ui_widget self.ui_widget = QtWidgets.QWidget() self.setup_ui_body(self.ui_widget) # main_layout = QtWidgets.QVBoxLayout(self) main_layout.addWidget(self.ui_widget) # Then comes the (optional) buttons section with_a = accept_button_name is not None with_r = reject_button_name is not None if with_a or with_r: button_layout = QtWidgets.QHBoxLayout() main_layout.addLayout(button_layout) # if with_a: self.accept_b = QtWidgets.QPushButton(accept_button_name) button_layout.addWidget(self.accept_b) self.accept_b.clicked.connect(self.accept) self.accepted.connect(self.on_accept) # remove autodefault, if you want default set it explicitly self.accept_b.setAutoDefault(False) # if with_r: self.reject_b = QtWidgets.QPushButton(reject_button_name) button_layout.addWidget(self.reject_b) self.reject_b.clicked.connect(self.reject) self.rejected.connect(self.on_reject) # remove autodefault, if you want default set it explicitly self.reject_b.setAutoDefault(False) # Finally the (optional) timeout message if timeout_ms is not None: assert isinstance(timeout_ms, int), \ "Timeout miliseconds must be int or None!" self.timeout_lbl = QtWidgets.QLabel( self.TIMEOUT_LBL_TXT.format(timeout_ms / 1000)) main_layout.addWidget(self.timeout_lbl) self.timeout_ms = timeout_ms def _set_timer(self): """ """ if self.timeout_ms is not None: QtCore.QTimer.singleShot(self.timeout_ms, self.reject)
[docs] def exec_(self, *args, **kwargs): """ Start the dialog in 'exclusive' way, blocking the rest of the app. """ self._set_timer() return super().exec_(*args, **kwargs)
[docs] def show(self, *args, **kwargs): """ Start the dialog in parallel to the rest of the app. """ self._set_timer() return super().show(*args, **kwargs)
# OVERRIDE THESE
[docs] def setup_ui_body(self, widget): """ Populate the widget with your desired contents. The widget will be above the buttons. """ pass
[docs] def on_accept(self): """ This method will be called if the user presses the (optional) accept button. """ pass
[docs] def on_reject(self): """ This method will be called if the user presses the (optional) reject button. """ pass
[docs]class InfoDialog(FlexibleDialog): """ A type of dialog that shows a header and body strings. """ # If true, the user can select the text with the mouse INTERACT_HEADER = False INTERACT_BODY = True def __init__(self, header, message, accept_button_name=None, reject_button_name=None, timeout_ms=None, parent=None, print_msg=True, header_style="font-weight: bold; color: black"): """ :param str header: This will be the dialog title :param str message: This will go below the title, separated by a line :param header_style: A CSS-like style to format the header. Check ``FlexibleDialog`` docstrings for more details. """ self._h_txt = header self._msg_txt = message self.print_msg = print_msg self._h_style = header_style super().__init__(accept_button_name, reject_button_name, timeout_ms, parent)
[docs] def setup_ui_body(self, widget): """ """ lyt = QtWidgets.QVBoxLayout(widget) # self.header_lbl = QtWidgets.QLabel(self._h_txt) self.body_lbl = QtWidgets.QLabel(self._msg_txt) self.line = QtWidgets.QFrame() self.line.setFrameShape(QtWidgets.QFrame.HLine) self.line.setFrameShadow(QtWidgets.QFrame.Sunken) # lyt.addWidget(self.header_lbl) lyt.addWidget(self.line) lyt.addWidget(self.body_lbl) if self.INTERACT_HEADER: self.header_lbl.setTextInteractionFlags( QtCore.Qt.TextSelectableByMouse) if self.INTERACT_BODY: self.body_lbl.setTextInteractionFlags( QtCore.Qt.TextSelectableByMouse) # self.header_lbl.setStyleSheet(self._h_style)
def _print_if(self): """ """ if self.print_msg: print(self._h_txt) print(self._msg_txt)
[docs] def exec_(self, *args, **kwargs): """ """ outcome = super().exec_(*args, **kwargs) self._print_if() return outcome
[docs] def show(self, *args, **kwargs): """ """ outcome = super().show(*args, **kwargs) self._print_if() return outcome
[docs]class ExceptionDialog(InfoDialog): """ This class is intended to be used at the main loop level, to catch any exceptions that the app may have and show them in a Dialog. To do that, it suffices to put the following line anywhere before ``app.exec_()``:: sys.excepthook = ExceptionDialog.excepthook Source: https://stackoverflow.com/a/55819545/4511978 """ DEACTIVATE = False ERROR_TXT = """ ERROR! If you think this is an app error consider reporting the following to the developers (see Help->About):""" def __init__(self, error_msg, timeout_ms=None, parent=None): """ Check ``InfoDialog`` docstring for details. """ super().__init__(self.ERROR_TXT, error_msg, "OK", "Don't show errors again", header_style="font-weight: bold; color: red")
[docs] def on_reject(self): """ If the user presses on don't show errors again, the whole class gets deactivated, so further created instances won't pop up. """ print("deactivated!") self.__class__.DEACTIVATE = True
[docs] @classmethod def excepthook(cls, exc_type, exc_value, exc_tb): """ Set this method as ``sys.excepthook = <THIS_CLASS>.excepthook`` somewhere before ``app.exec_()`` to wrap all Python exceptions with this dialog. """ msg = "".join(traceback.format_exception(exc_type, exc_value, exc_tb)) print(cls.ERROR_TXT) print(msg) # if not cls.DEACTIVATE: cls(msg).exec_()