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