1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2025-03-10 05:34:36 +00:00

Add working zoom functions to GraphicsView viewers.

This commit is contained in:
glubsy 2020-06-28 03:31:03 +02:00
parent 9f15139d5f
commit 370b582c9b
2 changed files with 376 additions and 111 deletions

View File

@ -83,7 +83,7 @@ class DetailsDialog(DetailsDialogBase):
# self.horizontalLayout.setColumnStretch(3,0) # self.horizontalLayout.setColumnStretch(3,0)
self.horizontalLayout.setSpacing(1) self.horizontalLayout.setSpacing(1)
self.selectedImageViewer = ScrollAreaImageViewer(self, "selectedImage") self.selectedImageViewer = GraphicsViewViewer(self, "selectedImage")
# self.selectedImage = QLabel(self) # self.selectedImage = QLabel(self)
# sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) # sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
# sizePolicy.setHorizontalStretch(0) # sizePolicy.setHorizontalStretch(0)
@ -100,6 +100,11 @@ class DetailsDialog(DetailsDialogBase):
# self.horizontalLayout.addItem(QSpacerItem(5,0, QSizePolicy.Minimum), # self.horizontalLayout.addItem(QSpacerItem(5,0, QSizePolicy.Minimum),
# 1, 3, 1, 1, Qt.Alignment(Qt.AlignRight)) # 1, 3, 1, 1, Qt.Alignment(Qt.AlignRight))
# FIXME make a subclass to initialize buttons later
# FIXME use qwidgetaction to make the popup on resize work -> QWidgetAction::createWidget()
# FIXME figure out why margins are changing when the window is updating (after Normal Size, on resize)
# it seems toggling the scrollbars reduce viewport size and messes up sizeHint returned?
# thus shrinking the space available for the toolbar?
self.verticalToolBar = QToolBar(self) self.verticalToolBar = QToolBar(self)
# self.verticalToolBar.setMaximumWidth(10) # self.verticalToolBar.setMaximumWidth(10)
self.verticalToolBar.setOrientation(Qt.Orientation(Qt.Vertical)) self.verticalToolBar.setOrientation(Qt.Orientation(Qt.Vertical))
@ -151,11 +156,12 @@ class DetailsDialog(DetailsDialogBase):
self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignCenter) self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignCenter)
self.referenceImageViewer = ScrollAreaImageViewer(self, "referenceImage") self.referenceImageViewer = GraphicsViewViewer(self, "referenceImage")
# self.referenceImage = QLabel(self) # self.referenceImage = QLabel(self)
# sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) # sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
# sizePolicy.setHorizontalStretch(0) # sizePolicy.setHorizontalStretch(0)
# sizePolicy.setVerticalStretch(0) # sizePolicy.setVerticalStretch(0)
# self.verticalToolBar.setSizePolicy(sizePolicy)
# sizePolicy.setHeightForWidth( # sizePolicy.setHeightForWidth(
# self.referenceImage.sizePolicy().hasHeightForWidth() # self.referenceImage.sizePolicy().hasHeightForWidth()
# ) # )
@ -217,6 +223,8 @@ class DetailsDialog(DetailsDialogBase):
self) self)
def _update(self): def _update(self):
if self.vController is None: # Not yet constructed!
return
if not self.app.model.selected_dupes: if not self.app.model.selected_dupes:
# No item from the model, disable and clear everything. # No item from the model, disable and clear everything.
self.vController.resetViewersState() self.vController.resetViewersState()
@ -225,8 +233,6 @@ class DetailsDialog(DetailsDialogBase):
group = self.app.model.results.get_group_of_duplicate(dupe) group = self.app.model.results.get_group_of_duplicate(dupe)
ref = group.ref ref = group.ref
if self.vController is None: # Not yet constructed!
return
self.vController.updateView(ref, dupe, group) self.vController.updateView(ref, dupe, group)
# --- Override # --- Override
@ -277,11 +283,11 @@ class DetailsDialog(DetailsDialogBase):
@pyqtSlot() @pyqtSlot()
def zoomIn(self): def zoomIn(self):
self.vController.scaleImagesBy(1.25) self.vController.zoomIn()
@pyqtSlot() @pyqtSlot()
def zoomOut(self): def zoomOut(self):
self.vController.scaleImagesBy(0.8) self.vController.zoomOut()
@pyqtSlot() @pyqtSlot()
def zoomBestFit(self): def zoomBestFit(self):

View File

