File: util.py

package info (click to toggle)
python-qpageview 0.6.2-5
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 780 kB
  • sloc: python: 5,215; makefile: 22
file content (295 lines) | stat: -rw-r--r-- 9,794 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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# -*- coding: utf-8 -*-
#
# This file is part of the qpageview package.
#
# Copyright (c) 2019 - 2019 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.

"""
Small utilities and simple base classes for the qpageview module.
"""


import collections
import contextlib

from PyQt5.QtCore import QPoint, QPointF, QRect, QRectF, QSize, Qt
from PyQt5.QtGui import QBitmap, QMouseEvent, QRegion
from PyQt5.QtWidgets import QApplication


class Rectangular:
    """Defines a Qt-inspired and -based interface for rectangular objects.

    The attributes x, y, width and height default to 0 at the class level
    and can be set and read directly.

    For convenience, Qt-styled methods are available to access and modify these
    attributes.

    """
    x = 0
    y = 0
    width = 0
    height = 0

    def setPos(self, point):
        """Set the x and y coordinates from the given QPoint point."""
        self.x = point.x()
        self.y = point.y()

    def pos(self):
        """Return our x and y coordinates as a QPoint(x, y)."""
        return QPoint(self.x, self.y)

    def setSize(self, size):
        """Set the height and width attributes from the given QSize size."""
        self.width = size.width()
        self.height = size.height()

    def size(self):
        """Return the height and width attributes as a QSize(width, height)."""
        return QSize(self.width, self.height)

    def setGeometry(self, rect):
        """Set our x, y, width and height directly from the given QRect."""
        self.x, self.y, self.width, self.height = rect.getRect()

    def geometry(self):
        """Return our x, y, width and height as a QRect."""
        return QRect(self.x, self.y, self.width, self.height)

    def rect(self):
        """Return QRect(0, 0, width, height)."""
        return QRect(0, 0, self.width, self.height)


class MapToPage:
    """Simple class wrapping a QTransform to map rect and point to page coordinates."""
    def __init__(self, transform):
        self.t = transform

    def rect(self, rect):
        """Convert QRect or QRectF to a QRect in page coordinates."""
        return self.t.mapRect(QRectF(rect)).toRect()

    def point(self, point):
        """Convert QPointF or QPoint to a QPoint in page coordinates."""
        return self.t.map(QPointF(point)).toPoint()


class MapFromPage(MapToPage):
    """Simple class wrapping a QTransform to map rect and point from page to original coordinates."""
    def rect(self, rect):
        """Convert QRect or QRectF to a QRectF in original coordinates."""
        return self.t.mapRect(QRectF(rect))

    def point(self, point):
        """Convert QPointF or QPoint to a QPointF in original coordinates."""
        return self.t.map(QPointF(point))


class LongMousePressMixin:
    """Mixin class to add support for long mouse press to a QWidget.

    To handle a long mouse press event, implement longMousePressEvent().

    """

    #: Whether to enable handling of long mouse presses; set to False to disable
    longMousePressEnabled = True

    #: Allow moving some pixels before a long mouse press is considered a drag
    longMousePressTolerance = 3

    #: How long to presse a mouse button (in msec) for a long press
    longMousePressTime = 800

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._longPressTimer = None
        self._longPressAttrs = None
        self._longPressPos = None

    def _startLongMousePressEvent(self, ev):
        """Start the timer for a QMouseEvent mouse press event."""
        self._cancelLongMousePressEvent()
        self._longPressTimer = self.startTimer(self.longMousePressTime)
        # copy the event's attributes because Qt might reuse the event
        self._longPressAttrs = (ev.type(),
            ev.localPos(), ev.windowPos(), ev.screenPos(),
            ev.button(), ev.buttons(), ev.modifiers())
        self._longPressPos = ev.pos()

    def _checkLongMousePressEvent(self, ev):
        """Cancel the press event if the current event has moved more than 3 pixels."""
        if self._longPressTimer is not None:
            dist = (self._longPressPos - ev.pos()).manhattanLength()
            if dist > self.longMousePressTolerance:
                self._cancelLongMousePressEvent()

    def _cancelLongMousePressEvent(self):
        """Stop the timer for a long mouse press event."""
        if self._longPressTimer is not None:
            self.killTimer(self._longPressTimer)
            self._longPressTimer = None
            self._longPressAttrs = None
            self._longPressPos = None

    def longMousePressEvent(self, ev):
        """Implement this to handle a long mouse press event."""
        pass

    def timerEvent(self, ev):
        """Implemented to check for a long mouse button press."""
        if ev.timerId() == self._longPressTimer:
            event = QMouseEvent(*self._longPressAttrs)
            self._cancelLongMousePressEvent()
            self.longMousePressEvent(event)
        super().timerEvent(ev)

    def mousePressEvent(self, ev):
        """Reimplemented to check for a long mouse button press."""
        if self.longMousePressEnabled:
            self._startLongMousePressEvent(ev)
        super().mousePressEvent(ev)

    def mouseMoveEvent(self, ev):
        """Reimplemented to check for moves during a long press."""
        self._checkLongMousePressEvent(ev)
        super().mouseMoveEvent(ev)

    def mouseReleaseEvent(self, ev):
        """Reimplemented to cancel a long press."""
        self._cancelLongMousePressEvent()
        super().mouseReleaseEvent(ev)


