"""
This file contains the routines for message dialogues
"""
#=========================================================================================
# Licence, Reference and Credits
#=========================================================================================
__copyright__ = "Copyright (C) CCPN project (http://www.ccpn.ac.uk) 2014 - 2021"
__credits__ = ("Ed Brooksbank, Joanna Fox, Victoria A Higman, Luca Mureddu, Eliza Płoskoń",
"Timothy J Ragan, Brian O Smith, Gary S Thompson & Geerten W Vuister")
__licence__ = ("CCPN licence. See http://www.ccpn.ac.uk/v3-software/downloads/license")
__reference__ = ("Skinner, S.P., Fogh, R.H., Boucher, W., Ragan, T.J., Mureddu, L.G., & Vuister, G.W.",
"CcpNmr AnalysisAssign: a flexible platform for integrated NMR analysis",
"J.Biomol.Nmr (2016), 66, 111-124, http://doi.org/10.1007/s10858-016-0060-y")
#=========================================================================================
# Last code modification
#=========================================================================================
__modifiedBy__ = "$modifiedBy: Ed Brooksbank $"
__dateModified__ = "$dateModified: 2021-11-09 15:26:01 +0000 (Tue, November 09, 2021) $"
__version__ = "$Revision: 3.0.4 $"
#=========================================================================================
# Created
#=========================================================================================
__author__ = "$Author: CCPN $"
__date__ = "$Date: 2017-04-07 10:28:41 +0000 (Fri, April 07, 2017) $"
#=========================================================================================
# Start of code
#=========================================================================================
import textwrap
from PyQt5 import QtWidgets, QtCore
from ccpn.ui.gui.widgets.Font import setWidgetFont
from ccpn.ui.gui.widgets.CheckBox import CheckBox
from PyQt5.QtCore import QEvent
# from ccpn.ui.gui.guiSettings import messageFont, messageFontBold
def _isDarwin():
return 'darwin' in QtCore.QSysInfo().kernelType().lower()
Ok = QtWidgets.QMessageBox.Ok
Cancel = QtWidgets.QMessageBox.Cancel
Yes = QtWidgets.QMessageBox.Yes
No = QtWidgets.QMessageBox.No
Retry = QtWidgets.QMessageBox.Retry
Ignore = QtWidgets.QMessageBox.Ignore
Abort = QtWidgets.QMessageBox.Abort
Close = QtWidgets.QMessageBox.Close
Information = QtWidgets.QMessageBox.Information
Warning = QtWidgets.QMessageBox.Warning
Question = QtWidgets.QMessageBox.Question
Critical = QtWidgets.QMessageBox.Critical
Save = QtWidgets.QMessageBox.Save
Discard = QtWidgets.QMessageBox.Discard
default_icons = (Information, Question, Warning, Critical)
if _isDarwin():
Question = Warning
LINELENGTH = 100
WRAPBORDER = 5
WRAPSCALE = 1.01
def _wrapString(text, lineLength=LINELENGTH):
"""Wrap a line of text to the desired width of the dialog
Returns list of individual lines and the concatenated string for dialog
"""
newWrapped = []
_text = text.split('\n')
for text in _text:
wrapped = textwrap.wrap(text, width=lineLength, replace_whitespace=False, break_long_words=False)
if not text:
newWrapped.append('')
for mm in wrapped:
if len(mm) > LINELENGTH:
for chPos in range(0, len(mm), lineLength):
newWrapped.append(mm[chPos:chPos + lineLength])
else:
newWrapped.append(mm)
return newWrapped, '\n'.join(newWrapped)
# # merge lines that have now been created by splitting longer lines (if no newlines in first line)
# newWrapped2 = []
# if len(newWrapped) > 1:
# lineNum = 0
# while lineNum < len(newWrapped):
# l1 = newWrapped[lineNum]
# if lineNum == len(newWrapped) - 1:
# newWrapped2.append(l1)
# break
#
# l2 = newWrapped[lineNum + 1]
# if not l2:
# newWrapped2.append(l1)
# newWrapped2.append(l2)
# lineNum += 1
# elif (len(l1) + len(l2) < LINELENGTH) and '\n' not in l1:
# # not sure it will get here now
# newWrapped2.append(l1 + ' ' + l2)
# lineNum += 1
# else:
# newWrapped2.append(l1)
# if lineNum == len(newWrapped) - 2:
# newWrapped2.append(l2)
#
# lineNum += 1
# else:
# newWrapped2 = newWrapped
#
# return newWrapped2, '\n'.join(newWrapped2)
[docs]class MessageDialog(QtWidgets.QMessageBox):
"""
Base class for all dialogues
Using the 'multiline' to emulate the windowTitle, as on Mac the windows do not get their title
"""
def __init__(self, title, basicText, message, icon=Information, iconPath=None, parent=None, scrollableMessage=False):
QtWidgets.QMessageBox.__init__(self, None)
# set modality to take control
self.setWindowModality(QtCore.Qt.ApplicationModal)
self._parent = parent
self.setWindowTitle(title)
basicTextWrap, basicText = _wrapString(basicText)
messageWrap, message = _wrapString(message)
self.setText(basicText)
self.setInformativeText(message)
self.setIcon(icon)
self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
# self.setFont(messageFont) #GWV: Does not seem to do anything
# self.setMinimumSize(QtCore.QSize(300, 100)) #GWV: Does not seem to do anything
# self.resize(300, 100) #GWV: Does not seem to do anything
# Adapted from best solution so far from: http://apocalyptech.com/linux/qt/qmessagebox/
layout = self.layout()
# set the fonts for the labels (pushButtons are set later)
for widgetLabel in self.findChildren((QtWidgets.QLabel, QtWidgets.QTextEdit)):
setWidgetFont(widgetLabel, )
maxTextWidth = 50
widgetBasic = None
item = layout.itemAtPosition(0, 2) # grid position of basic text item
if item:
widgetBasic = item.widget()
setWidgetFont(widgetBasic, bold=True)
# get the bounding rectangle for each line of basicText
for wrapLine in basicTextWrap:
tWidth = int((QtGui.QFontMetrics(widgetBasic.font()).boundingRect(wrapLine).width() + WRAPBORDER) * WRAPSCALE)
maxTextWidth = max(maxTextWidth, tWidth)
widgetMessage = None
item = layout.itemAtPosition(1, 2) # grid position of informative text item
if item:
widgetMessage = item.widget()
# get the bounding rectangle for each line of informativeText
for wrapLine in messageWrap:
tWidth = int((QtGui.QFontMetrics(widgetMessage.font()).boundingRect(wrapLine).width() + WRAPBORDER) * WRAPSCALE)
maxTextWidth = max(maxTextWidth, tWidth)
if widgetBasic:
widgetBasic.setFixedWidth(maxTextWidth)
if widgetMessage:
if scrollableMessage: # insert the Label widgetMessage inside a scrollArea. Could be done automatically if len(text) > someValue...
scrollArea = QtWidgets.QScrollArea(self)
scrollArea.setWidgetResizable(True)
widgetMessage.setWordWrap(True)
scrollArea.setWidget(widgetMessage)
layout.addWidget(scrollArea, 1, 2)
else:
widgetMessage.setFixedWidth(maxTextWidth)
palette = QtGui.QPalette()
self.setPalette(palette)
if iconPath:
image = QtGui.QPixmap(iconPath)
scaledImage = image.scaled(48, 48, QtCore.Qt.KeepAspectRatio)
self.setIconPixmap(scaledImage)
# self.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)
[docs] def event(self, event):
"""
handler to make dialogs proely modal but at the sametime accept the correct keys for default actions
"""
# accepted events apple-delete, apple-c apple-v, esc, return, spacebar, command period, apple-z apple-y,
# apple-shift-z apple-h, apple-option-h, control tab, tab, shift tab, arrow keys, contol arrow keys
# control-shift-arrows, apple-a
ACCEPTED_MODAL_KEYS = (
(Qt.NoModifier, Qt.Key_Space,),
(Qt.ControlModifier, Qt.Key_Delete),
(Qt.ControlModifier, Qt.Key_Backspace),
(Qt.ControlModifier, Qt.Key_C),
(Qt.ControlModifier, Qt.Key_V),
(Qt.NoModifier, Qt.Key_Escape,),
(Qt.NoModifier, Qt.Key_Return,),
(Qt.NoModifier, Qt.Key_Space,),
(Qt.ControlModifier, Qt.Key_Period),
(Qt.ControlModifier, Qt.Key_Z),
(Qt.ControlModifier, Qt.Key_Y),
(Qt.ControlModifier | Qt.ShiftModifier, Qt.Key_Z),
(Qt.ControlModifier, Qt.Key_H),
(Qt.ControlModifier | Qt.AltModifier, Qt.Key_H),
(Qt.MetaModifier, Qt.Key_Tab),
(Qt.NoModifier, Qt.Key_Tab,),
(Qt.ShiftModifier, Qt.Key_Tab),
(Qt.NoModifier, Qt.Key_Left,),
(Qt.NoModifier, Qt.Key_Right,),
(Qt.NoModifier, Qt.Key_Up,),
(Qt.NoModifier, Qt.Key_Down,),
(Qt.MetaModifier, Qt.Key_Left),
(Qt.MetaModifier, Qt.Key_Right),
(Qt.MetaModifier, Qt.Key_Up),
(Qt.MetaModifier, Qt.Key_Down),
(Qt.MetaModifier | Qt.ShiftModifier, Qt.Key_Left),
(Qt.MetaModifier | Qt.ShiftModifier, Qt.Key_Right),
(Qt.MetaModifier | Qt.ShiftModifier, Qt.Key_Up),
(Qt.MetaModifier | Qt.ShiftModifier, Qt.Key_Down),
(Qt.ControlModifier, Qt.Key_A)
)
result = False
if event.type() == QEvent.ShortcutOverride:
test = (event.modifiers(), event.key())
if test in ACCEPTED_MODAL_KEYS:
event.accept()
result = True
else:
result = super(MessageDialog, self).event(event)
return result
[docs] def resizeEvent(self, ev):
"""
required to set the initial position of the message box on the centre of the screen
"""
# set the font of the push buttons, must be here after __init__ has completed
for child in self.findChildren(QtWidgets.QPushButton):
setWidgetFont(child, )
# must be the first event outside of the __init__ otherwise frameGeometries are not valid
super(MessageDialog, self).resizeEvent(ev)
activeWindow = QtWidgets.QApplication.activeWindow()
if activeWindow:
point = activeWindow.rect().center()
global_point = activeWindow.mapToGlobal(point)
self.move(global_point
- self.frameGeometry().center()
+ self.frameGeometry().topLeft())
[docs]def showInfo(title, message, parent=None, iconPath=None):
"""Display an info message
"""
dialog = MessageDialog('Information', title, message, Information, iconPath, parent)
dialog.setStandardButtons(Ok)
#dialog = QtWidgets.QMessageBox.information(parent, title, message)
dialog.raise_()
dialog.exec_()
return
[docs]def showNotImplementedMessage():
showInfo('Not implemented yet!',
'This function has not been implemented in the current version')
[docs]def showOkCancel(title, message, parent=None, iconPath=None):
dialog = MessageDialog('Query', title, message, Question, iconPath, parent)
dialog.setStandardButtons(Ok | Cancel)
dialog.setDefaultButton(Ok)
dialog.raise_()
return dialog.exec_() == Ok
[docs]def showYesNo(title, message, parent=None, iconPath=None):
dialog = MessageDialog('Query', title, message, Question, iconPath, parent)
dialog.setStandardButtons(Yes | No)
dialog.setDefaultButton(Yes)
dialog.raise_()
return dialog.exec_() == Yes
[docs]def showRetryIgnoreCancel(title, message, parent=None, iconPath=None):
dialog = MessageDialog('Retry', title, message, Question, iconPath, parent)
dialog.setStandardButtons(Retry | Ignore | Cancel)
dialog.setDefaultButton(Retry)
dialog.raise_()
result = dialog.exec_()
if result == Retry:
return True
elif result == Cancel:
return False
else:
return None
[docs]def showSaveDiscardCancel(title, message, parent=None, iconPath=None):
dialog = MessageDialog('Query', title, message, Question, iconPath, parent)
dialog.setStandardButtons(Save | Discard | Cancel)
dialog.setDefaultButton(Save)
dialog.raise_()
result = dialog.exec_()
if result == Save:
return True
elif result == Discard:
return False
else:
return None
[docs]def showWarning(title, message, parent=None, iconPath=None, scrollableMessage=False):
dialog = MessageDialog(title='Warning', basicText=title, message=message, icon=Warning, iconPath=iconPath, parent=parent,
scrollableMessage=scrollableMessage)
dialog.setStandardButtons(Close)
dialog.raise_()
dialog.exec_()
return
[docs]def showOkCancelWarning(title, message, parent=None, iconPath=None):
dialog = MessageDialog('Warning', title, message, Warning, iconPath, parent)
dialog.setStandardButtons(Ok | Cancel)
dialog.setDefaultButton(Cancel)
dialog.raise_()
return dialog.exec_() == Ok
[docs]def showYesNoWarning(title, message, parent=None, iconPath=None):
dialog = MessageDialog('Warning', title, message, Warning, iconPath, parent)
dialog.setStandardButtons(Yes | No)
dialog.setDefaultButton(No)
dialog.raise_()
return dialog.exec_() == Yes
[docs]def showMulti(title, message, texts, objects=None, parent=None, iconPath=None, okText='OK', cancelText='Cancel', destructive=(), checkbox=None, checked=True):
if objects:
assert len(objects) == len(texts)
dialog = MessageDialog('Query', title, message, Question, iconPath, parent)
_checkbox = None
for text in texts:
lower_text = text.strip().lower()
if checkbox and (lower_text in checkbox or checkbox in lower_text):
raise Exception('Checkboxes and buttons cannot have the same name!')
else:
role = QtWidgets.QMessageBox.ActionRole
if lower_text == 'cancel' or lower_text == cancelText.strip().lower():
role = QtWidgets.QMessageBox.RejectRole
if not isinstance(destructive, str):
destructive = [item.strip().lower() for item in destructive]
else:
destructive = destructive.strip().lower()
if lower_text in destructive:
role = QtWidgets.QMessageBox.DestructiveRole
if lower_text == 'ok' or lower_text == okText.strip().lower():
role = QtWidgets.QMessageBox.AcceptRole
button = dialog.addButton(text, role)
if lower_text == 'ok' or lower_text == okText.strip().lower():
dialog.setDefaultButton(button)
if checkbox is not None:
_checkbox = CheckBox(parent=dialog, text=checkbox, checked=checked)
dialog.setCheckBox(_checkbox)
if _checkbox is not None:
_checkbox.setFocus()
index = dialog.exec_()
result = ''
if dialog.clickedButton() is not None:
if objects:
result = objects[index]
else:
result = texts[index]
if checkbox is not None and _checkbox.isChecked():
result = ' %s %s ' % (result, checkbox)
return result
[docs]def showError(title, message, parent=None, iconPath=None):
dialog = MessageDialog('Error', title, message, Critical, iconPath, parent)
dialog.setStandardButtons(Close)
dialog.raise_()
dialog.exec_()
return
[docs]def showMessage(title, message, parent=None, iconPath=None):
dialog = MessageDialog('Message', title, message, Information, iconPath, parent)
dialog.setStandardButtons(Close)
dialog.raise_()
dialog.exec_()
return
# testing simple progress/busy popup
from PyQt5 import QtCore
from PyQt5 import QtGui, QtWidgets
from PyQt5.QtCore import pyqtSlot
from ccpn.ui.gui.popups.Dialog import CcpnDialog
from ccpn.ui.gui.widgets.Label import Label
from contextlib import contextmanager
from time import sleep, time
[docs]@contextmanager
def progressManager(parent, title=None, progressMax=100):
thisProg = progressPopup(parent=parent, title=title, progressMax=progressMax)
try:
thisProg.progress_simulation()
thisProg.update()
QtWidgets.QApplication.processEvents() # still doesn't catch all the paint events
sleep(0.1)
yield # yield control to the main process
finally:
thisProg.update()
QtWidgets.QApplication.processEvents() # hopefully it will redraw the popup
thisProg.close()
# return correct focus control to the parent
QtWidgets.QApplication.setActiveWindow(parent)
def _stoppableProgressBar(data, title='Calculating...', buttonText='Cancel'):
""" Use this for opening a _stoppableProgressBar before time consuming operations. the cancel button allows
the user to stop the loop manually.
eg:
for i in _stoppableProgressBar(range(10), title, buttonText):
# do stuff
pass
for use in a zip loop, wrap with 'list':
eg for (cs, ts) in _stoppableProgressBar(list(zip(controlSpectra, targetSpectra)))
"""
widget = QtWidgets.QProgressDialog(title, buttonText, 0, len(data)) # starts = 0, ends = len(data)
widget.setAutoClose(True)
widget.raise_()
c = 0
for v in iter(data):
QtCore.QCoreApplication.instance().processEvents()
if widget.wasCanceled():
raise RuntimeError('Stopped by user')
c += 1
widget.setValue(c)
yield (v)
import math, sys
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import *
[docs]class busyOverlay(QtWidgets.QWidget):
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent)
palette = QPalette(self.palette())
palette.setColor(palette.Background, Qt.transparent)
self.setPalette(palette)
[docs] def paintEvent(self, event):
painter = QPainter()
painter.begin(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.fillRect(event.rect(), QBrush(QColor(255, 255, 255, 127)))
painter.setPen(QPen(Qt.NoPen))
for i in range(6):
if (self.counter / 5) % 6 == i:
painter.setBrush(QBrush(QColor(127 + (self.counter % 5) * 32, 127, 127)))
else:
painter.setBrush(QBrush(QColor(127, 127, 127)))
painter.drawEllipse(
self.width() / 2 + 30 * math.cos(2 * math.pi * i / 6.0) - 10,
self.height() / 2 + 30 * math.sin(2 * math.pi * i / 6.0) - 10,
20, 20)
painter.end()
[docs] def showEvent(self, event):
self.timer = self.startTimer(50)
self.counter = 0
[docs] def timerEvent(self, event):
self.counter += 1
self.update()
if self.counter == 60:
self.killTimer(self.timer)
self.hide()
# class MainWindow(QMainWindow):
#
# def __init__(self, parent = None):
#
# QMainWindow.__init__(self, parent)
#
# widget = QWidget(self)
# self.editor = QTextEdit()
# self.editor.setPlainText("0123456789"*100)
# layout = QGridLayout(widget)
# layout.addWidget(self.editor, 0, 0, 1, 3)
# button = QPushButton("Wait")
# layout.addWidget(button, 1, 1, 1, 1)
#
# self.setCentralWidget(widget)
# self.overlay = Overlay(self.centralWidget())
# self.overlay.hide()
# button.clicked.connect(self.overlay.show)
#
# def resizeEvent(self, event):
#
# self.overlay.resize(event.size())
# event.accept()
if __name__ == '__main__':
import sys
from ccpn.ui.gui.widgets.Application import TestApplication
from ccpn.ui.gui.widgets.BasePopup import BasePopup
from ccpn.ui.gui.widgets.Button import Button
import time
app = QtWidgets.QApplication(sys.argv)
# for i in _stoppableProgressBar([1]*10000):
# time.sleep(0.2)
def callback():
# print(showInfo('My info window', 'test info'))
# print(showMulti('Test', 'Multi Choice', ['Apples', 'Bananas', 'Pears']))
# print(showError('Test', 'This is a test error message'))
# print(showYesNo('Test', 'Yes or No message'))
# # print(showOkCancel('Test', 'Ok or Cancel message'))
# # print(showRetryIgnoreCancel('Test', 'Some message'))
# # print(showWarning('Test', 'Warning message'))
# print(showWarning(
# 'Test for a basic popup with a long line of text as the basic text and a path: /Users/ejb66/PycharmProjects/Git/AnalysisV3/internal/scripts/something/filename.txt',
# 'Warning message'))
print(showWarning('Another Warning',
'Test for a basic popup with a long line of text as the basic text and a path:\n/Users/ejb66/PycharmProjects/Git/AnalysisV3/internal/scripts/something/filename.txt'))
print(showWarning('Another Warning and Test for a basic popup with a long line of text as the basic text',
'Test for a basic popup with a long line of text as the basic text and a path\n/Users/ejb66/PycharmProjects/Git/AnalysisV3/internal/scripts/something/filename.txt '
'and text with no spaces qwertyuiopasdfghjklzxcvbnm0123456789_qwertyuiopasdfghjklzxcvbnm0123456789_qwertyuiopasdfghjklzxcvbnm0123456789_qwertyuiopasdfghjklzxcvbnm0123456789'))
print(showWarning('Another Warning and Test qwertyuiopasdfghjklzxcvbnm0123456789_qwertyuiopasdfghjklzxcvbnm0123456789_qwertyuiopasdfghjklzxcvbnm0123456789_qwertyuiopasdfghjklzxcvbnm0123456789\n for a basic popup with a long line of text as the basic text',
'Test for a basic popup with a long line of text as the basic text and a path\n/Users/ejb66/PycharmProjects/Git/AnalysisV3/internal/scripts/something/filename.txt '
'and text with no spaces qwertyuiopasdfghjklzxcvbnm0123456789_qwertyuiopasdfghjklzxcvbnm0123456789_qwertyuiopasdfghjklzxcvbnm0123456789_qwertyuiopasdfghjklzxcvbnm0123456789 something\n else'))
# app = TestApplication()
# # popup = BasePopup(title='Test MessageReporter')
# #popup.setSize(200,30)
# # button = Button(popup, text='hit me', callback=callback)
#
# popup = progressPopup(busyFunc=callback)
#
# popup.show()
# popup.raise_()
#
# app.start()
callback()