"""
Convenience functions to prompt the user.
The following functions create a dialog window to either notify the user of an
event that happened or to request information from the user.
"""
import traceback
from . import Qt
from . import QtWidgets
from . import application
from . import binding
from .convert import to_qfont
[docs]def critical(message, *, title=None, font=None):
"""Display a `critical` message in a dialog window.
Parameters
----------
message : :class:`str` or :class:`Exception`
The message to display. The message can use HTML/CSS markup, for example,
``'<html>A <p style="color:red;font-size:18px"><i>critical</i></p> error occurred!</html>'``
title : :class:`str`, optional
The text to display in the title bar of the dialog window.
If :data:`None` then uses the text in the title bar of the active window.
font : :class:`int`, :class:`str`, :class:`tuple` or :class:`QtGui.QFont`, optional
The font to use. If an :class:`int` then the point size, if a :class:`str` then
the family name, if a :class:`tuple` then the (family name, point size).
"""
if isinstance(message, Exception):
message = traceback.format_exc()
app, mb = _message_box(title=title, message=message, font=font)
mb.setIcon(QtWidgets.QMessageBox.Icon.Critical)
mb.exec()
[docs]def double(message, *, title=None, font=None, value=0, minimum=-2147483647, maximum=2147483647, step=1, decimals=2):
"""Request a double-precision value (a Python :class:`float` data type).
Parameters
----------
message : :class:`str`
The message that is shown to the user to describe what the value represents.
The message can use HTML/CSS markup, for example, ``'<html>Enter a mass, in μg</html>'``
title : :class:`str`, optional
The text to display in the title bar of the dialog window.
If :data:`None` then uses the text in the title bar of the active window.
font : :class:`int`, :class:`str`, :class:`tuple` or :class:`QtGui.QFont`, optional
The font to use. If an :class:`int` then the point size, if a :class:`str` then
the family name, if a :class:`tuple` then the (family name, point size).
value : :class:`float`, optional
The initial value.
minimum : :class:`float`, optional
The minimum value that the user can enter.
maximum : :class:`float`, optional
The maximum value that the user can enter.
step : :class:`float`, optional
The step size by which the value is increased and decreased.
decimals : :class:`int`, optional
The number of digits that are displayed after the decimal point.
Returns
-------
:class:`float` or :data:`None`
The value or :data:`None` if the user cancelled the request.
"""
app, dialog = _input_dialog(title=title, message=message, font=font)
dialog.setInputMode(QtWidgets.QInputDialog.InputMode.DoubleInput)
dialog.setDoubleRange(minimum, maximum)
dialog.setDoubleStep(step)
dialog.setDoubleDecimals(decimals)
dialog.setDoubleValue(value)
ok = dialog.exec()
return dialog.doubleValue() if ok else None
[docs]def filename(*, title='Select File', directory=None, filters=None, multiple=False):
"""Request to select the file(s) to open.
Parameters
----------
title : :class:`str`, optional
The text to display in the title bar of the dialog window.
directory : :class:`str`, optional
The initial directory to start in.
filters : :class:`str`, :class:`list` of :class:`str` or :class:`dict`, optional
Only filenames that match the specified `filters` are shown.
Examples::
'Images (*.png *.xpm *.jpg)'
'Images (*.png *.xpm *.jpg);;Text files (*.txt);;XML files (*.xml)'
['Images (*.png *.xpm *.jpg)', 'Text files (*.txt)', 'XML files (*.xml)']
{'Images': ('*.png', '*.xpm', '*.jpg'), 'Text files': '*.txt'}
multiple : :class:`bool`, optional
Whether multiple files can be selected.
Returns
-------
:class:`str`, :class:`list` of :class:`str` or :data:`None`
The name(s) of the file(s) to open or :data:`None` if the user cancelled the request.
"""
app, title = _get_app_and_title(title)
filters = _get_file_filters(filters)
if multiple:
if title == 'Select File':
title += 's'
name, _ = QtWidgets.QFileDialog.getOpenFileNames(app.activeWindow(), title, directory, filters)
else:
name, _ = QtWidgets.QFileDialog.getOpenFileName(app.activeWindow(), title, directory, filters)
return name if name else None
[docs]def folder(*, title='Select Folder', directory=None):
"""Request to select an existing folder or to create a new folder.
Parameters
----------
title : :class:`str`, optional
The text to display in the title bar of the dialog window.
directory : :class:`str`, optional
The initial directory to start in.
Returns
-------
:class:`str` or :data:`None`
The name of the selected folder or :data:`None` if the user cancelled the request.
"""
app, title = _get_app_and_title(title)
name = QtWidgets.QFileDialog.getExistingDirectory(app.activeWindow(), title, directory)
return name if name else None
[docs]def integer(message, *, title=None, font=None, value=0, minimum=-2147483647, maximum=2147483647, step=1):
"""Request an integer value.
Parameters
----------
message : :class:`str`
The message that is shown to the user to describe what the value represents.
The message can use HTML/CSS markup, for example, ``'<html>Enter a mass, in μg</html>'``
title : :class:`str`, optional
The text to display in the title bar of the dialog window.
If :data:`None` then uses the text in the title bar of the active window.
font : :class:`int`, :class:`str`, :class:`tuple` or :class:`QtGui.QFont`, optional
The font to use. If an :class:`int` then the point size, if a :class:`str` then
the family name, if a :class:`tuple` then the (family name, point size).
value : :class:`int`, optional
The initial value.
minimum : :class:`int`, optional
The minimum value that the user can enter.
maximum : :class:`int`, optional
The maximum value that the user can enter.
step : :class:`int`, optional
The step size by which the value is increased and decreased.
Returns
-------
:class:`int` or :data:`None`
The value or :data:`None` if the user cancelled the request.
"""
app, dialog = _input_dialog(title=title, message=message, font=font)
dialog.setInputMode(QtWidgets.QInputDialog.InputMode.IntInput)
dialog.setIntRange(minimum, maximum)
dialog.setIntStep(step)
dialog.setIntValue(value)
ok = dialog.exec()
return dialog.intValue() if ok else None
[docs]def item(message, items, *, title=None, font=None, index=0):
"""Request an item from a list of items.
Parameters
----------
message : :class:`str`
The message that is shown to the user to describe what the list of items represent.
The message can use HTML/CSS markup.
items : :class:`list` or :class:`tuple`
The list of items to choose from. The items can be of any data type.
title : :class:`str`, optional
The text to display in the title bar of the dialog window.
If :data:`None` then uses the text in the title bar of the active window.
font : :class:`int`, :class:`str`, :class:`tuple` or :class:`QtGui.QFont`, optional
The font to use. If an :class:`int` then the point size, if a :class:`str` then
the family name, if a :class:`tuple` then the (family name, point size).
index : :class:`int`, optional
The index of the initial item that is selected.
Returns
-------
The selected item or :data:`None` if the user cancelled the request.
Notes
-----
The data type of the selected item is preserved. For example, if
`items` = ``[1, 2.0, 2+3j, 'hello', b'world', True, QtWidgets.QPushButton]``
and the user selects the ``2+3j`` item then a :class:`complex` data type is returned.
"""
items_ = [str(i) for i in items]
app, dialog = _input_dialog(title=title, message=message, font=font)
dialog.setComboBoxItems(items_)
dialog.setTextValue(items_[index])
dialog.setComboBoxEditable(False)
dialog.setInputMethodHints(Qt.InputMethodHint.ImhNone)
ok = dialog.exec()
return items[items_.index(dialog.textValue())] if ok else None
[docs]def save(*, title='Save As', directory=None, filters=None, options=None):
"""Request to enter the name of a file to save.
Parameters
----------
title : :class:`str`, optional
The text to display in the title bar of the dialog window.
directory : :class:`str`, optional
The initial directory to start in.
filters : :class:`str`, :class:`list` of :class:`str` or :class:`dict`, optional
Only filenames that match the specified `filters` are shown.
Examples::
'Images (*.png *.xpm *.jpg)'
'Images (*.png *.xpm *.jpg);;Text files (*.txt);;XML files (*.xml)'
['Images (*.png *.xpm *.jpg)', 'Text files (*.txt)', 'XML files (*.xml)']
{'Images': ('*.png', '*.xpm', '*.jpg'), 'Text files': '*.txt'}
options : `QtWidgets.QFileDialog.Option <https://doc.qt.io/qt-6/qfiledialog.html#Option-enum>`_, optional
Specify additional options on how to run the dialog.
Returns
-------
:class:`str` or :data:`None`
The name of the file to save or :data:`None` if the user cancelled the request.
"""
app, title = _get_app_and_title(title)
filters = _get_file_filters(filters)
if options is None:
name, _ = QtWidgets.QFileDialog.getSaveFileName(app.activeWindow(), title, directory, filters)
else:
name, _ = QtWidgets.QFileDialog.getSaveFileName(app.activeWindow(), title, directory, filters, options=options)
return name if name else None
[docs]def text(message, *, title=None, font=None, value='', multi_line=False, echo=QtWidgets.QLineEdit.EchoMode.Normal):
"""Request text.
Parameters
----------
message : :class:`str`
The message that is shown to the user to describe what the list of items represent.
The message can use HTML/CSS markup.
title : :class:`str`, optional
The text to display in the title bar of the dialog window.
If :data:`None` then uses the text in the title bar of the active window.
font : :class:`int`, :class:`str`, :class:`tuple` or :class:`QtGui.QFont`, optional
The font to use. If an :class:`int` then the point size, if a :class:`str` then
the family name, if a :class:`tuple` then the (family name, point size).
value : :class:`str`, optional
The initial value.
multi_line : :class:`bool`, optional
Whether the entered text can span multiple lines.
echo : :class:`int` or `QLineEdit.EchoMode <https://doc.qt.io/qt-6/qlineedit.html#EchoMode-enum>`_, optional
The echo mode for the text value. Useful if requesting a password.
Returns
-------
:class:`str` or :data:`None`
The text that the user entered or :data:`None` if the user cancelled the request.
"""
app, dialog = _input_dialog(title=title, message=message, font=font)
dialog.setTextValue(value)
dialog.setInputMethodHints(Qt.InputMethodHint.ImhNone)
if multi_line:
dialog.setOption(QtWidgets.QInputDialog.InputDialogOption.UsePlainTextEditForTextInput)
else:
dialog.setTextEchoMode(QtWidgets.QLineEdit.EchoMode(echo))
ok = dialog.exec()
return dialog.textValue().strip() if ok else None
[docs]def password(message, *, title=None, font=None):
"""Request a password.
Parameters
----------
message : :class:`str`
The message that is shown to the user to describe what the list of items represent.
The message can use HTML/CSS markup.
title : :class:`str`, optional
The text to display in the title bar of the dialog window.
If :data:`None` then uses the text in the title bar of the active window.
font : :class:`int`, :class:`str`, :class:`tuple` or :class:`QtGui.QFont`, optional
The font to use. If an :class:`int` then the point size, if a :class:`str` then
the family name, if a :class:`tuple` then the (family name, point size).
Returns
-------
:class:`str` or :data:`None`
The password or :data:`None` if the user cancelled the request.
"""
return text(message, title=title, font=font, echo=QtWidgets.QLineEdit.EchoMode.Password)
[docs]def warning(message, *, title=None, font=None):
"""Display a `warning` message in a dialog window.
Parameters
----------
message : :class:`str` or :class:`Exception`
The message to display. The message can use HTML/CSS markup, for example,
``'<html>A <p style="color:yellow">warning</p>...</html>'``
title : :class:`str`, optional
The text to display in the title bar of the dialog window.
If :data:`None` then uses the text in the title bar of the active window.
font : :class:`int`, :class:`str`, :class:`tuple` or :class:`QtGui.QFont`, optional
The font to use. If an :class:`int` then the point size, if a :class:`str` then
the family name, if a :class:`tuple` then the (family name, point size).
"""
if isinstance(message, Exception):
message = traceback.format_exc()
app, mb = _message_box(title=title, message=message, font=font)
mb.setIcon(QtWidgets.QMessageBox.Icon.Warning)
mb.exec()
[docs]def ok_cancel(message, *, title=None, font=None, default=True):
"""Ask for a response to a message where the logical options are ``Ok`` or ``Cancel``.
Parameters
----------
message : :class:`str`
The message to ask the user. The message can use HTML/CSS markup.
title : :class:`str`, optional
The text to display in the title bar of the dialog window.
If :data:`None` then uses the text in the title bar of the active window.
font : :class:`int`, :class:`str`, :class:`tuple` or :class:`QtGui.QFont`, optional
The font to use. If an :class:`int` then the point size, if a :class:`str` then
the family name, if a :class:`tuple` then the (family name, point size).
default : :class:`bool`, optional
The answer to be selected by default. If :data:`True` then ``Ok`` is
the default answer, otherwise ``Cancel`` is the default answer.
Returns
-------
:class:`bool` or :data:`None`
:data:`True` if the user answered ``Ok``, :data:`None` if the user answered ``Cancel``.
"""
ok = QtWidgets.QMessageBox.StandardButton.Ok
cancel = QtWidgets.QMessageBox.StandardButton.Cancel
app, mb = _message_box(title=title, message=message, font=font)
mb.setIcon(QtWidgets.QMessageBox.Icon.Question)
mb.setStandardButtons(ok | cancel)
mb.setDefaultButton(ok if default else cancel)
response = mb.exec()
if _equal(response, ok):
return True
return None
[docs]def yes_no(message, *, title=None, font=None, default=True):
"""Ask for a response to a message where the logical options are ``Yes`` or ``No``.
Parameters
----------
message : :class:`str`
The message to ask the user. The message can use HTML/CSS markup.
title : :class:`str`, optional
The text to display in the title bar of the dialog window.
If :data:`None` then uses the text in the title bar of the active window.
font : :class:`int`, :class:`str`, :class:`tuple` or :class:`QtGui.QFont`, optional
The font to use. If an :class:`int` then the point size, if a :class:`str` then
the family name, if a :class:`tuple` then the (family name, point size).
default : :class:`bool`, optional
The answer to be selected by default. If :data:`True` then ``Yes`` is
the default answer, otherwise ``No`` is the default answer.
Returns
-------
:class:`bool`
:data:`True` if the user answered ``Yes``, :data:`False` if the user answered ``No``.
"""
yes = QtWidgets.QMessageBox.StandardButton.Yes
no = QtWidgets.QMessageBox.StandardButton.No
app, mb = _message_box(title=title, message=message, font=font)
mb.setIcon(QtWidgets.QMessageBox.Icon.Question)
mb.setStandardButtons(yes | no)
mb.setDefaultButton(yes if default else no)
response = mb.exec()
return _equal(response, yes)
[docs]def yes_no_cancel(message, *, title=None, font=None, default=True):
"""Ask for a response to a message where the logical options are ``Yes``, ``No`` or ``Cancel``.
Parameters
----------
message : :class:`str`
The message to ask the user. The message can use HTML/CSS markup.
title : :class:`str`, optional
The text to display in the title bar of the dialog window.
If :data:`None` then uses the text in the title bar of the active window.
font : :class:`int`, :class:`str`, :class:`tuple` or :class:`QtGui.QFont`, optional
The font to use. If an :class:`int` then the point size, if a :class:`str` then
the family name, if a :class:`tuple` then the (family name, point size).
default : :class:`bool`, optional
The answer to be selected by default. If :data:`True` then ``Yes`` is
the default answer, if :data:`False` then ``No`` is the default answer,
else if :data:`None` then ``Cancel`` is the default answer.
Returns
-------
:class:`bool` or :data:`None`
:data:`True` if the user answered ``Yes``, :data:`False` if the user answered ``No``,
or :data:`None` if the user answered ``Cancel``.
"""
yes = QtWidgets.QMessageBox.StandardButton.Yes
no = QtWidgets.QMessageBox.StandardButton.No
cancel = QtWidgets.QMessageBox.StandardButton.Cancel
app, mb = _message_box(title=title, message=message, font=font)
mb.setIcon(QtWidgets.QMessageBox.Icon.Question)
mb.setStandardButtons(yes | no | cancel)
if default is None:
mb.setDefaultButton(cancel)
elif default:
mb.setDefaultButton(yes)
else:
mb.setDefaultButton(no)
response = mb.exec()
if _equal(response, yes):
return True
elif _equal(response, no):
return False
return None
def _get_app_and_title(title):
"""Returns a tuple of the QApplication instance and the title bar text of the active window."""
app = application()
if title is None:
w = app.activeWindow()
title = 'MSL' if w is None else w.windowTitle()
return app, title
def _input_dialog(*, title=None, message=None, font=None):
app, title = _get_app_and_title(title)
dialog = QtWidgets.QInputDialog(app.activeWindow(), flags=Qt.WindowType.WindowCloseButtonHint)
dialog.setWindowTitle(title)
dialog.setLabelText(message)
if font:
dialog.setFont(to_qfont(font))
return app, dialog
def _message_box(*, title=None, message=None, font=None, buttons=None, default=None):
app, title = _get_app_and_title(title)
mb = QtWidgets.QMessageBox(app.activeWindow())
mb.setWindowTitle(title)
mb.setText(message)
if font:
mb.setFont(to_qfont(font))
if buttons:
mb.setStandardButtons(buttons)
if default:
mb.setDefaultButton(default)
return app, mb
def _get_file_filters(filters):
"""Make the `filters` value be in the appropriate syntax."""
def _check_extn(ex):
"""Check the format of the file extension."""
if ex is None:
return all_files
if '*' in ex:
return ex
if ex.startswith('.'):
return f'*{ex}'
return f'*.{ex}'
all_files = 'All Files (*)'
if filters is None:
return all_files
if isinstance(filters, dict):
f = ''
for name, extn in filters.items():
if isinstance(extn, (list, tuple)):
extn = ' '.join(_check_extn(e) for e in extn)
f += f'{name} ({extn});;'
else:
f += f'{name} ({_check_extn(extn)});;'
return f[:-2]
if isinstance(filters, (list, tuple)):
return ';;'.join(f if f is not None else all_files for f in filters)
if filters.endswith(';;'): # noqa: filters.endswith exists since it must be a string
return filters[:-2]
return filters
def _equal(response, enum):
"""In PyQt6 all enums are implemented as an enum.Enum not enum.IntEnum"""
if binding.name == 'PyQt6':
return response == enum.value
return response == enum