From 011939f5eefb4dbd033b6ed75d948e42320680a0 Mon Sep 17 00:00:00 2001 From: glubsy Date: Tue, 23 Jun 2020 05:03:56 +0200 Subject: [PATCH] Keep scale accross files of the same dupe group. * Also fix scaled down pixmap when updating pixmap in the same group * Fix ignoring mouse wheel event when max scale has been reached * Fix toggle scrollbars when asking for normal size --- qt/pe/details_dialog.py | 12 ++-- qt/pe/image_viewer.py | 148 +++++++++++++++++++++++++--------------- 2 files changed, 98 insertions(+), 62 deletions(-) diff --git a/qt/pe/details_dialog.py b/qt/pe/details_dialog.py index c47aabd7..8d9ff198 100644 --- a/qt/pe/details_dialog.py +++ b/qt/pe/details_dialog.py @@ -63,7 +63,7 @@ class DetailsDialog(DetailsDialogBase): self.setupActions() self.setWindowTitle(tr("Details")) self.resize(502, 502) - self.setMinimumSize(QSize(500, 500)) + self.setMinimumSize(QSize(250, 250)) # self.verticalLayout = QVBoxLayout(self) # self.verticalLayout.setSpacing(0) @@ -83,7 +83,7 @@ class DetailsDialog(DetailsDialogBase): # self.horizontalLayout.setColumnStretch(3,0) self.horizontalLayout.setSpacing(1) - self.selectedImageViewer = ScrollAreaImageViewer(self, "selectedImage") + self.selectedImageViewer = QWidgetImageViewer(self, "selectedImage") # self.selectedImage = QLabel(self) # sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) # sizePolicy.setHorizontalStretch(0) @@ -151,7 +151,7 @@ class DetailsDialog(DetailsDialogBase): self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignCenter) - self.referenceImageViewer = ScrollAreaImageViewer(self, "referenceImage") + self.referenceImageViewer = QWidgetImageViewer(self, "referenceImage") # self.referenceImage = QLabel(self) # sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) # sizePolicy.setHorizontalStretch(0) @@ -199,7 +199,7 @@ class DetailsDialog(DetailsDialogBase): # We use different types of controller depending on the # underlying widgets we use to display images - # because their interface methods might differ + # because their interface and methods might differ if isinstance(self.selectedImageViewer, QWidgetImageViewer): self.vController = QWidgetController( self.selectedImageViewer, @@ -227,7 +227,7 @@ class DetailsDialog(DetailsDialogBase): if self.vController is None: # Not yet constructed! return - self.vController.update(ref, dupe) + self.vController.updateView(ref, dupe, group) # --- Override def resizeEvent(self, event): @@ -252,7 +252,7 @@ class DetailsDialog(DetailsDialogBase): if self.vController is None or not self.vController.bestFit: return # Only update the scaled down pixmaps - self.vController._updateImages() + self.vController.updateBothImages() def show(self): # Compute the maximum size the table view can reach diff --git a/qt/pe/image_viewer.py b/qt/pe/image_viewer.py index f2fa523e..3c80b017 100644 --- a/qt/pe/image_viewer.py +++ b/qt/pe/image_viewer.py @@ -7,6 +7,9 @@ from PyQt5.QtGui import QPixmap, QPainter, QPalette, QCursor from PyQt5.QtWidgets import ( QLabel, QSizePolicy, QWidget, QScrollArea, QScrollBar, QApplication, QAbstractScrollArea ) +MAX_SCALE = 12.0 +MIN_SCALE = 0.1 + class BaseController(QObject): """Abstract Base class. Singleton. Base proxy interface to keep image viewers synchronized. @@ -28,13 +31,21 @@ class BaseController(QObject): self.bestFit = True self.wantScrollBars = True self.parent = parent #To change buttons' states + self.cached_group = None def _setupConnections(self): self.selectedViewer.connectMouseSignals() self.referenceViewer.connectMouseSignals() - def update(self, ref, dupe): - self.resetState() + 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() @@ -46,36 +57,37 @@ class BaseController(QObject): self.referencePixmap = QPixmap(str(ref.path)) self.parent.buttonImgSwap.setEnabled(True) - self._updateImages() + self.updateBothImages(same_group) - def _updateImages(self): - target_size = None - if not self.selectedPixmap.isNull(): - target_size = self.selectedViewer.size() + def updateBothImages(self, same_group=False): + selected_size = self._updateImage( + self.selectedPixmap, self.scaledSelectedPixmap, self.selectedViewer, None, same_group) + # the SelectedImageViewer widget sometimes ends up being bigger + # than the ReferenceImageViewer by one pixel, which distorts the + # scaled down pixmap for the reference, hence we'll reuse its size here. + self._updateImage( + self.referencePixmap, self.scaledReferencePixmap, self.referenceViewer, selected_size, same_group) + + + def _updateImage(self, pixmap, scaledpixmap, viewer, target_size=None, same_group=False): + # If not same_group, we need full update""" + if not pixmap.isNull(): + target_size = viewer.size() if not self.bestFit: + if same_group: + viewer.setImage(pixmap) + viewer.centerViewAndUpdate() + return target_size # zoomed in state, expand - self.scaledSelectedPixmap = self.selectedPixmap.scaled( + scaledpixmap = pixmap.scaled( target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation) else: # best fit, keep ratio always - self.scaledSelectedPixmap = self.selectedPixmap.scaled( + scaledpixmap = pixmap.scaled( target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) - self.selectedViewer.setImage(self.scaledSelectedPixmap) - self.selectedViewer.centerViewAndUpdate() - - if not self.referencePixmap.isNull(): - # the selectedImage viewer widget sometimes ends up being bigger - # than the referenceImage viewer, which distorts by one pixel the - # scaled down pixmap for the reference, hence we'll reuse its size here. - # target_size = self.selectedViewer.size() - if not self.bestFit: - self.scaledReferencePixmap = self.referencePixmap.scaled( - target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation) - else: - self.scaledReferencePixmap = self.referencePixmap.scaled( - target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) - self.referenceViewer.setImage(self.scaledReferencePixmap) - self.referenceViewer.centerViewAndUpdate() + viewer.setImage(scaledpixmap) + viewer.centerViewAndUpdate() + return target_size @pyqtSlot(float) def scaleImagesBy(self, factor): @@ -84,8 +96,8 @@ class BaseController(QObject): self.selectedViewer.scaleBy(factor) self.referenceViewer.scaleBy(factor) - self.parent.buttonZoomIn.setEnabled(self.current_scale < 9.0) - self.parent.buttonZoomOut.setEnabled(self.current_scale > 0.5) + self.parent.buttonZoomIn.setEnabled(self.current_scale < MAX_SCALE) + self.parent.buttonZoomOut.setEnabled(self.current_scale > MIN_SCALE) self.parent.buttonBestFit.setEnabled(self.bestFit is False) self.parent.buttonNormalSize.setEnabled(self.current_scale != 1.0) @@ -96,12 +108,13 @@ class BaseController(QObject): self.selectedViewer.scaleAt(scale) self.referenceViewer.scaleAt(scale) - self.parent.buttonZoomIn.setEnabled(self.current_scale < 9.0) - self.parent.buttonZoomOut.setEnabled(self.current_scale > 0.5) + self.parent.buttonZoomIn.setEnabled(self.current_scale < MAX_SCALE) + self.parent.buttonZoomOut.setEnabled(self.current_scale > MIN_SCALE) self.parent.buttonBestFit.setEnabled(self.bestFit is False) self.parent.buttonNormalSize.setEnabled(self.current_scale != 1.0) def resetState(self): + """Only called when the group of dupes has changed""" self.selectedPixmap = QPixmap() self.scaledSelectedPixmap = QPixmap() self.referencePixmap = QPixmap() @@ -111,6 +124,9 @@ class BaseController(QObject): self.selectedViewer.resetCenter() self.referenceViewer.resetCenter() + self.selectedViewer.centerViewAndUpdate() + self.referenceViewer.centerViewAndUpdate() + self.parent.buttonZoomIn.setEnabled(False) self.parent.buttonZoomOut.setEnabled(False) self.parent.buttonBestFit.setEnabled(False) # active mode by default @@ -120,11 +136,11 @@ class BaseController(QObject): """No item from the model, disable and clear everything.""" self.resetState() self.selectedViewer.setImage(self.selectedPixmap) # null - self.selectedViewer.setDisabled(True) + self.selectedViewer.setEnabled(False) self.referenceViewer.setImage(self.referencePixmap) # null - self.referenceViewer.setDisabled(True) - self.parent.buttonImgSwap.setDisabled(True) - self.parent.buttonNormalSize.setDisabled(True) + self.referenceViewer.setEnabled(False) + self.parent.buttonImgSwap.setEnabled(False) + self.parent.buttonNormalSize.setEnabled(False) @pyqtSlot() def ScaleToBestFit(self): @@ -137,7 +153,9 @@ class BaseController(QObject): self.selectedViewer.resetCenter() self.referenceViewer.resetCenter() - self._updateImages() + + target_size = self._updateImage(self.selectedPixmap, self.scaledSelectedPixmap, self.selectedViewer, None, True) + self._updateImage(self.referencePixmap, self.scaledReferencePixmap, self.referenceViewer, target_size, True) self.parent.buttonBestFit.setEnabled(False) self.parent.buttonZoomOut.setEnabled(False) @@ -187,6 +205,11 @@ class QWidgetController(BaseController): self.selectedViewer.centerViewAndUpdate() self.referenceViewer.centerViewAndUpdate() + def _updateImage(self, pixmap, scaledpixmap, viewer, target_size, same_group): + #FIXME might not need this at all, REMOVE? + super()._updateImage(pixmap, scaledpixmap, viewer, target_size, same_group) + viewer.update() + class ScrollAreaController(BaseController): """Specialized version fro QLabel-based viewers.""" @@ -226,12 +249,12 @@ class ScrollAreaController(BaseController): def onMouseWheel(self, scale, delta): self.scaleImagesAt(scale) self.selectedViewer.adjustScrollBarsScaled(delta) - # Signal will automatically change the other: + # Signal from scrollbars will automatically change the other: # self.referenceViewer.adjustScrollBarsScaled(delta) @pyqtSlot(int) def onVScrollBarChanged(self, value): - if self.sender() is self.referenceViewer: + if self.sender() is self.referenceViewer._verticalScrollBar: if not self.selectedViewer.ignore_signal: self.selectedViewer._verticalScrollBar.setValue(value) else: @@ -240,8 +263,8 @@ class ScrollAreaController(BaseController): @pyqtSlot(int) def onHScrollBarChanged(self, value): - if self.sender() is self.referenceViewer: - if not selectedViewer.ignore_signal: + 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: @@ -261,6 +284,7 @@ class ScrollAreaController(BaseController): self.selectedViewer.toggleScrollBars() self.referenceViewer.toggleScrollBars() + class GraphicsViewController(BaseController): """Specialized version fro QGraphicsView-based viewers.""" def __init__(self, selectedViewer, referenceViewer, parent): @@ -330,7 +354,7 @@ class QWidgetImageViewer(QWidget): """ Resets origin """ # Make sure we are not still panning around self._mousePanningDelta = QPointF() - self.scaleBy(1.0) + self.scaleAt(1.0) self.update() def changeEvent(self, event): @@ -385,8 +409,12 @@ class QWidgetImageViewer(QWidget): return if event.angleDelta().y() > 0: + if self._current_scale > MAX_SCALE: + return self.mouseWheeled.emit(1.25) # zoom-in else: + if self._current_scale < MIN_SCALE: + return self.mouseWheeled.emit(0.8) # zoom-out def setImage(self, pixmap): @@ -516,11 +544,14 @@ class ScrollAreaImageViewer(QScrollArea): def getPixmap(self): return self._pixmap - def toggleScrollBars(self): + 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: @@ -592,26 +623,31 @@ class ScrollAreaImageViewer(QScrollArea): event.ignore() return oldScale = self._current_scale - if event.angleDelta().y() > 0: - self._current_scale *= 1.25 # zoom-in + if event.angleDelta().y() > 0: # zoom-in + if oldScale < MAX_SCALE: + self._current_scale *= 1.25 else: - self._current_scale *= 0.8 # zoom-out + if oldScale > MIN_SCALE: # zoom-out + 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) - def setImage(self, pixmap, cache=True): - if pixmap.isNull(): - if not self._pixmap.isNull(): - self._pixmap = pixmap - self.update() - return - elif not self.isEnabled(): - self.setEnabled(True) - self.connectMouseSignals() + def setImage(self, pixmap): self._pixmap = pixmap self.label.setPixmap(pixmap) + self.label.update() self.label.adjustSize() + if pixmap.isNull(): + self.setEnabled(False) + self.disconnectMouseSignals() + else: + if not self.isEnabled(): + self.setEnabled(True) + self.connectMouseSignals() def centerViewAndUpdate(self): self._rect = self.label.rect() @@ -709,7 +745,7 @@ class ScrollAreaImageViewer(QScrollArea): self.verticalScrollBar().setValue( self.verticalScrollBar().value() - self._mousePanningDelta.y()) - def adjustScrollBarCentered(self, scrollBar, factor): + def adjustScrollBarCentered(self): """Just center in the middle.""" self._horizontalScrollBar.setValue( int(self._horizontalScrollBar.maximum() / 2)) @@ -722,7 +758,7 @@ class ScrollAreaImageViewer(QScrollArea): # self.label._mousePanningDelta = self._mousePanningDelta self._current_scale = 1.0 # self.scaleBy(1.0) - # self.label.update() # already called in scaleBy + # self.label.update() # already called in scaleBy² def setCenter(self, point): self._lastMouseClickPoint = point @@ -742,7 +778,7 @@ class ScrollAreaImageViewer(QScrollArea): self.scaleAt(1.0) self.ensureWidgetVisible(self.label) # needed for centering # self.label.update() - self.toggleScrollBars() + self.toggleScrollBars(True) @pyqtSlot(QPoint) def onDraggedMouse(self, delta): @@ -751,7 +787,7 @@ class ScrollAreaImageViewer(QScrollArea): # self.label.move(self.label.pos() + delta) # self.label.update() - #FIXME signal from scrollbars has already synced the values here! + # Signal from scrollbars had already synced the values here self.adjustScrollBarsAuto() # print(f"{self} onDraggedMouse slot with delta {delta}")