diff --git a/qt/pe/details_dialog.py b/qt/pe/details_dialog.py index e8c1f469..230e0abc 100644 --- a/qt/pe/details_dialog.py +++ b/qt/pe/details_dialog.py @@ -83,7 +83,7 @@ class DetailsDialog(DetailsDialogBase): # self.horizontalLayout.setColumnStretch(3,0) self.horizontalLayout.setSpacing(1) - self.selectedImageViewer = ScrollAreaImageViewer(self, "selectedImage") + self.selectedImageViewer = GraphicsViewViewer(self, "selectedImage") # self.selectedImage = QLabel(self) # sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) # sizePolicy.setHorizontalStretch(0) @@ -100,6 +100,11 @@ class DetailsDialog(DetailsDialogBase): # self.horizontalLayout.addItem(QSpacerItem(5,0, QSizePolicy.Minimum), # 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.setMaximumWidth(10) 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.referenceImageViewer = ScrollAreaImageViewer(self, "referenceImage") + self.referenceImageViewer = GraphicsViewViewer(self, "referenceImage") # self.referenceImage = QLabel(self) - # sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) + # sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) # sizePolicy.setHorizontalStretch(0) # sizePolicy.setVerticalStretch(0) + # self.verticalToolBar.setSizePolicy(sizePolicy) # sizePolicy.setHeightForWidth( # self.referenceImage.sizePolicy().hasHeightForWidth() # ) @@ -217,6 +223,8 @@ class DetailsDialog(DetailsDialogBase): self) def _update(self): + if self.vController is None: # Not yet constructed! + return if not self.app.model.selected_dupes: # No item from the model, disable and clear everything. self.vController.resetViewersState() @@ -225,8 +233,6 @@ class DetailsDialog(DetailsDialogBase): group = self.app.model.results.get_group_of_duplicate(dupe) ref = group.ref - if self.vController is None: # Not yet constructed! - return self.vController.updateView(ref, dupe, group) # --- Override @@ -277,11 +283,11 @@ class DetailsDialog(DetailsDialogBase): @pyqtSlot() def zoomIn(self): - self.vController.scaleImagesBy(1.25) + self.vController.zoomIn() @pyqtSlot() def zoomOut(self): - self.vController.scaleImagesBy(0.8) + self.vController.zoomOut() @pyqtSlot() def zoomBestFit(self): diff --git a/qt/pe/image_viewer.py b/qt/pe/image_viewer.py index b6f12844..f5b8d961 100644 --- a/qt/pe/image_viewer.py +++ b/qt/pe/image_viewer.py @@ -3,7 +3,7 @@ # http://www.gnu.org/licenses/gpl-3.0.html 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, QScrollBar, QApplication, QAbstractScrollArea ) @@ -62,6 +62,7 @@ class BaseController(QObject): self.centerViews(same_group and self.referencePixmap.isNull()) def updateBothImages(self, same_group=False): + # FIXME this is called on every resize event, ignore_update = self.referencePixmap.isNull() if ignore_update: self.selectedViewer.ignore_signal = True @@ -79,7 +80,7 @@ class BaseController(QObject): self.selectedViewer.ignore_signal = 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(): # disable the blank widget. viewer.setImage(pixmap) @@ -91,6 +92,7 @@ class BaseController(QObject): viewer.setImage(pixmap) return target_size # zoomed in state, expand + # only if not same_group, we need full update scaledpixmap = pixmap.scaled( target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation) else: @@ -109,8 +111,15 @@ class BaseController(QObject): 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.selectedViewer.scaleAt(1.0) + self.referenceViewer.scaleAt(1.0) + self.centerViews() #FIXME move buttons somwhere else @@ -128,8 +137,12 @@ class BaseController(QObject): 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.selectedViewer.scaleAt(1.0) + self.referenceViewer.scaleAt(1.0) self.centerViews() #FIXME move buttons somwhere else @@ -144,6 +157,14 @@ class BaseController(QObject): self.referenceViewer.setImage(self.referencePixmap) # null self.referenceViewer.setEnabled(False) + @pyqtSlot() + def zoomIn(self): + self.scaleImagesBy(1.25) + + @pyqtSlot() + def zoomOut(self): + self.scaleImagesBy(0.8) + @pyqtSlot(float) def scaleImagesBy(self, factor): """Compute new scale from factor and scale.""" @@ -171,6 +192,8 @@ class BaseController(QObject): """Setup before scaling to bestfit""" self.setBestFit(True) self.current_scale = 1.0 + self.selectedViewer.current_scale = 1.0 + self.referenceViewer.current_scale = 1.0 self.selectedViewer.scaleAt(1.0) self.referenceViewer.scaleAt(1.0) @@ -301,7 +324,7 @@ class ScrollAreaController(BaseController): def scaleImagesBy(self, factor): super().scaleImagesBy(factor) # The other is automatically updated via sigals - self.selectedViewer.adjustScrollBarsFactor(factor) + # self.selectedViewer.adjustScrollBarsFactor(factor) @pyqtSlot() def ScaleToBestFit(self): @@ -318,6 +341,11 @@ class GraphicsViewController(BaseController): def __init__(self, selectedViewer, referenceViewer, parent): super().__init__(selectedViewer, referenceViewer, parent) + def _setupConnections(self): + super()._setupConnections() + self.selectedViewer.connectScrollBars() + self.referenceViewer.connectScrollBars() + @pyqtSlot() def syncCenters(self): if self.sender() is self.referenceViewer: @@ -325,6 +353,179 @@ class GraphicsViewController(BaseController): else: 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): """Use a QPixmap, but no scrollbars.""" @@ -340,7 +541,7 @@ class QWidgetImageViewer(QWidget): self._rect = QRectF() self._lastMouseClickPoint = QPointF() self._mousePanningDelta = QPointF() - self._current_scale = 1.0 + self.current_scale = 1.0 self._drag = False self._dragConnection = None self._wheelConnection = None @@ -374,7 +575,7 @@ class QWidgetImageViewer(QWidget): def paintEvent(self, event): painter = QPainter(self) 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.drawPixmap(self._rect.topLeft(), self._pixmap) @@ -382,7 +583,6 @@ class QWidgetImageViewer(QWidget): """ Resets origin """ # Make sure we are not still panning around self._mousePanningDelta = QPointF() - self.scaleAt(1.0) self.update() def changeEvent(self, event): @@ -415,7 +615,7 @@ class QWidgetImageViewer(QWidget): return self._mousePanningDelta += (event.pos() - self._lastMouseClickPoint) \ - * 1.0 / self._current_scale + * 1.0 / self.current_scale self._lastMouseClickPoint = event.pos() if self._drag: self.mouseDragged.emit(self._mousePanningDelta) @@ -437,11 +637,11 @@ class QWidgetImageViewer(QWidget): return if event.angleDelta().y() > 0: - if self._current_scale > MAX_SCALE: + if self.current_scale > MAX_SCALE: return self.mouseWheeled.emit(1.25) # zoom-in else: - if self._current_scale < MIN_SCALE: + if self.current_scale < MIN_SCALE: return self.mouseWheeled.emit(0.8) # zoom-out @@ -467,11 +667,11 @@ class QWidgetImageViewer(QWidget): return True if not self.pixmap.isNull() else False def scaleBy(self, factor): - self._current_scale *= factor + self.current_scale *= factor self.update() def scaleAt(self, scale): - self._current_scale = scale + self.current_scale = scale self.update() def sizeHint(self): @@ -480,7 +680,7 @@ class QWidgetImageViewer(QWidget): @pyqtSlot() def scaleToNormalSize(self): """Called when the pixmap is set back to original size.""" - self._current_scale = 1.0 + self.current_scale = 1.0 self.update() @pyqtSlot(QPointF) @@ -495,23 +695,23 @@ class ScalablePixmap(QWidget): def __init__(self, parent): super().__init__(parent) self._pixmap = QPixmap() - self._current_scale = 1.0 + self.current_scale = 1.0 def paintEvent(self, event): painter = QPainter(self) # painter.translate(self.rect().center()) # painter.setRenderHint(QPainter.Antialiasing, False) # 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) - # print(f"ScalableWidget paintEvent scale {self._current_scale}") + # print(f"ScalableWidget paintEvent scale {self.current_scale}") def setPixmap(self, pixmap): self._pixmap = pixmap # self.update() def sizeHint(self): - return self._pixmap.size() * self._current_scale + return self._pixmap.size() * self.current_scale # return self._pixmap.size() def minimumSizeHint(self): @@ -535,7 +735,7 @@ class ScrollAreaImageViewer(QScrollArea): self._rect = QRectF() self._lastMouseClickPoint = QPointF() self._mousePanningDelta = QPoint() - self._current_scale = 1.0 + self.current_scale = 1.0 self._drag = False self._dragConnection = None self._wheelConnection = None @@ -606,6 +806,7 @@ class ScrollAreaImageViewer(QScrollArea): def connectScrollBars(self): """Only call once controller is connected.""" # Cyclic connections are handled by Qt + return self._verticalScrollBar.valueChanged.connect( self.controller.onVScrollBarChanged, Qt.UniqueConnection) self._horizontalScrollBar.valueChanged.connect( @@ -651,19 +852,19 @@ class ScrollAreaImageViewer(QScrollArea): if self.bestFit: event.ignore() return - oldScale = self._current_scale + oldScale = self.current_scale if event.angleDelta().y() > 0: # zoom-in if oldScale < MAX_SCALE: - self._current_scale *= 1.25 + self.current_scale *= 1.25 else: if oldScale > MIN_SCALE: # zoom-out - self._current_scale *= 0.8 - if oldScale == self._current_scale: + self.current_scale *= 0.8 + if oldScale == self.current_scale: return deltaToPos = (event.position() / oldScale) - (self.label.pos() / oldScale) - delta = (deltaToPos * self._current_scale) - (deltaToPos * oldScale) - self.mouseWheeled.emit(self._current_scale, delta) + delta = (deltaToPos * self.current_scale) - (deltaToPos * oldScale) + self.mouseWheeled.emit(self.current_scale, delta) def setImage(self, pixmap): self._pixmap = pixmap @@ -680,7 +881,7 @@ class ScrollAreaImageViewer(QScrollArea): def centerViewAndUpdate(self): self._rect = self.label.rect() 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.viewport().update() @@ -693,8 +894,8 @@ class ScrollAreaImageViewer(QScrollArea): return True if not self.pixmap.isNull() else False def scaleBy(self, factor): - self._current_scale *= factor - # print(f"scaleBy(factor={factor}) current_scale={self._current_scale}") + self.current_scale *= factor + # print(f"scaleBy(factor={factor}) current_scale={self.current_scale}") # This kills my computer when scaling up! DO NOT USE! # self._pixmap = self._pixmap.scaled( @@ -713,7 +914,7 @@ class ScrollAreaImageViewer(QScrollArea): self.label.resize(self.label.size().__imul__(factor)) # self.label.updateGeometry() - self.label._current_scale = self._current_scale + self.label.current_scale = self.current_scale self.label.update() # Center view on zoom change(?) same as imageLabel->resize(imageLabel->pixmap()->size()) # self.label.adjustSize() @@ -732,9 +933,9 @@ class ScrollAreaImageViewer(QScrollArea): # self.adjustScrollBarCentered() def scaleAt(self, scale): - self._current_scale = scale + self.current_scale = scale self.label.resize(self._pixmap.size().__imul__(scale)) - self.label._current_scale = scale + self.label.current_scale = scale self.label.update() # self.label.adjustSize() @@ -783,7 +984,7 @@ class ScrollAreaImageViewer(QScrollArea): def resetCenter(self): """ Resets origin """ self._mousePanningDelta = QPoint() - self._current_scale = 1.0 + self.current_scale = 1.0 # self.scaleBy(1.0) # self.label.update() # already called in scaleBy @@ -794,10 +995,10 @@ class ScrollAreaImageViewer(QScrollArea): return self._lastMouseClickPoint def sizeHint(self): - return self._pixmap.rect().size() + return self.viewport().rect().size() - def viewportSizeHint(self): - return self._pixmap.rect().size() + # def viewportSizeHint(self): + # return self.viewport().rect().size() @pyqtSlot() def scaleToNormalSize(self): @@ -827,9 +1028,9 @@ class ScrollAreaImageViewer(QScrollArea): from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem class GraphicsViewViewer(QGraphicsView): - """Re-Implementation.""" + """Re-Implementation using a more full fledged class.""" mouseDragged = pyqtSignal() - mouseWheeled = pyqtSignal(bool) + mouseWheeled = pyqtSignal(float, QPointF) def __init__(self, parent, name=""): super().__init__(parent) @@ -841,7 +1042,9 @@ class GraphicsViewViewer(QGraphicsView): self._lastMouseClickPoint = QPointF() self._mousePanningDelta = QPointF() 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._dragConnection = None self._wheelConnection = None @@ -849,25 +1052,29 @@ class GraphicsViewViewer(QGraphicsView): self.wantScrollBars = True self.bestFit = True self.controller = None - self._centerPoint = QPointF(0.0, 0.0) + self._centerPoint = QPointF() self.centerOn(self._centerPoint) # specific to this class self._scene = QGraphicsScene() + self._scene.setBackgroundBrush(Qt.black) self._item = QGraphicsPixmapItem() self.setScene(self._scene) self._scene.addItem(self._item) self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag) + self.matrix = QTransform() + self._horizontalScrollBar = self.horizontalScrollBar() + self._verticalScrollBar = self.verticalScrollBar() + self.ignore_signal = False if self.wantScrollBars: - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.toggleScrollBars() else: self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setResizeAnchor(QGraphicsView.AnchorViewCenter) - self.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) + self.setAlignment(Qt.AlignCenter) self.setViewportUpdateMode (QGraphicsView.FullViewportUpdate) self.setMouseTracking(True) @@ -877,7 +1084,7 @@ class GraphicsViewViewer(QGraphicsView): self.controller.syncCenters) if not self._wheelConnection: self._wheelConnection = self.mouseWheeled.connect( - self.controller.scaleImages) + self.controller.onMouseWheel) def disconnectMouseSignals(self): if self._dragConnection: @@ -887,6 +1094,27 @@ class GraphicsViewViewer(QGraphicsView): self.mouseWheeled.disconnect() 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): if self.bestFit: event.ignore() @@ -897,7 +1125,6 @@ class GraphicsViewViewer(QGraphicsView): self._drag = False event.ignore() return - self._lastMouseClickPoint = event.pos() self._app.setOverrideCursor(Qt.ClosedHandCursor) self.setMouseTracking(True) @@ -905,40 +1132,53 @@ class GraphicsViewViewer(QGraphicsView): super().mousePressEvent(event) # 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): if self.bestFit: event.ignore() return if event.button() == Qt.LeftButton: self._drag = False - self._app.restoreOverrideCursor() self.setMouseTracking(False) + self.updateCenterPoint() super().mouseReleaseEvent(event) - def wheelEvent(self, event): + def mouseMoveEvent(self, event): if self.bestFit: event.ignore() 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: - self.mouseWheeled.emit(True) # zoom-in + factor = self.zoomInFactor 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): self._pixmap = pixmap @@ -947,14 +1187,15 @@ class GraphicsViewViewer(QGraphicsView): # self._item.setOffset(offset) # self.setSceneRect(offset.x()*4, offset.y()*4, -offset.x()*8, -offset.y()*8) 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): + # self._rect = self.sceneRect() + # self._rect.translate(-self._rect.center()) + # self._item.update() + # self.viewport().update() pass - def scaleBy(self, factor): - # super().scale(factor, factor) - self.zoom(factor) def setCenter(self, point): self._centerPoint = point @@ -966,59 +1207,77 @@ class GraphicsViewViewer(QGraphicsView): def resetCenter(self): """ Resets origin """ self._mousePanningDelta = QPointF() - self._current_scale = 1.0 - self.scaleBy(1.0) + self.current_scale = 1.0 # self.update() - self.setCenter(self._scene.sceneRect().center()) + # self.setCenter(self._scene.sceneRect().center()) def setNewCenter(self, position): self._centerPoint = position 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() def scaleToNormalSize(self): """Called when the pixmap is set back to original size.""" - self.scaleBy(1.0) # FIXME - - super().fitInView(self._scene.sceneRect(), Qt.KeepAspectRatio ) - self.setNewCenter(self._scene.sceneRect().center()) + self.bestFit = False + self.scaleAt(1.0) + self.toggleScrollBars() self.update() - # def sizeHint(self): - # return self._item.rect().size() + def adjustScrollBarsScaled(self, delta): + """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): - # return self._item.rect().size() + # return self.viewport().rect().size() - def zoom_in(self): - self.zoom(self._scaleFactor) - - def zoom_out(self): - self.zoom(1.0 / self._scaleFactor) - - def zoom(self, factor): - #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 adjustScrollBarsFactor(self, factor): + """After scaling, no mouse position, default to center.""" + # scrollBar.setMaximum(scrollBar.maximum() - scrollBar.minimum() + scrollBar.pageStep()) + self._horizontalScrollBar.setValue(int(factor * self._horizontalScrollBar.value() + \ + ((factor - 1) * self._horizontalScrollBar.pageStep()/2))) + self._verticalScrollBar.setValue(int(factor * self._verticalScrollBar.value() + \ + ((factor - 1) * self._verticalScrollBar.pageStep()/2))) + 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()) \ No newline at end of file