1# qutil.py -- various Qt4-related utility functions 2# 3# Copyright (c) 2008 - 2014 by Wilbert Berendsen 4# 5# This program is free software; you can redistribute it and/or 6# modify it under the terms of the GNU General Public License 7# as published by the Free Software Foundation; either version 2 8# of the License, or (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program; if not, write to the Free Software 17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18# See http://www.gnu.org/licenses/ for more information. 19 20""" 21Some Qt4-related utility functions. 22""" 23 24 25import contextlib 26import re 27import weakref 28 29from PyQt5.QtCore import QEventLoop, QSettings, QSize, QTimer, Qt 30from PyQt5.QtGui import QColor, QKeySequence 31from PyQt5.QtWidgets import QAction, QApplication, QProgressDialog 32 33import appinfo 34 35 36def saveDialogSize(dialog, key, default=QSize()): 37 """Makes the size of a QDialog persistent. 38 39 Resizes a QDialog from the setting saved in QSettings().value(key), 40 defaulting to the optionally specified default size, and stores the 41 size of the dialog at its finished() signal. 42 43 Call this method at the end of the dialog constructor, when its 44 widgets are instantiated. 45 46 """ 47 size = QSettings().value(key, default, QSize) 48 if size: 49 dialog.resize(size) 50 dialogref = weakref.ref(dialog) 51 def save(): 52 dialog = dialogref() 53 if dialog: 54 QSettings().setValue(key, dialog.size()) 55 dialog.finished.connect(save) 56 57 58@contextlib.contextmanager 59def signalsBlocked(*objs): 60 """Blocks the signals of the given QObjects and then returns a contextmanager""" 61 blocks = [obj.blockSignals(True) for obj in objs] 62 try: 63 yield 64 finally: 65 for obj, block in zip(objs, blocks): 66 obj.blockSignals(block) 67 68 69@contextlib.contextmanager 70def deleteLater(*qobjs): 71 """Performs code and calls deleteLater() when done on the specified QObjects.""" 72 try: 73 yield 74 finally: 75 for obj in qobjs: 76 obj.deleteLater() 77 78 79def addAccelerators(actions, used=[]): 80 """Adds accelerators to the list of QActions (or QLabels used as buddy). 81 82 Actions that have accelerators are skipped, the accelerators that they use 83 are not used. This can be used for e.g. menus that are created on the fly. 84 85 used is a sequence of already used accelerators (in lower case). 86 87 """ 88 # filter out the actions that already have an accelerator 89 todo = [] 90 used = set(used) 91 for a in actions: 92 if a.text(): 93 accel = getAccelerator(a.text()) 94 used.add(accel) if accel else todo.append(a) 95 96 def finditers(action): 97 """Yields two-tuples (priority, re.finditer object). 98 99 The finditer object finds suitable accelerator positions. 100 The priority can be used if multiple actions want the same shortcut. 101 102 """ 103 text = action.text() 104 if isinstance(action, QAction) and not action.shortcut().isEmpty(): 105 # if the action has a shortcut with A-Z or 0-9, match that character 106 shortcut = action.shortcut()[action.shortcut().count()-1] 107 key = shortcut & ~Qt.ALT & ~Qt.SHIFT & ~Qt.CTRL & ~Qt.META 108 if 48 < key < 58 or 64 < key < 91 or 96 < key < 123: 109 yield 0, re.finditer(r'\b{0:c}'.format(key), text, re.I) 110 yield 1, re.finditer(r'\b\w', text) 111 yield 2, re.finditer(r'\B\w', text) 112 113 def find(action): 114 """Yields three-tuples (priority, pos, accel) from finditers().""" 115 for prio, matches in finditers(action): 116 for m in matches: 117 yield prio, m.start(), m.group().lower() 118 119 todo = [(a, find(a)) for a in todo] 120 121 while todo: 122 # just pick the first accel for every action 123 accels = {} 124 for a, source in todo: 125 for prio, pos, accel in source: 126 if accel not in used: 127 accels.setdefault(accel, []).append((prio, pos, a, source)) 128 break 129 130 # now, fore every accel, if more than one action wants the same accel, 131 # pick the action with the first priority or position, and try again the 132 # other actions. 133 todo = [] 134 used.update(accels) 135 for action_list in accels.values(): 136 action_list.sort(key=lambda i: i[:2]) 137 pos, a = action_list[0][1:3] 138 a.setText(a.text()[:pos] + '&' + a.text()[pos:]) 139 todo.extend((a, source) for prio, pos, a, source in action_list[1:]) 140 141 142def getAccelerator(text): 143 """Returns the accelerator (in lower case) contained in the text, if any. 144 145 An accelerator is a character preceded by an ampersand &. 146 147 """ 148 m = re.search(r'&(\w)', text.replace('&&', '')) 149 if m: 150 return m.group(1).lower() 151 152 153def removeAccelerator(text): 154 """Removes accelerator ampersands from a QAction.text() string.""" 155 return text.replace('&&', '\0').replace('&', '').replace('\0', '&') 156 157 158def removeShortcut(action, key): 159 """Removes matching QKeySequence from the list of the action.""" 160 key = QKeySequence(key) 161 shortcuts = action.shortcuts() 162 for s in action.shortcuts(): 163 if key.matches(s) or s.matches(key): 164 shortcuts.remove(s) 165 action.setShortcuts(shortcuts) 166 167 168def addcolor(color, r, g, b): 169 """Adds r, g and b values to the given color and returns a new QColor instance.""" 170 r += color.red() 171 g += color.green() 172 b += color.blue() 173 d = max(r, g, b) - 255 174 if d > 0: 175 r = max(0, r - d) 176 g = max(0, g - d) 177 b = max(0, b - d) 178 return QColor(r, g, b) 179 180 181def mixcolor(color1, color2, mix): 182 """Returns a QColor as if color1 is painted on color2 with alpha value mix (0.0 - 1.0).""" 183 r1, g1, b1 = color1.red(), color1.green(), color1.blue() 184 r2, g2, b2 = color2.red(), color2.green(), color2.blue() 185 r = r1 * mix + r2 * (1 - mix) 186 g = g1 * mix + g2 * (1 - mix) 187 b = b1 * mix + b2 * (1 - mix) 188 return QColor(r, g, b) 189 190 191@contextlib.contextmanager 192def busyCursor(cursor=Qt.WaitCursor, processEvents=True): 193 """Performs the contained code using a busy cursor. 194 195 The default cursor used is Qt.WaitCursor. 196 If processEvents is True (the default), QApplication.processEvents() 197 will be called once before the contained code is executed. 198 199 """ 200 QApplication.setOverrideCursor(cursor) 201 processEvents and QApplication.processEvents() 202 try: 203 yield 204 finally: 205 QApplication.restoreOverrideCursor() 206 207 208def waitForSignal(signal, message="", timeout=0): 209 """Waits (max timeout msecs if given) for a signal to be emitted. 210 211 It the waiting lasts more than 2 seconds, a progress dialog is displayed 212 with the message. 213 214 Returns True if the signal was emitted. 215 Return False if the wait timed out or the dialog was canceled by the user. 216 217 """ 218 loop = QEventLoop() 219 dlg = QProgressDialog(minimum=0, maximum=0, labelText=message) 220 dlg.setWindowTitle(appinfo.appname) 221 dlg.setWindowModality(Qt.ApplicationModal) 222 QTimer.singleShot(2000, dlg.show) 223 dlg.canceled.connect(loop.quit) 224 if timeout: 225 QTimer.singleShot(timeout, dlg.cancel) 226 stop = lambda: loop.quit() 227 signal.connect(stop) 228 loop.exec_() 229 signal.disconnect(stop) 230 dlg.hide() 231 dlg.deleteLater() 232 return not dlg.wasCanceled() 233 234 235