@ -3,7 +3,7 @@
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtCore import QObject, Qt, QSize, QRectF, QPointF, QPoint, pyqtSlot, pyqtSignal, QEvent from PyQt5.QtCore import QObject, Qt, QSize, QRectF, QPointF, QPoint, pyqtSlot, pyqtSignal, QEvent
from PyQt5.QtGui import QPixmap, QPainter, QPalette, QCursor from PyQt5.QtGui import QPixmap, QPainter, QPalette, QCursor, QTransform
from PyQt5.QtWidgets import ( QLabel, QSizePolicy, QWidget, QScrollArea, from PyQt5.QtWidgets import ( QLabel, QSizePolicy, QWidget, QScrollArea,
QScrollBar, QApplication, QAbstractScrollArea ) QScrollBar, QApplication, QAbstractScrollArea )
@ -62,6 +62,7 @@ class BaseController(QObject):
self.centerViews(same_group and self.referencePixmap.isNull()) self.centerViews(same_group and self.referencePixmap.isNull())
def updateBothImages(self, same_group=False): def updateBothImages(self, same_group=False):
# FIXME this is called on every resize event,
ignore_update = self.referencePixmap.isNull() ignore_update = self.referencePixmap.isNull()
if ignore_update: if ignore_update:
self.selectedViewer.ignore_signal = True self.selectedViewer.ignore_signal = True
@ -79,7 +80,7 @@ class BaseController(QObject):
self.selectedViewer.ignore_signal = False self.selectedViewer.ignore_signal = False
def _updateImage(self, pixmap, scaledpixmap, viewer, target_size=None, same_group=False): def _updateImage(self, pixmap, scaledpixmap, viewer, target_size=None, same_group=False):
# If not same_group, we need full update""" # FIXME this is called on every resize event, split into a separate function
if pixmap.isNull(): if pixmap.isNull():
# disable the blank widget. # disable the blank widget.
viewer.setImage(pixmap) viewer.setImage(pixmap)
@ -91,6 +92,7 @@ class BaseController(QObject):
viewer.setImage(pixmap) viewer.setImage(pixmap)
return target_size return target_size
# zoomed in state, expand # zoomed in state, expand
# only if not same_group, we need full update
scaledpixmap = pixmap.scaled( scaledpixmap = pixmap.scaled(
target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation) target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
else: else:
@ -109,8 +111,15 @@ class BaseController(QObject):
self.scaledReferencePixmap = QPixmap() self.scaledReferencePixmap = QPixmap()
self.setBestFit(True) self.setBestFit(True)
self.current_scale = 1.0 self.current_scale = 1.0
self.selectedViewer.current_scale = 1.0
self.referenceViewer.current_scale = 1.0
self.selectedViewer.resetCenter() self.selectedViewer.resetCenter()
self.referenceViewer.resetCenter() self.referenceViewer.resetCenter()
self.selectedViewer.scaleAt(1.0)
self.referenceViewer.scaleAt(1.0)
self.centerViews() self.centerViews()
#FIXME move buttons somwhere else #FIXME move buttons somwhere else
@ -128,8 +137,12 @@ class BaseController(QObject):
self.scaledReferencePixmap = QPixmap() self.scaledReferencePixmap = QPixmap()
self.setBestFit(True) self.setBestFit(True)
self.current_scale = 1.0 self.current_scale = 1.0
self.selectedViewer.current_scale = 1.0
self.referenceViewer.current_scale = 1.0
self.selectedViewer.resetCenter() self.selectedViewer.resetCenter()
self.referenceViewer.resetCenter() self.referenceViewer.resetCenter()
self.selectedViewer.scaleAt(1.0)
self.referenceViewer.scaleAt(1.0)
self.centerViews() self.centerViews()
#FIXME move buttons somwhere else #FIXME move buttons somwhere else
@ -144,6 +157,14 @@ class BaseController(QObject):
self.referenceViewer.setImage(self.referencePixmap) # null self.referenceViewer.setImage(self.referencePixmap) # null
self.referenceViewer.setEnabled(False) self.referenceViewer.setEnabled(False)
@pyqtSlot()
def zoomIn(self):
self.scaleImagesBy(1.25)
@pyqtSlot()
def zoomOut(self):
self.scaleImagesBy(0.8)
@pyqtSlot(float) @pyqtSlot(float)
def scaleImagesBy(self, factor): def scaleImagesBy(self, factor):
"""Compute new scale from factor and scale.""" """Compute new scale from factor and scale."""
@ -171,6 +192,8 @@ class BaseController(QObject):
"""Setup before scaling to bestfit""" """Setup before scaling to bestfit"""
self.setBestFit(True) self.setBestFit(True)
self.current_scale = 1.0 self.current_scale = 1.0
self.selectedViewer.current_scale = 1.0
self.referenceViewer.current_scale = 1.0
self.selectedViewer.scaleAt(1.0) self.selectedViewer.scaleAt(1.0)
self.referenceViewer.scaleAt(1.0) self.referenceViewer.scaleAt(1.0)
@ -301,7 +324,7 @@ class ScrollAreaController(BaseController):
def scaleImagesBy(self, factor): def scaleImagesBy(self, factor):
super().scaleImagesBy(factor) super().scaleImagesBy(factor)
# The other is automatically updated via sigals # The other is automatically updated via sigals
self.selectedViewer.adjustScrollBarsFactor(factor) # self.selectedViewer.adjustScrollBarsFactor(factor)
@pyqtSlot() @pyqtSlot()
def ScaleToBestFit(self): def ScaleToBestFit(self):
@ -318,6 +341,11 @@ class GraphicsViewController(BaseController):
def __init__(self, selectedViewer, referenceViewer, parent): def __init__(self, selectedViewer, referenceViewer, parent):
super().__init__(selectedViewer, referenceViewer, parent) super().__init__(selectedViewer, referenceViewer, parent)
def _setupConnections(self):
super()._setupConnections()
self.selectedViewer.connectScrollBars()
self.referenceViewer.connectScrollBars()
@pyqtSlot() @pyqtSlot()
def syncCenters(self): def syncCenters(self):
if self.sender() is self.referenceViewer: if self.sender() is self.referenceViewer:
@ -325,6 +353,179 @@ class GraphicsViewController(BaseController):
else: else:
self.referenceViewer.setCenter(self.selectedViewer.getCenter()) self.referenceViewer.setCenter(self.selectedViewer.getCenter())
@pyqtSlot(float, QPointF)
def onMouseWheel(self, factor, newCenter):
self.current_scale *= factor
if self.sender() is self.referenceViewer:
self.selectedViewer.scaleBy(factor)
self.selectedViewer.setCenter(self.referenceViewer.getCenter())
else:
self.referenceViewer.scaleBy(factor)
self.referenceViewer.setCenter(self.selectedViewer.getCenter())
# self.selectedViewer.adjustScrollBarsScaled(delta)
# Signal from scrollbars will automatically change the other:
# self.referenceViewer.adjustScrollBarsScaled(delta)
@pyqtSlot(int)
def onVScrollBarChanged(self, value):
if self.sender() is self.referenceViewer._verticalScrollBar:
if not self.selectedViewer.ignore_signal:
self.selectedViewer._verticalScrollBar.setValue(value)
else:
if not self.referenceViewer.ignore_signal:
self.referenceViewer._verticalScrollBar.setValue(value)
@pyqtSlot(int)
def onHScrollBarChanged(self, value):
if self.sender() is self.referenceViewer._horizontalScrollBar:
if not self.selectedViewer.ignore_signal:
self.selectedViewer._horizontalScrollBar.setValue(value)
else:
if not self.referenceViewer.ignore_signal:
self.referenceViewer._horizontalScrollBar.setValue(value)
@pyqtSlot()
def swapPixmaps(self):
self.referenceViewer._pixmap.swap(self.selectedViewer._pixmap)
self.referenceViewer.setCachedPixmap()
self.selectedViewer.setCachedPixmap()
@pyqtSlot()
def ScaleToBestFit(self):
"""Setup before scaling to bestfit"""
self.setBestFit(True)
self.current_scale = 1.0
self.selectedViewer.fitScale()
self.referenceViewer.fitScale()
self.parent.buttonBestFit.setEnabled(False)
self.parent.buttonZoomOut.setEnabled(False)
self.parent.buttonZoomIn.setEnabled(False)
self.parent.buttonNormalSize.setEnabled(True)
def updateView(self, ref, dupe, group):
# Keep current scale accross dupes from the same group
same_group = True
if group != self.cached_group:
same_group = False
self.resetState()
self.cached_group = group
self.selectedPixmap = QPixmap(str(dupe.path))
if ref is dupe: # currently selected file is the actual reference file
self.referencePixmap = QPixmap()
self.parent.buttonImgSwap.setEnabled(False)
self.parent.buttonNormalSize.setEnabled(True)
else:
self.referencePixmap = QPixmap(str(ref.path))
self.parent.buttonImgSwap.setEnabled(True)
self.parent.buttonNormalSize.setEnabled(True)
self.selectedViewer.setImage(self.selectedPixmap)
self.referenceViewer.setImage(self.referencePixmap)
self.updateBothImages(same_group)
self.centerViews(same_group and self.referencePixmap.isNull())
def updateBothImages(self, same_group=False):
"""This is called only during resize events and while bestFit."""
ignore_update = self.referencePixmap.isNull()
if ignore_update:
self.selectedViewer.ignore_signal = True
self._updateFitImage(
self.selectedPixmap, self.selectedViewer)
self._updateFitImage(
self.referencePixmap, self.referenceViewer)
if ignore_update:
self.selectedViewer.ignore_signal = False
def _updateFitImage(self, pixmap, viewer):
# If not same_group, we need full update"""
if pixmap.isNull():
return
if viewer.bestFit:
viewer.fitScale()
def resetState(self):
"""Only called when the group of dupes has changed. We reset our
controller internal state and buttons, center view on viewers."""
self.selectedPixmap = QPixmap()
self.referencePixmap = QPixmap()
self.setBestFit(True)
self.current_scale = 1.0
self.selectedViewer.current_scale = 1.0
self.referenceViewer.current_scale = 1.0
self.selectedViewer.resetCenter()
self.referenceViewer.resetCenter()
self.selectedViewer.fitScale()
self.referenceViewer.fitScale()
# self.centerViews()
#FIXME move buttons somwhere else
self.parent.buttonZoomIn.setEnabled(False)
self.parent.buttonZoomOut.setEnabled(False)
self.parent.buttonBestFit.setEnabled(False) # active mode by default
self.parent.buttonNormalSize.setEnabled(True)
def resetViewersState(self):
"""No item from the model, disable and clear everything."""
# only called by the details dialog
self.selectedPixmap = QPixmap()
self.scaledSelectedPixmap = QPixmap()
self.referencePixmap = QPixmap()
self.scaledReferencePixmap = QPixmap()
self.setBestFit(True)
self.current_scale = 1.0
self.selectedViewer.current_scale = 1.0
self.referenceViewer.current_scale = 1.0
self.selectedViewer.resetCenter()
self.referenceViewer.resetCenter()
# self.centerViews()
#FIXME move buttons somwhere else
self.parent.buttonZoomIn.setEnabled(False)
self.parent.buttonZoomOut.setEnabled(False)
self.parent.buttonBestFit.setEnabled(False) # active mode by default
self.parent.buttonImgSwap.setEnabled(False)
self.parent.buttonNormalSize.setEnabled(False)
self.selectedViewer.setImage(self.selectedPixmap) # null
self.selectedViewer.setEnabled(False)
self.referenceViewer.setImage(self.referencePixmap) # null
self.referenceViewer.setEnabled(False)
@pyqtSlot(float)
def scaleImagesBy(self, factor):
self.selectedViewer.updateCenterPoint()
self.referenceViewer.updateCenterPoint()
super().scaleImagesBy(factor)
# self.selectedViewer.setNewCenter(self.selectedViewer._scene.sceneRect().center())
# self.selectedViewer._centerPoint = self.selectedViewer.viewport().rect().center()
# self.referenceViewer._mousePanningDelta = self.selectedViewer._mousePanningDelta
# # self.selectedViewer._mousePanningDelta = self.referenceViewer._mousePanningDelta
# self.selectedViewer.adjustScrollBarsAuto()
# self.referenceViewer.adjustScrollBarsAuto()
self.selectedViewer.centerOn(self.selectedViewer._centerPoint)
# self.selectedViewer.updateCenterPoint()
# self.referenceViewer.setCenter(self.selectedViewer.getCenter())
# self.selectedViewer.setCenter(self.referenceViewer.getCenter())
# self.referenceViewer.setCenter(self.selectedViewer.getCenter())
# The other is automatically updated via sigals
# self.selectedViewer.adjustScrollBarsFactor(factor)
class QWidgetImageViewer(QWidget): class QWidgetImageViewer(QWidget):
"""Use a QPixmap, but no scrollbars.""" """Use a QPixmap, but no scrollbars."""
@ -340,7 +541,7 @@ class QWidgetImageViewer(QWidget):
self._rect = QRectF() self._rect = QRectF()
self._lastMouseClickPoint = QPointF() self._lastMouseClickPoint = QPointF()
self._mousePanningDelta = QPointF() self._mousePanningDelta = QPointF()
self._current_scale = 1.0 self.current_scale = 1.0
self._drag = False self._drag = False
self._dragConnection = None self._dragConnection = None
self._wheelConnection = None self._wheelConnection = None
@ -374,7 +575,7 @@ class QWidgetImageViewer(QWidget):
def paintEvent(self, event): def paintEvent(self, event):
painter = QPainter(self) painter = QPainter(self)
painter.translate(self.rect().center()) painter.translate(self.rect().center())
painter.scale(self._current_scale, self._current_scale) painter.scale(self.current_scale, self.current_scale)
painter.translate(self._mousePanningDelta) painter.translate(self._mousePanningDelta)
painter.drawPixmap(self._rect.topLeft(), self._pixmap) painter.drawPixmap(self._rect.topLeft(), self._pixmap)
@ -382,7 +583,6 @@ class QWidgetImageViewer(QWidget):
""" Resets origin """ """ Resets origin """
# Make sure we are not still panning around # Make sure we are not still panning around
self._mousePanningDelta = QPointF() self._mousePanningDelta = QPointF()
self.scaleAt(1.0)
self.update() self.update()
def changeEvent(self, event): def changeEvent(self, event):
@ -415,7 +615,7 @@ class QWidgetImageViewer(QWidget):
return return
self._mousePanningDelta += (event.pos() - self._lastMouseClickPoint) \ self._mousePanningDelta += (event.pos() - self._lastMouseClickPoint) \
* 1.0 / self._current_scale * 1.0 / self.current_scale
self._lastMouseClickPoint = event.pos() self._lastMouseClickPoint = event.pos()
if self._drag: if self._drag:
self.mouseDragged.emit(self._mousePanningDelta) self.mouseDragged.emit(self._mousePanningDelta)
@ -437,11 +637,11 @@ class QWidgetImageViewer(QWidget):
return return
if event.angleDelta().y() > 0: if event.angleDelta().y() > 0:
if self._current_scale > MAX_SCALE: if self.current_scale > MAX_SCALE:
return return
self.mouseWheeled.emit(1.25) # zoom-in self.mouseWheeled.emit(1.25) # zoom-in
else: else:
if self._current_scale < MIN_SCALE: if self.current_scale < MIN_SCALE:
return return
self.mouseWheeled.emit(0.8) # zoom-out self.mouseWheeled.emit(0.8) # zoom-out
@ -467,11 +667,11 @@ class QWidgetImageViewer(QWidget):
return True if not self.pixmap.isNull() else False return True if not self.pixmap.isNull() else False
def scaleBy(self, factor): def scaleBy(self, factor):
self._current_scale *= factor self.current_scale *= factor
self.update() self.update()
def scaleAt(self, scale): def scaleAt(self, scale):
self._current_scale = scale self.current_scale = scale
self.update() self.update()
def sizeHint(self): def sizeHint(self):
@ -480,7 +680,7 @@ class QWidgetImageViewer(QWidget):
@pyqtSlot() @pyqtSlot()
def scaleToNormalSize(self): def scaleToNormalSize(self):
"""Called when the pixmap is set back to original size.""" """Called when the pixmap is set back to original size."""
self._current_scale = 1.0 self.current_scale = 1.0
self.update() self.update()
@pyqtSlot(QPointF) @pyqtSlot(QPointF)
@ -495,23 +695,23 @@ class ScalablePixmap(QWidget):
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
self._pixmap = QPixmap() self._pixmap = QPixmap()
self._current_scale = 1.0 self.current_scale = 1.0
def paintEvent(self, event): def paintEvent(self, event):
painter = QPainter(self) painter = QPainter(self)
# painter.translate(self.rect().center()) # painter.translate(self.rect().center())
# painter.setRenderHint(QPainter.Antialiasing, False) # painter.setRenderHint(QPainter.Antialiasing, False)
# scale the coordinate system: # scale the coordinate system:
painter.scale(self._current_scale, self._current_scale) painter.scale(self.current_scale, self.current_scale)
painter.drawPixmap(self.rect().topLeft(), self._pixmap) #same as (0,0, self.pixmap) painter.drawPixmap(self.rect().topLeft(), self._pixmap) #same as (0,0, self.pixmap)
# print(f"ScalableWidget paintEvent scale {self._current_scale}") # print(f"ScalableWidget paintEvent scale {self.current_scale}")
def setPixmap(self, pixmap): def setPixmap(self, pixmap):
self._pixmap = pixmap self._pixmap = pixmap
# self.update() # self.update()
def sizeHint(self): def sizeHint(self):
return self._pixmap.size() * self._current_scale return self._pixmap.size() * self.current_scale
# return self._pixmap.size() # return self._pixmap.size()
def minimumSizeHint(self): def minimumSizeHint(self):
@ -535,7 +735,7 @@ class ScrollAreaImageViewer(QScrollArea):
self._rect = QRectF() self._rect = QRectF()
self._lastMouseClickPoint = QPointF() self._lastMouseClickPoint = QPointF()
self._mousePanningDelta = QPoint() self._mousePanningDelta = QPoint()
self._current_scale = 1.0 self.current_scale = 1.0
self._drag = False self._drag = False
self._dragConnection = None self._dragConnection = None
self._wheelConnection = None self._wheelConnection = None
@ -606,6 +806,7 @@ class ScrollAreaImageViewer(QScrollArea):
def connectScrollBars(self): def connectScrollBars(self):
"""Only call once controller is connected.""" """Only call once controller is connected."""
# Cyclic connections are handled by Qt # Cyclic connections are handled by Qt
return
self._verticalScrollBar.valueChanged.connect( self._verticalScrollBar.valueChanged.connect(
self.controller.onVScrollBarChanged, Qt.UniqueConnection) self.controller.onVScrollBarChanged, Qt.UniqueConnection)
self._horizontalScrollBar.valueChanged.connect( self._horizontalScrollBar.valueChanged.connect(
@ -651,19 +852,19 @@ class ScrollAreaImageViewer(QScrollArea):
if self.bestFit: if self.bestFit:
event.ignore() event.ignore()
return return
oldScale = self._current_scale oldScale = self.current_scale
if event.angleDelta().y() > 0: # zoom-in if event.angleDelta().y() > 0: # zoom-in
if oldScale < MAX_SCALE: if oldScale < MAX_SCALE:
self._current_scale *= 1.25 self.current_scale *= 1.25
else: else:
if oldScale > MIN_SCALE: # zoom-out if oldScale > MIN_SCALE: # zoom-out
self._current_scale *= 0.8 self.current_scale *= 0.8
if oldScale == self._current_scale: if oldScale == self.current_scale:
return return
deltaToPos = (event.position() / oldScale) - (self.label.pos() / oldScale) deltaToPos = (event.position() / oldScale) - (self.label.pos() / oldScale)
delta = (deltaToPos * self._current_scale) - (deltaToPos * oldScale) delta = (deltaToPos * self.current_scale) - (deltaToPos * oldScale)
self.mouseWheeled.emit(self._current_scale, delta) self.mouseWheeled.emit(self.current_scale, delta)
def setImage(self, pixmap): def setImage(self, pixmap):
self._pixmap = pixmap self._pixmap = pixmap
@ -680,7 +881,7 @@ class ScrollAreaImageViewer(QScrollArea):
def centerViewAndUpdate(self): def centerViewAndUpdate(self):
self._rect = self.label.rect() self._rect = self.label.rect()
self.label.rect().translate(-self._rect.center()) self.label.rect().translate(-self._rect.center())
self.label._current_scale = self._current_scale self.label.current_scale = self.current_scale
self.label.update() self.label.update()
# self.viewport().update() # self.viewport().update()
@ -693,8 +894,8 @@ class ScrollAreaImageViewer(QScrollArea):
return True if not self.pixmap.isNull() else False return True if not self.pixmap.isNull() else False
def scaleBy(self, factor): def scaleBy(self, factor):
self._current_scale *= factor self.current_scale *= factor
# print(f"scaleBy(factor={factor}) current_scale={self._current_scale}") # print(f"scaleBy(factor={factor}) current_scale={self.current_scale}")
# This kills my computer when scaling up! DO NOT USE! # This kills my computer when scaling up! DO NOT USE!
# self._pixmap = self._pixmap.scaled( # self._pixmap = self._pixmap.scaled(
@ -713,7 +914,7 @@ class ScrollAreaImageViewer(QScrollArea):
self.label.resize(self.label.size().__imul__(factor)) self.label.resize(self.label.size().__imul__(factor))
# self.label.updateGeometry() # self.label.updateGeometry()
self.label._current_scale = self._current_scale self.label.current_scale = self.current_scale
self.label.update() self.label.update()
# Center view on zoom change(?) same as imageLabel->resize(imageLabel->pixmap()->size()) # Center view on zoom change(?) same as imageLabel->resize(imageLabel->pixmap()->size())
# self.label.adjustSize() # self.label.adjustSize()
@ -732,9 +933,9 @@ class ScrollAreaImageViewer(QScrollArea):
# self.adjustScrollBarCentered() # self.adjustScrollBarCentered()
def scaleAt(self, scale): def scaleAt(self, scale):
self._current_scale = scale self.current_scale = scale
self.label.resize(self._pixmap.size().__imul__(scale)) self.label.resize(self._pixmap.size().__imul__(scale))
self.label._current_scale = scale self.label.current_scale = scale
self.label.update() self.label.update()
# self.label.adjustSize() # self.label.adjustSize()
@ -783,7 +984,7 @@ class ScrollAreaImageViewer(QScrollArea):
def resetCenter(self): def resetCenter(self):
""" Resets origin """ """ Resets origin """
self._mousePanningDelta = QPoint() self._mousePanningDelta = QPoint()
self._current_scale = 1.0 self.current_scale = 1.0
# self.scaleBy(1.0) # self.scaleBy(1.0)
# self.label.update() # already called in scaleBy # self.label.update() # already called in scaleBy
@ -794,10 +995,10 @@ class ScrollAreaImageViewer(QScrollArea):
return self._lastMouseClickPoint return self._lastMouseClickPoint
def sizeHint(self): def sizeHint(self):
return self._pixmap.rect().size() return self.viewport().rect().size()
def viewportSizeHint(self): # def viewportSizeHint(self):
return self._pixmap.rect().size() # return self.viewport().rect().size()
@pyqtSlot() @pyqtSlot()
def scaleToNormalSize(self): def scaleToNormalSize(self):
@ -827,9 +1028,9 @@ class ScrollAreaImageViewer(QScrollArea):
from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
class GraphicsViewViewer(QGraphicsView): class GraphicsViewViewer(QGraphicsView):
"""Re-Implementation.""" """Re-Implementation using a more full fledged class."""
mouseDragged = pyqtSignal() mouseDragged = pyqtSignal()
mouseWheeled = pyqtSignal(bool) mouseWheeled = pyqtSignal(float, QPointF)
def __init__(self, parent, name=""): def __init__(self, parent, name=""):
super().__init__(parent) super().__init__(parent)
@ -841,7 +1042,9 @@ class GraphicsViewViewer(QGraphicsView):
self._lastMouseClickPoint = QPointF() self._lastMouseClickPoint = QPointF()
self._mousePanningDelta = QPointF() self._mousePanningDelta = QPointF()
self._scaleFactor = 1.3 self._scaleFactor = 1.3
self._current_scale = 1.0 self.zoomInFactor = self._scaleFactor
self.zoomOutFactor = 1.0 / self._scaleFactor
self.current_scale = 1.0
self._drag = False self._drag = False
self._dragConnection = None self._dragConnection = None
self._wheelConnection = None self._wheelConnection = None
@ -849,25 +1052,29 @@ class GraphicsViewViewer(QGraphicsView):
self.wantScrollBars = True self.wantScrollBars = True
self.bestFit = True self.bestFit = True
self.controller = None self.controller = None
self._centerPoint = QPointF(0.0, 0.0) self._centerPoint = QPointF()
self.centerOn(self._centerPoint) self.centerOn(self._centerPoint)
# specific to this class # specific to this class
self._scene = QGraphicsScene() self._scene = QGraphicsScene()
self._scene.setBackgroundBrush(Qt.black)
self._item = QGraphicsPixmapItem() self._item = QGraphicsPixmapItem()
self.setScene(self._scene) self.setScene(self._scene)
self._scene.addItem(self._item) self._scene.addItem(self._item)
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag) self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
self.matrix = QTransform()
self._horizontalScrollBar = self.horizontalScrollBar()
self._verticalScrollBar = self.verticalScrollBar()
self.ignore_signal = False
if self.wantScrollBars: if self.wantScrollBars:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.toggleScrollBars()
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
else: else:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setResizeAnchor(QGraphicsView.AnchorViewCenter) self.setResizeAnchor(QGraphicsView.AnchorViewCenter)
self.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.setAlignment(Qt.AlignCenter)
self.setViewportUpdateMode (QGraphicsView.FullViewportUpdate) self.setViewportUpdateMode (QGraphicsView.FullViewportUpdate)
self.setMouseTracking(True) self.setMouseTracking(True)
@ -877,7 +1084,7 @@ class GraphicsViewViewer(QGraphicsView):
self.controller.syncCenters) self.controller.syncCenters)
if not self._wheelConnection: if not self._wheelConnection:
self._wheelConnection = self.mouseWheeled.connect( self._wheelConnection = self.mouseWheeled.connect(
self.controller.scaleImages) self.controller.onMouseWheel)
def disconnectMouseSignals(self): def disconnectMouseSignals(self):
if self._dragConnection: if self._dragConnection:
@ -887,6 +1094,27 @@ class GraphicsViewViewer(QGraphicsView):
self.mouseWheeled.disconnect() self.mouseWheeled.disconnect()
self._wheelConnection = None self._wheelConnection = None
def connectScrollBars(self):
"""Only call once controller is connected."""
# Cyclic connections are handled by Qt
self._verticalScrollBar.valueChanged.connect(
self.controller.onVScrollBarChanged, Qt.UniqueConnection)
self._horizontalScrollBar.valueChanged.connect(
self.controller.onHScrollBarChanged, Qt.UniqueConnection)
def toggleScrollBars(self, forceOn=False):
if not self.wantScrollBars:
return
# Ensure that it's off on the first run
if self.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded:
if forceOn:
return
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
else:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
def mousePressEvent(self, event): def mousePressEvent(self, event):
if self.bestFit: if self.bestFit:
event.ignore() event.ignore()
@ -897,7 +1125,6 @@ class GraphicsViewViewer(QGraphicsView):
self._drag = False self._drag = False
event.ignore() event.ignore()
return return
self._lastMouseClickPoint = event.pos() self._lastMouseClickPoint = event.pos()
self._app.setOverrideCursor(Qt.ClosedHandCursor) self._app.setOverrideCursor(Qt.ClosedHandCursor)
self.setMouseTracking(True) self.setMouseTracking(True)
@ -905,40 +1132,53 @@ class GraphicsViewViewer(QGraphicsView):
super().mousePressEvent(event) super().mousePressEvent(event)
# event.accept() # event.accept()
def mouseMoveEvent(self, event):
if self.bestFit:
event.ignore()
return
self._centerPoint = self.mapToScene( self.rect().center() )
super().mouseMoveEvent(event)
if self._drag:
self.mouseDragged.emit()
# self._item.update()
def mouseReleaseEvent(self, event): def mouseReleaseEvent(self, event):
if self.bestFit: if self.bestFit:
event.ignore() event.ignore()
return return
if event.button() == Qt.LeftButton: if event.button() == Qt.LeftButton:
self._drag = False self._drag = False
self._app.restoreOverrideCursor() self._app.restoreOverrideCursor()
self.setMouseTracking(False) self.setMouseTracking(False)
self.updateCenterPoint()
super().mouseReleaseEvent(event) super().mouseReleaseEvent(event)
def wheelEvent(self, event): def mouseMoveEvent(self, event):
if self.bestFit: if self.bestFit:
event.ignore() event.ignore()
return return
if self._drag:
delta = (event.pos() - self._lastMouseClickPoint)
self._lastMouseClickPoint = event.pos()
# We can simply rely on the scrollbar updating each other here
# self.mouseDragged.emit()
self.updateCenterPoint()
super().mouseMoveEvent(event)
def updateCenterPoint(self):
self._centerPoint = self.mapToScene( self.viewport().rect().center())
def wheelEvent(self, event):
if self.bestFit or MIN_SCALE > self.current_scale > MAX_SCALE:
event.ignore()
return
pointBeforeScale = QPointF(self.mapToScene(self.mapFromGlobal(QCursor.pos())))
# Get the original screen centerpoint
screenCenter = QPointF(self.mapToScene( self.rect().center() ))
if event.angleDelta().y() > 0: if event.angleDelta().y() > 0:
self.mouseWheeled.emit(True) # zoom-in factor = self.zoomInFactor
else: else:
self.mouseWheeled.emit(False) # zoom-out factor = self.zoomOutFactor
self.scaleBy(factor)
pointAfterScale = QPointF(self.mapToScene( self.mapFromGlobal(QCursor.pos())))
#Get the offset of how the screen moved
offset = pointBeforeScale - pointAfterScale
#Adjust to the new center for correct zooming
newCenter = screenCenter + offset
self.setCenter(newCenter)
self.mouseWheeled.emit(factor, newCenter)
def setImage(self, pixmap): def setImage(self, pixmap):
self._pixmap = pixmap self._pixmap = pixmap
@ -947,14 +1187,15 @@ class GraphicsViewViewer(QGraphicsView):
# self._item.setOffset(offset) # self._item.setOffset(offset)
# self.setSceneRect(offset.x()*4, offset.y()*4, -offset.x()*8, -offset.y()*8) # self.setSceneRect(offset.x()*4, offset.y()*4, -offset.x()*8, -offset.y()*8)
self.translate(1, 1) self.translate(1, 1)
# self._scene.setSceneRect(self._pixmap.rect()) # self._scene.setSceneRect(QRectF(self._pixmap.rect())) # not sure if this works
def centerViewAndUpdate(self): def centerViewAndUpdate(self):
# self._rect = self.sceneRect()
# self._rect.translate(-self._rect.center())
# self._item.update()
# self.viewport().update()
pass pass
def scaleBy(self, factor):
# super().scale(factor, factor)
self.zoom(factor)
def setCenter(self, point): def setCenter(self, point):
self._centerPoint = point self._centerPoint = point
@ -966,59 +1207,77 @@ class GraphicsViewViewer(QGraphicsView):
def resetCenter(self): def resetCenter(self):
""" Resets origin """ """ Resets origin """
self._mousePanningDelta = QPointF() self._mousePanningDelta = QPointF()
self._current_scale = 1.0 self.current_scale = 1.0
self.scaleBy(1.0)
# self.update() # self.update()
self.setCenter(self._scene.sceneRect().center()) # self.setCenter(self._scene.sceneRect().center())
def setNewCenter(self, position): def setNewCenter(self, position):
self._centerPoint = position self._centerPoint = position
self.centerOn(self._centerPoint) self.centerOn(self._centerPoint)
def setCachedPixmap(self):
"""In case we have changed the cached pixmap, reset it."""
self._item.setPixmap(self._pixmap)
self._item.update()
def scaleAt(self, scale):
# self.current_scale = scale
if scale == 1.0:
self.resetScale()
# self.setTransform( QTransform() )
self.scale(scale, scale)
def getScale(self):
return self.transform().m22()
def scaleBy(self, factor):
self.current_scale *= factor
super().scale(factor, factor)
def resetScale(self):
# self.setTransform( QTransform() )
self.resetTransform() # probably same as above
self.setCenter( self.scene().sceneRect().center() )
# self.scaleChanged.emit( self.transform().m22() )
def fitScale(self):
self.bestFit = True
super().fitInView(self._scene.sceneRect(), Qt.KeepAspectRatio )
self.setNewCenter(self._scene.sceneRect().center())
@pyqtSlot() @pyqtSlot()
def scaleToNormalSize(self): def scaleToNormalSize(self):
"""Called when the pixmap is set back to original size.""" """Called when the pixmap is set back to original size."""
self.scaleBy(1.0) # FIXME self.bestFit = False
self.scaleAt(1.0)
super().fitInView(self._scene.sceneRect(), Qt.KeepAspectRatio ) self.toggleScrollBars()
self.setNewCenter(self._scene.sceneRect().center())
self.update() self.update()
# def sizeHint(self): def adjustScrollBarsScaled(self, delta):
# return self._item.rect().size() """After scaling with the mouse, update relative to mouse position."""
self._horizontalScrollBar.setValue(
self._horizontalScrollBar.value() + delta.x())
self._verticalScrollBar.setValue(
self._verticalScrollBar.value() + delta.y())
def sizeHint(self):
return self.viewport().rect().size()
# def viewportSizeHint(self): # def viewportSizeHint(self):
# return self._item.rect().size() # return self.viewport().rect().size()
def zoom_in(self): def adjustScrollBarsFactor(self, factor):
self.zoom(self._scaleFactor) """After scaling, no mouse position, default to center."""
# scrollBar.setMaximum(scrollBar.maximum() - scrollBar.minimum() + scrollBar.pageStep())
def zoom_out(self): self._horizontalScrollBar.setValue(int(factor * self._horizontalScrollBar.value() + \
self.zoom(1.0 / self._scaleFactor) ((factor - 1) * self._horizontalScrollBar.pageStep()/2)))
self._verticalScrollBar.setValue(int(factor * self._verticalScrollBar.value() + \
def zoom(self, factor): ((factor - 1) * self._verticalScrollBar.pageStep()/2)))
#Get the position of the mouse before scaling, in scene coords
pointBeforeScale = QPointF(self.mapToScene(self.mapFromGlobal(QCursor.pos())))
#Get the original screen centerpoint
screenCenter = self.mapToScene( self.rect().center() )
super().scale(factor, factor)
#Get the position after scaling, in scene coords
pointAfterScale = QPointF( self.mapToScene( self.mapFromGlobal(QCursor.pos()) ) )
#Get the offset of how the screen moved
offset = QPointF( pointBeforeScale - pointAfterScale)
#Adjust to the new center for correct zooming
newCenter = QPointF(screenCenter + offset)
self.setNewCenter(newCenter)
# self.updateSceneRect(self._item.rect()) # TEST THIS?
# mouse position has changed!!
# emit mouseMoved( QGraphicsView::mapToScene( event->pos() ) );
# emit mouseMoved( QGraphicsView::mapToScene( mapFromGlobal(QCursor::pos()) ) );
# emit somethingChanged();
def adjustScrollBarsAuto(self):
"""After panning, update accordingly."""
self.horizontalScrollBar().setValue(
self.horizontalScrollBar().value() - self._mousePanningDelta.x())
self.verticalScrollBar().setValue(
self.verticalScrollBar().value() - self._mousePanningDelta.y())