File: qutil.py

package info (click to toggle)
frescobaldi 3.0.0~git20161001.0.eec60717%2Bds1-2
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 19,792 kB
  • ctags: 5,843
  • sloc: python: 37,853; sh: 180; makefile: 69
file content (234 lines) | stat: -rw-r--r-- 7,778 bytes parent folder | download | duplicates (2)
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()