diff --git a/qt/dg.qrc b/qt/dg.qrc index eb5d735f..545a9806 100644 --- a/qt/dg.qrc +++ b/qt/dg.qrc @@ -5,6 +5,5 @@ ../images/plus_8.png ../images/minus_8.png ../qtlib/images/search_clear_13.png - ../images/zoom-in.png diff --git a/qt/pe/details_dialog.py b/qt/pe/details_dialog.py index 44224eca..02e21d9b 100644 --- a/qt/pe/details_dialog.py +++ b/qt/pe/details_dialog.py @@ -4,7 +4,7 @@ # which should be included with this package. The terms are also available at # http://www.gnu.org/licenses/gpl-3.0.html -from PyQt5.QtCore import Qt, QSize, QRectF, QPointF +from PyQt5.QtCore import Qt, QSize, QRectF, QPointF, pyqtSlot from PyQt5.QtGui import QPixmap, QIcon, QKeySequence, QPainter, QPalette from PyQt5.QtWidgets import ( QVBoxLayout, @@ -19,7 +19,8 @@ from PyQt5.QtWidgets import ( QAction, QWidget, QScrollArea, - QApplication + QApplication, + QAbstractScrollArea ) from hscommon.trans import trget @@ -31,17 +32,24 @@ from qtlib.util import createActions tr = trget("ui") class ImageViewer(QWidget): + """ Displays image and allow manipulations """ def __init__(self, parent): - QWidget.__init__(self, parent) + super().__init__(parent) + self.parent = parent self.app = QApplication self.pixmap = QPixmap() self.m_rect = QRectF() self.reference = QPointF() self.delta = QPointF() - self.scale = 1.0 - self.label = QLabel(parent) - self.area = QScrollArea() + self.scalefactor = 1.0 + self.area = QScrollArea(parent) + self.area.setBackgroundRole(QPalette.Dark) + self.area.setWidgetResizable(True) + self.area.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents) + # self.area.viewport().setAttribute(Qt.WA_StaticContents) + + self.label = QLabel() sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -50,58 +58,91 @@ class ImageViewer(QWidget): self.label.setSizePolicy(sizePolicy) self.label.setAlignment(Qt.AlignCenter) self.label.setScaledContents(True) - - self.area.setBackgroundRole(QPalette.Dark) + self.area.setWidget(self.label) self.area.setVisible(False) def paintEvent(self, event): painter = QPainter(self) painter.translate(self.rect().center()) - painter.scale(self.scale, self.scale) + painter.scale(self.scalefactor, self.scalefactor) painter.translate(self.delta) painter.drawPixmap(self.m_rect.topLeft(), self.pixmap) - + def mousePressEvent(self, event): + if self.parent.bestFit: + event.ignore() # probably not needed + return + self.reference = event.pos() self.app.setOverrideCursor(Qt.ClosedHandCursor) self.setMouseTracking(True) def mouseMoveEvent(self, event): - self.delta += (event.pos() - self.reference) * 1.0/self.scale + if self.parent.bestFit: + event.ignore() # probably not needed + return + + self.delta += (event.pos() - self.reference) * 1.0/self.scalefactor self.reference = event.pos() self.update() def mouseReleaseEvent(self, event): + if self.parent.bestFit: + event.ignore() # probably not needed + return + self.app.restoreOverrideCursor() self.setMouseTracking(False) + def wheelEvent(self, event): + if self.parent.bestFit: + event.ignore() # probably not needed + return + + if event.angleDelta().y() > 0: + self.parent.zoomIn() + else: + self.parent.zoomOut() + def setPixmap(self, pixmap): self.pixmap = pixmap + if pixmap is None: + return self.m_rect = self.pixmap.rect() self.m_rect.translate(-self.m_rect.center()) self.update() - + def scale(self, factor): - self.scale *= factor + self.scalefactor = factor + print(f"ImaveViewer.scalefactor={self.scalefactor}") + # self.label.resize(self.scalefactor * self.label.size()) self.update() - + def sizeHint(self): return QSize(400, 400) + @pyqtSlot() + def pixmapReset(self): + self.scalefactor = 1.0 + self.update() + class DetailsDialog(DetailsDialogBase): def __init__(self, parent, app): - DetailsDialogBase.__init__(self, parent, app) + super().__init__(parent, app) self.selectedPixmap = None self.referencePixmap = None + self.scaledSelectedPixmap = None + self.scaledReferencePixmap = None self.scaleFactor = 1.0 - self.app = app + self.bestFit = True - def _setupActions(self): + def setupActions(self): # (name, shortcut, icon, desc, func) ACTIONS = [ ( + # FIXME probably not used right now "actionSwap", QKeySequence.Backspace, "swap", @@ -131,7 +172,7 @@ class DetailsDialog(DetailsDialogBase): ), ( "actionBestFit", - QKeySequence.Refresh, + tr("Ctrl+p"), "zoom-reset", tr("Best fit"), self.zoomBestFit, @@ -140,7 +181,7 @@ class DetailsDialog(DetailsDialogBase): createActions(ACTIONS, self) def _setupUi(self): - self._setupActions() + self.setupActions() self.setWindowTitle(tr("Details")) self.resize(502, 295) self.setMinimumSize(QSize(250, 250)) @@ -154,6 +195,7 @@ class DetailsDialog(DetailsDialogBase): self.horizontalLayout.setColumnStretch(1,0) self.horizontalLayout.setColumnStretch(2,1) self.horizontalLayout.setSpacing(4) + self.selectedImage = ImageViewer(self) # self.selectedImage = QLabel(self) # sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) @@ -179,8 +221,9 @@ class DetailsDialog(DetailsDialogBase): self.buttonImgSwap.setIcon(QIcon.fromTheme('document-revert', \ self.style().standardIcon(QStyle.SP_BrowserReload))) self.buttonImgSwap.setText('Swap images') + self.buttonImgSwap.setToolTip('Swap images') self.buttonImgSwap.pressed.connect(self.swapImages) - self.buttonImgSwap.released.connect(self.swapImages) + self.buttonImgSwap.released.connect(self.deswapImages) self.buttonZoomIn = QToolButton(self.verticalToolBar) self.buttonZoomIn.setToolButtonStyle(Qt.ToolButtonIconOnly) @@ -218,15 +261,16 @@ class DetailsDialog(DetailsDialogBase): self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignCenter) # self.horizontalLayout.addWidget(self.verticalToolBar, Qt.AlignVCenter) - self.referenceImage = QLabel(self) - sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth( - self.referenceImage.sizePolicy().hasHeightForWidth() - ) - self.referenceImage.setSizePolicy(sizePolicy) - self.referenceImage.setAlignment(Qt.AlignCenter) + self.referenceImage = ImageViewer(self) + # self.referenceImage = QLabel(self) + # sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) + # sizePolicy.setHorizontalStretch(0) + # sizePolicy.setVerticalStretch(0) + # sizePolicy.setHeightForWidth( + # self.referenceImage.sizePolicy().hasHeightForWidth() + # ) + # self.referenceImage.setSizePolicy(sizePolicy) + # self.referenceImage.setAlignment(Qt.AlignCenter) self.horizontalLayout.addWidget(self.referenceImage, 0, 2, 3, 1) # self.horizontalLayout.addWidget(self.referenceImage) self.verticalLayout.addLayout(self.horizontalLayout) @@ -250,48 +294,53 @@ class DetailsDialog(DetailsDialogBase): group = self.app.model.results.get_group_of_duplicate(dupe) ref = group.ref + self.resetState() self.selectedPixmap = QPixmap(str(dupe.path)) if ref is dupe: self.referencePixmap = None + self.scaledReferencePixmap = None self.buttonImgSwap.setEnabled(False) else: self.referencePixmap = QPixmap(str(ref.path)) self.buttonImgSwap.setEnabled(True) - - self._resetButtons() self._updateImages() def _updateImages(self): + target_size = None if self.selectedPixmap is not None: target_size = self.selectedImage.size() - if self.scaleFactor > 0: - scaledPixmap = self.selectedPixmap.scaled( - target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation) # widget expands here - else: - scaledPixmap = self.selectedPixmap.scaled( + if not self.bestFit: + # zoomed in state, expand + self.scaledSelectedPixmap = self.selectedPixmap.scaled( + target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation) + else: # best fit, keep ratio always + self.scaledSelectedPixmap = self.selectedPixmap.scaled( target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) - self.selectedImage.setPixmap(scaledPixmap) + self.selectedImage.setPixmap(self.scaledSelectedPixmap) # self.selectedImage.adjustSize() else: self.selectedImage.setPixmap(QPixmap()) - self.scaleFactor = 1.0 if self.referencePixmap is not None: - target_size = self.referenceImage.size() - if self.scaleFactor > 0: - scaledPixmap = self.referencePixmap.scaled( - target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation) # widget expands here + # 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.selectedImage.size() + if not self.bestFit: + self.scaledReferencePixmap = self.referencePixmap.scaled( + target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation) else: - scaledPixmap = self.referencePixmap.scaled( + self.scaledReferencePixmap = self.referencePixmap.scaled( target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) - self.referenceImage.setPixmap(scaledPixmap) + self.referenceImage.setPixmap(self.scaledReferencePixmap) else: self.referenceImage.setPixmap(QPixmap()) - self.scaleFactor = 1.0 # --- Override def resizeEvent(self, event): + if not self.bestFit: + return self._updateImages() def show(self): @@ -304,51 +353,110 @@ class DetailsDialog(DetailsDialogBase): if self.isVisible(): self._update() - def scaleImages(self, factor): - self.scaleFactor *= factor - print(f'Factor is now = {self.scaleFactor}.') - self.referenceImage.resize(self.scaleFactor * self.referencePixmap.size()) - self.selectedImage.resize(self.scaleFactor * self.selectedPixmap.size()) - - self.buttonZoomIn.setEnabled(self.scaleFactor < 3.0) - self.buttonZoomOut.setEnabled(self.scaleFactor > 1.0) - self.buttonBestFit.setEnabled(self.scaleFactor != 1.0) - - def _resetButtons(self): - self.buttonZoomIn.setEnabled(True) + def resetState(self): + self.scaledReferencePixmap = None + self.scaledSelectedPixmapPixmap = None + self.buttonZoomIn.setEnabled(False) self.buttonZoomOut.setEnabled(False) - self.buttonBestFit.setEnabled(False) + self.buttonBestFit.setEnabled(False) # active mode by default + self.buttonNormalSize.setEnabled(True) + self.bestFit = True + self.scaleFactor = 1.0 + def scaleImages(self, factor): + self.scaleFactor *= factor + print(f'QDialog scaleFactor = {self.scaleFactor} (+factor {factor})') + + # returns QSize, not good anymore + # self.referenceImage.scale(self.scaleFactor * self.referencePixmap.size()) + # self.selectedImage.scale(self.scaleFactor * self.selectedPixmap.size()) + + self.referenceImage.scale(self.scaleFactor) + self.selectedImage.scale(self.scaleFactor) + + self.buttonZoomIn.setEnabled(self.scaleFactor < 6.0) + self.buttonZoomOut.setEnabled(self.scaleFactor > 1.0) + self.buttonBestFit.setEnabled(self.bestFit is False) + self.buttonNormalSize.setEnabled(self.scaleFactor != 1.0) + + @pyqtSlot() def swapImages(self): + """ Swap pixmaps between ImageViewers """ # self.horizontalLayout.replaceWidget(self.selectedImage, self.referenceImage) - self._tempPixmap = self.referencePixmap - self.referencePixmap = self.selectedPixmap - self.selectedPixmap = self._tempPixmap - self._updateImages() + + # self._tempPixmap = self.referencePixmap + # referencePixmap = self.selectedPixmap + # self.selectedPixmap = self._tempPixmap + # self._updateImages() + + if self.bestFit: + self.selectedImage.setPixmap(self.scaledReferencePixmap) + self.referenceImage.setPixmap(self.scaledSelectedPixmap) + else: + self.selectedImage.setPixmap(self.referencePixmap) + self.referenceImage.setPixmap(self.selectedPixmap) + # swap the columns in the details table as well self.tableView.horizontalHeader().swapSections(1, 2) + @pyqtSlot() + def deswapImages(self): + """ Restore swapped pixmaps """ + if self.bestFit: + self.selectedImage.setPixmap(self.scaledSelectedPixmap) + self.referenceImage.setPixmap(self.scaledReferencePixmap) + else: + self.selectedImage.setPixmap(self.selectedPixmap) + self.referenceImage.setPixmap(self.referencePixmap) + + self.tableView.horizontalHeader().swapSections(1, 2) + + @pyqtSlot() def zoomIn(self): self.scaleImages(1.25) + @pyqtSlot() def zoomOut(self): self.scaleImages(0.8) - def zoomNormalSize(self): - self.scaleFactor = 1.0 - - def zoomBestFit(self): - self.scaleFactor = 1.0 - self.referenceImage.resize(self.scaleFactor * self.referencePixmap.size()) - self.selectedImage.resize(self.scaleFactor * self.selectedPixmap.size()) - self.buttonBestFit.setEnabled(False) - self.buttonZoomOut.setEnabled(False) - self.buttonZoomIn.setEnabled(True) + @pyqtSlot() + def scale_to_bestfit(self): + self.referenceImage.scale(self.scaleFactor) + self.selectedImage.scale(self.scaleFactor) self._updateImages() - def imagePan(self): - pass + @pyqtSlot() + def zoomBestFit(self): + self.bestFit = True + self.scaleFactor = 1.0 + self.buttonBestFit.setEnabled(False) + self.buttonZoomOut.setEnabled(False) + self.buttonZoomIn.setEnabled(False) + self.buttonNormalSize.setEnabled(True) + self.scale_to_bestfit() + + @pyqtSlot() + def zoomNormalSize(self): + self.bestFit = False + self.scaleFactor = 1.0 + + self.selectedImage.setPixmap(self.selectedPixmap) + + if self.referencePixmap is None: + self.referenceImage.setPixmap(QPixmap()) + else: + self.referenceImage.setPixmap(self.referencePixmap) + + self.selectedImage.pixmapReset() + self.referenceImage.pixmapReset() + # self.referenceImage.label.resize(self.scaleFactor * self.referencePixmap.size()) + # self.selectedImage.label.resize(self.scaleFactor * self.selectedPixmap.size()) + # self.referenceImage.label.adjustSize() + # self.selectedImage.label.adjustSize() + + self.buttonNormalSize.setEnabled(False) + self.buttonZoomIn.setEnabled(True) + self.buttonZoomOut.setEnabled(True) + self.buttonBestFit.setEnabled(True) + # self._updateImages() -# TODO: place handle above table view to drag and resize (splitter?) -# TODO: colorize or bolden values in table views when they differ? -# TODO: double click on details view PATH row opens path in default application \ No newline at end of file