def rotate(matrix, rotation, width, height, dest=False):
    """Rotate matrix inside a rectangular area of width x height.

    The ``matrix`` can be a either a QPainter or a QTransform. The ``rotation``
    is 0, 1, 2 or 3, etc. (``Rotate_0``, ``Rotate_90``, etc...). If ``dest`` is
    True, ``width`` and ``height`` refer to the destination, otherwise to the
    source.

    """
    if rotation & 3:
        if dest or not rotation & 1:
            matrix.translate(width / 2, height / 2)
        else:
            matrix.translate(height / 2, width / 2)
        matrix.rotate(rotation * 90)
        if not dest or not rotation & 1:
            matrix.translate(width / -2, height / -2)
        else:
            matrix.translate(height / -2, width / -2)


def align(w, h, ow, oh, alignment=Qt.AlignCenter):
    """Return (x, y) to align a rect w x h in an outer rectangle ow x oh.

    The alignment can be a combination of the Qt.Alignment flags.
    If w > ow, x = -1; and if h > oh, y = -1.

    """
    if w > ow:
        x = -1
    elif alignment & Qt.AlignHCenter:
        x = (ow - w) // 2
    elif alignment & Qt.AlignRight:
        x = ow - w
    else:
        x = 0
    if h > oh:
        y = -1
    elif alignment & Qt.AlignVCenter:
        y = (oh - h) // 2
    elif alignment & Qt.AlignBottom:
        y = oh - h
    else:
        y = 0
    return x, y


def alignrect(rect, point, alignment=Qt.AlignCenter):
    """Align rect with point according to the alignment.

    The alignment can be a combination of the Qt.Alignment flags.

    """
    rect.moveCenter(point)
    if alignment & Qt.AlignLeft:
        rect.moveLeft(point.x())
    elif alignment & Qt.AlignRight:
        rect.moveRight(point.x())
    if alignment & Qt.AlignTop:
        rect.moveTop(point.y())
    elif alignment & Qt.AlignBottom:
        rect.moveBottom(point.y())


# Found at: https://stackoverflow.com/questions/1986152/why-doesnt-python-have-a-sign-function
def sign(x):
    """Return the sign of x: -1 if x < 0, 0 if x == 0, or 1 if x > 0."""
    return bool(x > 0) - bool(x < 0)


@contextlib.contextmanager
def signalsBlocked(*objs):
    """Block the pyqtSignals of the given QObjects during the context."""
    blocks = [obj.blockSignals(True) for obj in objs]
    try:
        yield
    finally:
        for obj, block in zip(objs, blocks):
            obj.blockSignals(block)


def autoCropRect(image):
    """Return a QRect specifying the contents of the QImage.

    Edges of the image are trimmed if they have the same color.

    """
    # pick the color at most of the corners
    colors = collections.defaultdict(int)
    w, h = image.width(), image.height()
    for x, y in (0, 0), (w - 1, 0), (w - 1, h - 1), (0, h - 1):
        colors[image.pixel(x, y)] += 1
    most = max(colors, key=colors.get)
    # let Qt do the masking work
    mask = image.createMaskFromColor(most)
    return QRegion(QBitmap.fromImage(mask)).boundingRect()


def tempdir():
    """Return a temporary directory that is erased on app quit."""
    import tempfile
    global _tempdir
    try:
        _tempdir
    except NameError:
        name = QApplication.applicationName().translate({ord('/'): None}) or 'qpageview'
        _tempdir = tempfile.mkdtemp(prefix=name + '-')
        import atexit
        import shutil
        @atexit.register
        def remove():
            shutil.rmtree(_tempdir, ignore_errors=True)
    return tempfile.mkdtemp(dir=_tempdir)