Source code for qpageview.util

# -*- 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


[docs]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
[docs] def setPos(self, point): """Set the x and y coordinates from the given QPoint point.""" self.x = point.x() self.y = point.y()
[docs] def pos(self): """Return our x and y coordinates as a QPoint(x, y).""" return QPoint(self.x, self.y)
[docs] def setSize(self, size): """Set the height and width attributes from the given QSize size.""" self.width = size.width() self.height = size.height()
[docs] def size(self): """Return the height and width attributes as a QSize(width, height).""" return QSize(self.width, self.height)
[docs] 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()
[docs] def geometry(self): """Return our x, y, width and height as a QRect.""" return QRect(self.x, self.y, self.width, self.height)
[docs] def rect(self): """Return QRect(0, 0, width, height).""" return QRect(0, 0, self.width, self.height)
[docs]class MapToPage: """Simple class wrapping a QTransform to map rect and point to page coordinates.""" def __init__(self, transform): self.t = transform
[docs] def rect(self, rect): """Convert QRect or QRectF to a QRect in page coordinates.""" return self.t.mapRect(QRectF(rect)).toRect()
[docs] def point(self, point): """Convert QPointF or QPoint to a QPoint in page coordinates.""" return self.t.map(QPointF(point)).toPoint()
[docs]class MapFromPage(MapToPage): """Simple class wrapping a QTransform to map rect and point from page to original coordinates."""
[docs] def rect(self, rect): """Convert QRect or QRectF to a QRectF in original coordinates.""" return self.t.mapRect(QRectF(rect))
[docs] def point(self, point): """Convert QPointF or QPoint to a QPointF in original coordinates.""" return self.t.map(QPointF(point))
[docs]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
[docs] def longMousePressEvent(self, ev): """Implement this to handle a long mouse press event.""" pass
[docs] 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)
[docs] def mousePressEvent(self, ev): """Reimplemented to check for a long mouse button press.""" if self.longMousePressEnabled: self._startLongMousePressEvent(ev) super().mousePressEvent(ev)
[docs] def mouseMoveEvent(self, ev): """Reimplemented to check for moves during a long press.""" self._checkLongMousePressEvent(ev) super().mouseMoveEvent(ev)
[docs] def mouseReleaseEvent(self, ev): """Reimplemented to cancel a long press.""" self._cancelLongMousePressEvent() super().mouseReleaseEvent(ev)
[docs]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)
[docs]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
[docs]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
[docs]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)
[docs]@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)
[docs]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()
[docs]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)