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

working zoom functions, mouse wheel event

This commit is contained in:
glubsy 2020-06-04 02:33:54 +02:00
parent ea6197626b
commit 02bd822ca0
2 changed files with 185 additions and 78 deletions

View File

@ -5,6 +5,5 @@
<file alias="plus">../images/plus_8.png</file> <file alias="plus">../images/plus_8.png</file>
<file alias="minus">../images/minus_8.png</file> <file alias="minus">../images/minus_8.png</file>
<file alias="search_clear_13">../qtlib/images/search_clear_13.png</file> <file alias="search_clear_13">../qtlib/images/search_clear_13.png</file>
<file alias="zoom-in">../images/zoom-in.png</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -4,7 +4,7 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html # 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.QtGui import QPixmap, QIcon, QKeySequence, QPainter, QPalette
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QVBoxLayout, QVBoxLayout,
@ -19,7 +19,8 @@ from PyQt5.QtWidgets import (
QAction, QAction,
QWidget, QWidget,
QScrollArea, QScrollArea,
QApplication QApplication,
QAbstractScrollArea
) )
from hscommon.trans import trget from hscommon.trans import trget
@ -31,17 +32,24 @@ from qtlib.util import createActions
tr = trget("ui") tr = trget("ui")
class ImageViewer(QWidget): class ImageViewer(QWidget):
""" Displays image and allow manipulations """
def __init__(self, parent): def __init__(self, parent):
QWidget.__init__(self, parent) super().__init__(parent)
self.parent = parent
self.app = QApplication self.app = QApplication
self.pixmap = QPixmap() self.pixmap = QPixmap()
self.m_rect = QRectF() self.m_rect = QRectF()
self.reference = QPointF() self.reference = QPointF()
self.delta = QPointF() self.delta = QPointF()
self.scale = 1.0 self.scalefactor = 1.0
self.label = QLabel(parent)
self.area = QScrollArea()
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 = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0) sizePolicy.setVerticalStretch(0)
@ -50,58 +58,91 @@ class ImageViewer(QWidget):
self.label.setSizePolicy(sizePolicy) self.label.setSizePolicy(sizePolicy)
self.label.setAlignment(Qt.AlignCenter) self.label.setAlignment(Qt.AlignCenter)
self.label.setScaledContents(True) self.label.setScaledContents(True)
self.area.setBackgroundRole(QPalette.Dark)
self.area.setWidget(self.label) self.area.setWidget(self.label)
self.area.setVisible(False) self.area.setVisible(False)
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.scale, self.scale) painter.scale(self.scalefactor, self.scalefactor)
painter.translate(self.delta) painter.translate(self.delta)
painter.drawPixmap(self.m_rect.topLeft(), self.pixmap) painter.drawPixmap(self.m_rect.topLeft(), self.pixmap)
def mousePressEvent(self, event): def mousePressEvent(self, event):
if self.parent.bestFit:
event.ignore() # probably not needed
return
self.reference = event.pos() self.reference = event.pos()
self.app.setOverrideCursor(Qt.ClosedHandCursor) self.app.setOverrideCursor(Qt.ClosedHandCursor)
self.setMouseTracking(True) self.setMouseTracking(True)
def mouseMoveEvent(self, event): 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.reference = event.pos()
self.update() self.update()
def mouseReleaseEvent(self, event): def mouseReleaseEvent(self, event):
if self.parent.bestFit:
event.ignore() # probably not needed
return
self.app.restoreOverrideCursor() self.app.restoreOverrideCursor()
self.setMouseTracking(False) 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): def setPixmap(self, pixmap):
self.pixmap = pixmap self.pixmap = pixmap
if pixmap is None:
return
self.m_rect = self.pixmap.rect() self.m_rect = self.pixmap.rect()
self.m_rect.translate(-self.m_rect.center()) self.m_rect.translate(-self.m_rect.center())
self.update() self.update()
def scale(self, factor): 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() self.update()
def sizeHint(self): def sizeHint(self):
return QSize(400, 400) return QSize(400, 400)
@pyqtSlot()
def pixmapReset(self):
self.scalefactor = 1.0
self.update()
class DetailsDialog(DetailsDialogBase): class DetailsDialog(DetailsDialogBase):
def __init__(self, parent, app): def __init__(self, parent, app):
DetailsDialogBase.__init__(self, parent, app) super().__init__(parent, app)
self.selectedPixmap = None self.selectedPixmap = None
self.referencePixmap = None self.referencePixmap = None
self.scaledSelectedPixmap = None
self.scaledReferencePixmap = None
self.scaleFactor = 1.0 self.scaleFactor = 1.0
self.app = app self.bestFit = True
def _setupActions(self): def setupActions(self):
# (name, shortcut, icon, desc, func) # (name, shortcut, icon, desc, func)
ACTIONS = [ ACTIONS = [
( (
# FIXME probably not used right now
"actionSwap", "actionSwap",
QKeySequence.Backspace, QKeySequence.Backspace,
"swap", "swap",
@ -131,7 +172,7 @@ class DetailsDialog(DetailsDialogBase):
), ),
( (
"actionBestFit", "actionBestFit",
QKeySequence.Refresh, tr("Ctrl+p"),
"zoom-reset", "zoom-reset",
tr("Best fit"), tr("Best fit"),
self.zoomBestFit, self.zoomBestFit,
@ -140,7 +181,7 @@ class DetailsDialog(DetailsDialogBase):
createActions(ACTIONS, self) createActions(ACTIONS, self)
def _setupUi(self): def _setupUi(self):
self._setupActions() self.setupActions()
self.setWindowTitle(tr("Details")) self.setWindowTitle(tr("Details"))
self.resize(502, 295) self.resize(502, 295)
self.setMinimumSize(QSize(250, 250)) self.setMinimumSize(QSize(250, 250))
@ -154,6 +195,7 @@ class DetailsDialog(DetailsDialogBase):
self.horizontalLayout.setColumnStretch(1,0) self.horizontalLayout.setColumnStretch(1,0)
self.horizontalLayout.setColumnStretch(2,1) self.horizontalLayout.setColumnStretch(2,1)
self.horizontalLayout.setSpacing(4) self.horizontalLayout.setSpacing(4)
self.selectedImage = ImageViewer(self) self.selectedImage = ImageViewer(self)
# self.selectedImage = QLabel(self) # self.selectedImage = QLabel(self)
# sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) # sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
@ -179,8 +221,9 @@ class DetailsDialog(DetailsDialogBase):
self.buttonImgSwap.setIcon(QIcon.fromTheme('document-revert', \ self.buttonImgSwap.setIcon(QIcon.fromTheme('document-revert', \
self.style().standardIcon(QStyle.SP_BrowserReload))) self.style().standardIcon(QStyle.SP_BrowserReload)))
self.buttonImgSwap.setText('Swap images') self.buttonImgSwap.setText('Swap images')
self.buttonImgSwap.setToolTip('Swap images')
self.buttonImgSwap.pressed.connect(self.swapImages) 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 = QToolButton(self.verticalToolBar)
self.buttonZoomIn.setToolButtonStyle(Qt.ToolButtonIconOnly) 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, 1, 1, 1, 1, Qt.AlignCenter)
# self.horizontalLayout.addWidget(self.verticalToolBar, Qt.AlignVCenter) # self.horizontalLayout.addWidget(self.verticalToolBar, Qt.AlignVCenter)
self.referenceImage = QLabel(self) self.referenceImage = ImageViewer(self)
sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) # self.referenceImage = QLabel(self)
sizePolicy.setHorizontalStretch(0) # sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
sizePolicy.setVerticalStretch(0) # sizePolicy.setHorizontalStretch(0)
sizePolicy.setHeightForWidth( # sizePolicy.setVerticalStretch(0)
self.referenceImage.sizePolicy().hasHeightForWidth() # sizePolicy.setHeightForWidth(
) # self.referenceImage.sizePolicy().hasHeightForWidth()
self.referenceImage.setSizePolicy(sizePolicy) # )
self.referenceImage.setAlignment(Qt.AlignCenter) # self.referenceImage.setSizePolicy(sizePolicy)
# self.referenceImage.setAlignment(Qt.AlignCenter)
self.horizontalLayout.addWidget(self.referenceImage, 0, 2, 3, 1) self.horizontalLayout.addWidget(self.referenceImage, 0, 2, 3, 1)
# self.horizontalLayout.addWidget(self.referenceImage) # self.horizontalLayout.addWidget(self.referenceImage)
self.verticalLayout.addLayout(self.horizontalLayout) self.verticalLayout.addLayout(self.horizontalLayout)
@ -250,48 +294,53 @@ 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
self.resetState()
self.selectedPixmap = QPixmap(str(dupe.path)) self.selectedPixmap = QPixmap(str(dupe.path))
if ref is dupe: if ref is dupe:
self.referencePixmap = None self.referencePixmap = None
self.scaledReferencePixmap = None
self.buttonImgSwap.setEnabled(False) self.buttonImgSwap.setEnabled(False)
else: else:
self.referencePixmap = QPixmap(str(ref.path)) self.referencePixmap = QPixmap(str(ref.path))
self.buttonImgSwap.setEnabled(True) self.buttonImgSwap.setEnabled(True)
self._resetButtons()
self._updateImages() self._updateImages()
def _updateImages(self): def _updateImages(self):
target_size = None
if self.selectedPixmap is not None: if self.selectedPixmap is not None:
target_size = self.selectedImage.size() target_size = self.selectedImage.size()
if self.scaleFactor > 0: if not self.bestFit:
scaledPixmap = self.selectedPixmap.scaled( # zoomed in state, expand
target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation) # widget expands here self.scaledSelectedPixmap = self.selectedPixmap.scaled(
else: target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
scaledPixmap = self.selectedPixmap.scaled( else: # best fit, keep ratio always
self.scaledSelectedPixmap = self.selectedPixmap.scaled(
target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.selectedImage.setPixmap(scaledPixmap) self.selectedImage.setPixmap(self.scaledSelectedPixmap)
# self.selectedImage.adjustSize() # self.selectedImage.adjustSize()
else: else:
self.selectedImage.setPixmap(QPixmap()) self.selectedImage.setPixmap(QPixmap())
self.scaleFactor = 1.0
if self.referencePixmap is not None: if self.referencePixmap is not None:
target_size = self.referenceImage.size() # the selectedImage viewer widget sometimes ends up being bigger
if self.scaleFactor > 0: # than the referenceImage viewer, which distorts by one pixel the
scaledPixmap = self.referencePixmap.scaled( # scaled down pixmap for the reference, hence we'll reuse its size here.
target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation) # widget expands here # target_size = self.selectedImage.size()
if not self.bestFit:
self.scaledReferencePixmap = self.referencePixmap.scaled(
target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
else: else:
scaledPixmap = self.referencePixmap.scaled( self.scaledReferencePixmap = self.referencePixmap.scaled(
target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.referenceImage.setPixmap(scaledPixmap) self.referenceImage.setPixmap(self.scaledReferencePixmap)
else: else:
self.referenceImage.setPixmap(QPixmap()) self.referenceImage.setPixmap(QPixmap())
self.scaleFactor = 1.0
# --- Override # --- Override
def resizeEvent(self, event): def resizeEvent(self, event):
if not self.bestFit:
return
self._updateImages() self._updateImages()
def show(self): def show(self):
@ -304,51 +353,110 @@ class DetailsDialog(DetailsDialogBase):
if self.isVisible(): if self.isVisible():
self._update() self._update()
def scaleImages(self, factor): def resetState(self):
self.scaleFactor *= factor self.scaledReferencePixmap = None
print(f'Factor is now = {self.scaleFactor}.') self.scaledSelectedPixmapPixmap = None
self.referenceImage.resize(self.scaleFactor * self.referencePixmap.size()) self.buttonZoomIn.setEnabled(False)
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)
self.buttonZoomOut.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): def swapImages(self):
""" Swap pixmaps between ImageViewers """
# self.horizontalLayout.replaceWidget(self.selectedImage, self.referenceImage) # self.horizontalLayout.replaceWidget(self.selectedImage, self.referenceImage)
self._tempPixmap = self.referencePixmap
self.referencePixmap = self.selectedPixmap # self._tempPixmap = self.referencePixmap
self.selectedPixmap = self._tempPixmap # referencePixmap = self.selectedPixmap
self._updateImages() # 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 # swap the columns in the details table as well
self.tableView.horizontalHeader().swapSections(1, 2) 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): def zoomIn(self):
self.scaleImages(1.25) self.scaleImages(1.25)
@pyqtSlot()
def zoomOut(self): def zoomOut(self):
self.scaleImages(0.8) self.scaleImages(0.8)
def zoomNormalSize(self): @pyqtSlot()
self.scaleFactor = 1.0 def scale_to_bestfit(self):
self.referenceImage.scale(self.scaleFactor)
def zoomBestFit(self): self.selectedImage.scale(self.scaleFactor)
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)
self._updateImages() self._updateImages()
def imagePan(self): @pyqtSlot()
pass 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