Controller class to decouple from the dialog class

The controller singleton acts as a proxy to relay
signals from each widget to the other
It should help encapsulating things better if we need to
use a different class for image viewers in the future.
This commit is contained in:
glubsy 2020-06-10 01:24:51 +02:00
parent 60ddb9b596
commit c3797918d2
2 changed files with 552 additions and 273 deletions

View File

@ -6,38 +6,24 @@
from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal
from PyQt5.QtGui import QPixmap, QIcon, QKeySequence
from PyQt5.QtWidgets import (
QVBoxLayout,
QAbstractItemView,
QHBoxLayout,
QLabel,
QSizePolicy,
QToolBar,
QToolButton,
QGridLayout,
QStyle,
QAction,
QWidget,
QApplication,
)
from PyQt5.QtWidgets import (QVBoxLayout, QAbstractItemView, QHBoxLayout,
QLabel, QSizePolicy, QToolBar, QToolButton, QGridLayout, QStyle, QAction,
QWidget, QApplication )
from hscommon.trans import trget
from hscommon import desktop
from ..details_dialog import DetailsDialog as DetailsDialogBase
from ..details_table import DetailsTable
from qtlib.util import createActions
from qt.pe.image_viewer import ImageViewer
from qt.pe.image_viewer import (QWidgetImageViewer,
QWidgetImageViewerController, QLabelImageViewerController)
tr = trget("ui")
class DetailsDialog(DetailsDialogBase):
def __init__(self, parent, app):
self.vController = None
super().__init__(parent, app)
self.selectedPixmap = QPixmap()
self.referencePixmap = QPixmap()
self.scaledSelectedPixmap = QPixmap()
self.scaledReferencePixmap = QPixmap()
self.scaleFactor = 1.0
self.bestFit = True
def setupActions(self):
# (name, shortcut, icon, desc, func)
@ -46,7 +32,7 @@ class DetailsDialog(DetailsDialogBase):
# FIXME probably not used right now
"actionSwap",
QKeySequence.Backspace,
"swap",
"view-refresh",
tr("Swap images"),
self.swapImages,
),
@ -67,14 +53,14 @@ class DetailsDialog(DetailsDialogBase):
(
"actionNormalSize",
QKeySequence.Refresh,
"zoom-normal",
"zoom-original",
tr("Normal size"),
self.zoomNormalSize,
),
(
"actionBestFit",
tr("Ctrl+p"),
"zoom-reset",
"zoom-best-fit",
tr("Best fit"),
self.zoomBestFit,
)
@ -97,7 +83,8 @@ class DetailsDialog(DetailsDialogBase):
self.horizontalLayout.setColumnStretch(2,1)
self.horizontalLayout.setSpacing(4)
self.selectedImage = ImageViewer(self, "selectedImage")
self.selectedImageViewer = QWidgetImageViewer(
self, "selectedImage")
# self.selectedImage = QLabel(self)
# sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
# sizePolicy.setHorizontalStretch(0)
@ -109,7 +96,7 @@ class DetailsDialog(DetailsDialogBase):
# self.selectedImage.setScaledContents(False)
# self.selectedImage.setAlignment(Qt.AlignCenter)
# # self.horizontalLayout.addWidget(self.selectedImage)
self.horizontalLayout.addWidget(self.selectedImage, 0, 0, 3, 1)
self.horizontalLayout.addWidget(self.selectedImageViewer, 0, 0, 3, 1)
self.verticalToolBar = QToolBar(self)
self.verticalToolBar.setOrientation(Qt.Orientation(2))
@ -119,7 +106,7 @@ class DetailsDialog(DetailsDialogBase):
self.buttonImgSwap = QToolButton(self.verticalToolBar)
self.buttonImgSwap.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.buttonImgSwap.setIcon(QIcon.fromTheme('document-revert', \
self.buttonImgSwap.setIcon(QIcon.fromTheme('view-refresh', \
self.style().standardIcon(QStyle.SP_BrowserReload)))
self.buttonImgSwap.setText('Swap images')
self.buttonImgSwap.setToolTip('Swap images')
@ -130,7 +117,7 @@ class DetailsDialog(DetailsDialogBase):
self.buttonZoomIn.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.buttonZoomIn.setDefaultAction(self.actionZoomIn)
self.buttonZoomIn.setText('ZoomIn')
self.buttonZoomIn.setIcon(QIcon.fromTheme(('zoom-in'), QIcon(":images/zoom-in.png")))
self.buttonZoomIn.setIcon(QIcon.fromTheme('zoom-in'))
self.buttonZoomOut = QToolButton(self.verticalToolBar)
self.buttonZoomOut.setToolButtonStyle(Qt.ToolButtonIconOnly)
@ -162,7 +149,8 @@ class DetailsDialog(DetailsDialogBase):
self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignCenter)
# self.horizontalLayout.addWidget(self.verticalToolBar, Qt.AlignVCenter)
self.referenceImage = ImageViewer(self, "referenceImage")
self.referenceImageViewer = QWidgetImageViewer(
self, "referenceImage")
# self.referenceImage = QLabel(self)
# sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
# sizePolicy.setHorizontalStretch(0)
@ -172,7 +160,7 @@ class DetailsDialog(DetailsDialogBase):
# )
# self.referenceImage.setSizePolicy(sizePolicy)
# self.referenceImage.setAlignment(Qt.AlignCenter)
self.horizontalLayout.addWidget(self.referenceImage, 0, 2, 3, 1)
self.horizontalLayout.addWidget(self.referenceImageViewer, 0, 2, 3, 1)
# self.horizontalLayout.addWidget(self.referenceImage)
self.verticalLayout.addLayout(self.horizontalLayout)
self.tableView = DetailsTable(self)
@ -189,9 +177,22 @@ class DetailsDialog(DetailsDialogBase):
self.verticalLayout.addWidget(self.tableView)
self.disable_buttons()
# We use different types of controller depending on the
# underlying widgets we use to display images
# because their interface methods might differ
if isinstance(self.selectedImageViewer, QWidgetImageViewer):
self.vController = QWidgetImageViewerController(
self.selectedImageViewer,
self.referenceImageViewer,
self)
elif isinstance(self.selectedImageViewer, ScrollAreaImageViewer):
self.vController = (
self.selectedImageViewer,
self.referenceImageViewer,
self)
def _update(self):
print("_update()")
if not self.app.model.selected_dupes:
self.clear_all()
return
@ -199,135 +200,21 @@ 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: # currently selected file is the ref
self.referencePixmap = QPixmap()
self.scaledReferencePixmap = QPixmap()
self.buttonImgSwap.setEnabled(False)
# disable the blank widget.
self.disable_widget(self.referenceImage)
else:
self.referencePixmap = QPixmap(str(ref.path))
self.buttonImgSwap.setEnabled(True)
self.enable_widget(self.referenceImage)
self.update_selected_widget()
self.update_reference_widget()
self._updateImages()
if self.vController is None:
return
self.vController.update(ref, dupe)
def _updateImages(self):
target_size = None
if self.selectedPixmap.isNull():
# self.disable_widget(self.selectedImage, self.referenceImage)
pass
else:
target_size = self.selectedImage.size()
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(self.scaledSelectedPixmap)
if self.referencePixmap.isNull():
# self.disable_widget(self.referenceImage, self.selectedImage)
pass
else:
# 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:
self.scaledReferencePixmap = self.referencePixmap.scaled(
target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.referenceImage.setPixmap(self.scaledReferencePixmap)
def update_selected_widget(self):
print("update_selected_widget()")
if not self.selectedPixmap.isNull():
self.enable_widget(self.selectedImage)
self.connect_signal(self.selectedImage, self.referenceImage)
else:
self.disable_widget(self.selectedImage)
self.disconnect_signal(self.referenceImage)
def update_reference_widget(self):
print("update_reference_widget()")
if not self.referencePixmap.isNull():
self.enable_widget(self.referenceImage)
self.connect_signal(self.referenceImage, self.selectedImage)
else:
self.disable_widget(self.referenceImage)
self.disconnect_signal(self.selectedImage)
def enable_widget(self, widget):
"""We want to receive signals from the other_widget."""
print(f"enable_widget({widget})")
if not widget.isEnabled():
widget.setEnabled(True)
def disable_widget(self, widget):
"""Disables this widget and prevents receiving signals from other_widget."""
print(f"disable_widget({widget})")
widget.setPixmap(QPixmap())
widget.setDisabled(True)
def connect_signal(self, widget, other_widget):
"""We want this widget to send its signal to the other_widget."""
print(f"connect_signal({widget}, {other_widget})")
if widget.connection is None:
if other_widget.isEnabled():
widget.connection = widget.mouseMoved.connect(other_widget.slot_paint_event)
print(f"Connected signal from {widget} to slot of {other_widget}")
def disconnect_signal(self, other_widget):
"""We don't want this widget to send its signal anymore to the other_widget."""
print(f"disconnect_signal({other_widget}")
if other_widget.connection:
other_widget.mouseMoved.disconnect()
other_widget.connection = None
print(f"Disconnected signal from {other_widget}")
def resetState(self):
self.referencePixmap = QPixmap()
self.scaledReferencePixmap = QPixmap()
self.selectedPixmap = QPixmap()
self.scaledSelectedPixmap = QPixmap()
self.buttonZoomIn.setEnabled(False)
self.buttonZoomOut.setEnabled(False)
self.buttonBestFit.setEnabled(False) # active mode by default
self.buttonNormalSize.setEnabled(True)
self.bestFit = True
self.scaleFactor = 1.0
self.referenceImage.setCenter()
self.selectedImage.setCenter()
if not self.vController.bestFit:
return
self.vController._updateImages()
def clear_all(self):
"""No item from the model, disable and clear everything."""
self.resetState()
self.selectedPixmap = QPixmap()
self.scaledSelectedPixmap = QPixmap()
self.selectedImage.setPixmap(QPixmap())
self.selectedImage.setDisabled(True)
self.referencePixmap = QPixmap()
self.scaledReferencePixmap = QPixmap()
self.referenceImage.setPixmap(QPixmap())
self.referenceImage.setDisabled(True)
self.buttonImgSwap.setDisabled(True)
self.buttonNormalSize.setDisabled(True)
self.vController.clear_all()
def disable_buttons(self):
# FIXME Only called once at startup
self.buttonImgSwap.setEnabled(False)
self.buttonZoomIn.setEnabled(False)
self.buttonZoomOut.setEnabled(False)
@ -336,102 +223,55 @@ class DetailsDialog(DetailsDialogBase):
# --- Override
def resizeEvent(self, event):
if not self.bestFit:
if self.vController is None:
return
self._updateImages()
def show(self):
print("show()")
DetailsDialogBase.show(self)
self._update()
# model --> view
def refresh(self):
print("refresh()")
DetailsDialogBase.refresh(self)
if self.isVisible():
self._update()
# ImageViewers
def scaleImages(self, factor):
self.scaleFactor *= factor
print(f'QDialog scaleFactor = {self.scaleFactor} (+factor {factor})')
self.referenceImage.scale(self.scaleFactor)
self.selectedImage.scale(self.scaleFactor)
self.buttonZoomIn.setEnabled(self.scaleFactor < 16.0)
self.buttonZoomOut.setEnabled(self.scaleFactor > 1.0)
self.buttonBestFit.setEnabled(self.bestFit is False)
self.buttonNormalSize.setEnabled(self.scaleFactor != 1.0)
self.vController.scaleImages(factor)
@pyqtSlot()
def swapImages(self):
"""Swap pixmaps between ImageViewers."""
if self.bestFit:
self.selectedImage.setPixmap(self.scaledReferencePixmap)
self.referenceImage.setPixmap(self.scaledSelectedPixmap)
else:
self.selectedImage.setPixmap(self.referencePixmap)
self.referenceImage.setPixmap(self.selectedPixmap)
self.vController.swapImages()
# swap the columns in the details table as well
self.tableView.horizontalHeader().swapSections(1, 2)
@pyqtSlot()
def deswapImages(self):
"""Restore swapped pixmaps between ImageViewers."""
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.vController.deswapImages()
# deswap the columns in the details table as well
self.tableView.horizontalHeader().swapSections(1, 2)
@pyqtSlot()
def zoomIn(self):
self.scaleImages(1.25)
self.vController.scaleImages(1.25)
@pyqtSlot()
def zoomOut(self):
self.scaleImages(0.8)
self.vController.scaleImages(0.8)
@pyqtSlot()
def scale_to_bestfit(self):
self.referenceImage.scale(self.scaleFactor)
self.selectedImage.scale(self.scaleFactor)
self.referenceImage.setCenter()
self.selectedImage.setCenter()
self._updateImages()
self.vController.scale_to_bestfit()
@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.vController.zoomBestFit()
self.scale_to_bestfit()
@pyqtSlot()
def zoomNormalSize(self):
self.bestFit = False
self.scaleFactor = 1.0
self.selectedImage.setPixmap(self.selectedPixmap)
self.referenceImage.setPixmap(self.referencePixmap)
self.selectedImage.pixmapReset()
self.referenceImage.pixmapReset()
self.update_selected_widget()
self.update_reference_widget()
self.buttonNormalSize.setEnabled(False)
self.buttonZoomIn.setEnabled(True)
self.buttonZoomOut.setEnabled(True)
self.buttonBestFit.setEnabled(True)
self.vController.zoomNormalSize()

View File

@ -2,33 +2,427 @@
# 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, pyqtSlot, pyqtSignal, QEvent
from PyQt5.QtCore import QObject, Qt, QSize, QRectF, QPointF, pyqtSlot, pyqtSignal, QEvent
from PyQt5.QtGui import QPixmap, QPainter, QPalette
from PyQt5.QtWidgets import ( QLabel, QSizePolicy, QWidget, QScrollArea,
QApplication, QAbstractScrollArea )
from PyQt5.QtWidgets import ( QLabel, QSizePolicy, QWidget, QScrollArea,
QScrollBar, QApplication, QAbstractScrollArea )
class ImageViewer(QWidget):
#TODO: fix panning while zoomed-in
#TODO: fix scroll area not showing up
#TODO: add keyboard shortcuts
class BaseController(QObject):
"""Base interface to keep image viewers synchronized.
Relays function calls. Singleton. """
def __init__(self, selectedViewer, referenceViewer, parent):
super().__init__()
self.selectedViewer = selectedViewer
self.referenceViewer = referenceViewer
self.selectedPixmap = QPixmap()
self.referencePixmap = QPixmap()
self.scaledSelectedPixmap = QPixmap()
self.scaledReferencePixmap = QPixmap()
self.scaleFactor = 1.0
self.bestFit = True
self.parent = parent #needed to change buttons' states
self._setupConnections()
def _setupConnections(self): #virtual
pass
def update(self, ref, dupe):
self.resetState()
self.selectedPixmap = QPixmap(str(dupe.path))
if ref is dupe: # currently selected file is the ref
self.referencePixmap = QPixmap()
self.scaledReferencePixmap = QPixmap()
self.parent.buttonImgSwap.setEnabled(False)
# disable the blank widget.
self.disable_widget(self.referenceViewer)
else:
self.referencePixmap = QPixmap(str(ref.path))
self.parent.buttonImgSwap.setEnabled(True)
self.enable_widget(self.referenceViewer)
self.update_selected_widget()
self.update_reference_widget()
self._updateImages()
def _updateImages(self):
target_size = None
if self.selectedPixmap.isNull():
# self.disable_widget(self.selectedViewer, self.referenceViewer)
pass
else:
target_size = self.selectedViewer.size()
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.selectedViewer.setPixmap(self.scaledSelectedPixmap)
if self.referencePixmap.isNull():
# self.disable_widget(self.referenceViewer, self.selectedViewer)
pass
else:
# 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.setPixmap(self.scaledReferencePixmap)
@pyqtSlot(float)
def scaleImages(self, factor):
self.scaleFactor *= factor
print(f'Controller scaleFactor = \
{self.scaleFactor} (+factor {factor})')
self.parent.buttonZoomIn.setEnabled(self.scaleFactor < 16.0)
self.parent.buttonZoomOut.setEnabled(self.scaleFactor > 1.0)
self.parent.buttonBestFit.setEnabled(self.bestFit is False)
self.parent.buttonNormalSize.setEnabled(self.scaleFactor != 1.0)
def sefCenter(self):
#FIXME need specialization?
self.selectedViewer.setCenter()
self.referenceViewer.setCenter()
def resetState(self):
self.selectedPixmap = QPixmap()
self.scaledSelectedPixmap = QPixmap()
self.referencePixmap = QPixmap()
self.scaledReferencePixmap = QPixmap()
self.setBestFit(True)
self.scaleFactor = 1.0
self.setCenter()
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 clear_all(self):
"""No item from the model, disable and clear everything."""
self.resetState()
self.selectedViewer.setPixmap(QPixmap())
self.selectedViewer.setDisabled(True)
self.referenceViewer.setPixmap(QPixmap())
self.referenceViewer.setDisabled(True)
self.parent.buttonImgSwap.setDisabled(True)
self.parent.buttonNormalSize.setDisabled(True)
def swapImages(self):
if self.bestFit:
self.selectedViewer.setPixmap(self.scaledReferencePixmap)
self.referenceViewer.setPixmap(self.scaledSelectedPixmap)
else:
self.selectedViewer.setPixmap(self.referencePixmap)
self.referenceViewer.setPixmap(self.selectedPixmap)
def deswapImages(self):
if self.bestFit:
self.selectedViewer.setPixmap(self.scaledSelectedPixmap)
self.referenceViewer.setPixmap(self.scaledReferencePixmap)
else:
self.selectedViewer.setPixmap(self.selectedPixmap)
self.referenceViewer.setPixmap(self.referencePixmap)
def zoomBestFit(self):
self.setBestFit(True)
self.scaleFactor = 1.0
self.parent.buttonBestFit.setEnabled(False)
self.parent.buttonZoomOut.setEnabled(False)
self.parent.buttonZoomIn.setEnabled(False)
self.parent.buttonNormalSize.setEnabled(True)
def zoomNormalSize(self):
self.setBestFit(False)
self.scaleFactor = 1.0
self.selectedViewer.setPixmap(self.selectedPixmap)
self.referenceViewer.setPixmap(self.referencePixmap)
self.selectedViewer.pixmapReset()
self.referenceViewer.pixmapReset()
self.update_selected_widget()
self.update_reference_widget()
self.parent.buttonNormalSize.setEnabled(False)
self.parent.buttonZoomIn.setEnabled(True)
self.parent.buttonZoomOut.setEnabled(True)
self.parent.buttonBestFit.setEnabled(True)
def setBestFit(self, value):
self.bestFit = value
self.selectedViewer.bestFit = value
self.referenceViewer.bestFit = value
def setCenter(self):
self.selectedViewer.setCenter()
self.referenceViewer.setCenter()
def update_selected_widget(self):
print("update_selected_widget()")
if not self.selectedPixmap.isNull():
self.enable_widget(self.selectedViewer)
self.connect_signal(self.selectedViewer, self.referenceViewer)
else:
self.disable_widget(self.selectedViewer)
self.disconnect_signal(self.referenceViewer)
def update_reference_widget(self):
print("update_reference_widget()")
if not self.referencePixmap.isNull():
self.enable_widget(self.referenceViewer)
self.connect_signal(self.referenceViewer, self.selectedViewer)
else:
self.disable_widget(self.referenceViewer)
self.disconnect_signal(self.selectedViewer)
def enable_widget(self, widget):
"""We want to receive signals from the other_widget."""
print(f"enable_widget({widget})")
if not widget.isEnabled():
widget.setEnabled(True)
def disable_widget(self, widget):
"""Disables this widget and prevents receiving signals from other_widget."""
print(f"disable_widget({widget})")
widget.setPixmap(QPixmap())
widget.setDisabled(True)
def connect_signal(self, widget, other_widget):
"""We want this widget to send its signal to the other_widget."""
print(f"connect_signal({widget}, {other_widget})")
if widget.connection is None:
if other_widget.isEnabled():
widget.connection = widget.mouseDragged.connect(other_widget.slot_paint_event)
print(f"Connected signal from {widget} to slot of {other_widget}")
def disconnect_signal(self, other_widget):
"""We don't want this widget to send its signal anymore to the other_widget."""
print(f"disconnect_signal({other_widget}")
if other_widget.connection:
other_widget.mouseDragged.disconnect()
other_widget.connection = None
print(f"Disconnected signal from {other_widget}")
class QWidgetImageViewerController(BaseController):
def __init__(self, selectedViewer, referenceViewer, parent):
super().__init__(selectedViewer, referenceViewer, parent)
# self._setupConnections()
def _setupConnections(self):
self.selectedViewer.mouseWheeled.connect(
self.scaleImages)
self.referenceViewer.mouseWheeled.connect(
self.scaleImages)
def scale(self, factor):
self.selectedViewer.scale(factor)
self.referenceViewer.scale(factor)
@pyqtSlot(float)
def scaleImages(self, factor):
super().scaleImages(factor)
# we scale the Qwidget itself in this case
self.selectedViewer.scale(self.scaleFactor)
self.referenceViewer.scale(self.scaleFactor)
def scale_to_bestfit(self):
self.scale(1.0)
super().setCenter()
super()._updateImages()
class QLabelImageViewerController(BaseController):
def __init__(self, selectedViewer, referenceViewer, parent):
super().__init__(selectedViewer, referenceViewer, parent)
def scale(self, factor):
pass #FIXME
@pyqtSlot(float)
def scaleImages(self, factor):
super().scaleImages(factor)
# we scale the member Qlable in this case
self.selectedViewer.scale(self.scaleFactor)
self.referenceViewer.scale(self.scaleFactor)
class GraphicsViewController(BaseController):
pass
class QWidgetImageViewer(QWidget):
"""Displays image and allows manipulations."""
mouseMoved = pyqtSignal(QPointF)
mouseDragged = pyqtSignal(QPointF)
mouseWheeled = pyqtSignal(float)
def __init__(self, parent, name=""):
super().__init__(parent)
self.parent = parent
self.app = QApplication
self.pixmap = QPixmap()
self.m_rect = QRectF()
self.reference = QPointF()
self.delta = QPointF()
self.scalefactor = 1.0
self.drag = False
self._app = QApplication
self._pixmap = QPixmap()
self._rect = QRectF()
self._reference = QPointF()
self._delta = QPointF()
self._scaleFactor = 1.0
self._drag = False
self.connection = None # signal bound to a slot
self.instance_name = name
self._instance_name = name
self.bestFit = True
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)
# sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
# self.label.setBackgroundRole(QPalette.Base)
# self.label.setSizePolicy(sizePolicy)
# self.label.setAlignment(Qt.AlignCenter)
# self.label.setScaledContents(True)
# self.scrollarea = QScrollArea(self)
# self.scrollarea.setBackgroundRole(QPalette.Dark)
# self.scrollarea.setWidgetResizable(True)
# self.scrollarea.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents)
# # self.scrollarea.viewport().setAttribute(Qt.WA_StaticContents)
# self.scrollarea.setWidget(self.label)
# self.scrollarea.setVisible(True)
def __repr__(self):
return f'{self._instance_name}'
def paintEvent(self, event):
painter = QPainter(self)
painter.translate(self.rect().center())
painter.scale(self._scaleFactor, self._scaleFactor)
painter.translate(self._delta)
painter.drawPixmap(self._rect.topLeft(), self._pixmap)
# print(f"{self} paintEvent delta={self._delta}")
def setCenter(self):
""" Resets origin """
self._delta = QPointF()
self._scaleFactor = 1.0
self.scale(self._scaleFactor)
self.update()
def changeEvent(self, event):
if event.type() == QEvent.EnabledChange:
print(f"{self} is now {'enabled' if self.isEnabled() else 'disabled'}")
def mousePressEvent(self, event):
if self.bestFit:
event.ignore()
return
if event.buttons() == Qt.LeftButton:
self._drag = True
self._reference = event.pos()
self._app.setOverrideCursor(Qt.ClosedHandCursor)
self.setMouseTracking(True)
def mouseMoveEvent(self, event):
if self.bestFit:
event.ignore()
return
self._delta += (event.pos() - self._reference) * 1.0/self._scaleFactor
self._reference = event.pos()
if self._drag:
self.mouseDragged.emit(self._delta)
self.update()
def mouseReleaseEvent(self, event):
if self.bestFit:
event.ignore()
return
if event.buttons() == Qt.LeftButton:
drag = False
self._app.restoreOverrideCursor()
self.setMouseTracking(False)
def wheelEvent(self, event):
if self.bestFit:
event.ignore()
return
if event.angleDelta().y() > 0:
self.mouseWheeled.emit(1.25) # zoom-in
else:
self.mouseWheeled.emit(0.8) # zoom-out
def setPixmap(self, pixmap):
if pixmap.isNull():
if not self._pixmap.isNull():
self._pixmap = pixmap
self.update()
return
elif not self.isEnabled():
self.setEnabled(True)
self._pixmap = pixmap
self._rect = self._pixmap.rect()
self._rect.translate(-self._rect.center())
self.update()
def scale(self, factor):
self._scaleFactor = factor
self.update()
def sizeHint(self):
return QSize(400, 400)
@pyqtSlot()
def pixmapReset(self):
"""Called when the pixmap is set back to original size."""
self._scaleFactor = 1.0
self.update()
@pyqtSlot(QPointF)
def slot_paint_event(self, delta):
self._delta = delta
self.update()
print(f"{self} received signal from {self.sender()}")
class ScrollAreaImageViewer(QScrollArea):
"""Version with Qlabel for testing"""
mouseDragged = pyqtSignal(QPointF)
def __init__(self, parent, name=""):
super().__init__(parent)
self._parent = parent
self._app = QApplication
self._pixmap = QPixmap()
self._rect = QRectF()
self._reference = QPointF()
self._delta = QPointF()
self._scaleFactor = 1.0
self._drag = False
self.connection = None # signal bound to a slot
self._instance_name = name
self.label = QLabel()
sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
@ -40,31 +434,31 @@ class ImageViewer(QWidget):
self.label.setAlignment(Qt.AlignCenter)
self.label.setScaledContents(True)
self.area.setWidget(self.label)
self.area.setVisible(False)
self.scrollarea = QScrollArea(self)
self.setBackgroundRole(QPalette.Dark)
self.setWidgetResizable(True)
self.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents)
# self.scrollarea.viewport().setAttribute(Qt.WA_StaticContents)
self.setWidget(self.label)
self.setVisible(True)
def __repr__(self):
return f'{self.instance_name}'
@pyqtSlot(QPointF)
def slot_paint_event(self, delta):
self.delta = delta
self.update()
print(f"{self} received signal from {self.sender()}")
return f'{self._instance_name}'
def paintEvent(self, event):
painter = QPainter(self)
painter.translate(self.rect().center())
painter.scale(self.scalefactor, self.scalefactor)
painter.translate(self.delta)
painter.drawPixmap(self.m_rect.topLeft(), self.pixmap)
# print(f"{self} paintEvent delta={self.delta}")
painter.scale(self._scaleFactor, self._scaleFactor)
painter.translate(self._delta)
painter.drawPixmap(self._rect.topLeft(), self._pixmap)
# print(f"{self} paintEvent delta={self._delta}")
def setCenter(self):
""" Resets origin """
self.delta = QPointF()
self.scalefactor = 1.0
self.scale(self.scalefactor)
self._delta = QPointF()
self._scaleFactor = 1.0
self.scale(self._scaleFactor)
self.update()
def changeEvent(self, event):
@ -72,70 +466,115 @@ class ImageViewer(QWidget):
print(f"{self} is now {'enabled' if self.isEnabled() else 'disabled'}")
def mousePressEvent(self, event):
if self.parent.bestFit:
if self._parent.bestFit:
event.ignore()
return
if event.buttons() == Qt.LeftButton:
self.drag = True
self._drag = True
self.reference = event.pos()
self.app.setOverrideCursor(Qt.ClosedHandCursor)
self._reference = event.pos()
self._app.setOverrideCursor(Qt.ClosedHandCursor)
self.setMouseTracking(True)
def mouseMoveEvent(self, event):
if self.parent.bestFit:
if self._parent.bestFit:
event.ignore()
return
self.delta += (event.pos() - self.reference) * 1.0/self.scalefactor
self.reference = event.pos()
if self.drag:
self.mouseMoved.emit(self.delta)
self._delta += (event.pos() - self._reference) * 1.0/self._scaleFactor
self._reference = event.pos()
if self._drag:
self.mouseDragged.emit(self._delta)
self.update()
def mouseReleaseEvent(self, event):
if self.parent.bestFit:
if self._parent.bestFit:
event.ignore()
return
if event.buttons() == Qt.LeftButton:
drag = False
self.app.restoreOverrideCursor()
self._app.restoreOverrideCursor()
self.setMouseTracking(False)
def wheelEvent(self, event):
if self.parent.bestFit:
if self._parent.bestFit:
event.ignore()
return
if event.angleDelta().y() > 0:
self.parent.zoomIn()
self._parent.zoomIn()
else:
self.parent.zoomOut()
self._parent.zoomOut()
def setPixmap(self, pixmap):
if pixmap.isNull():
if not self.pixmap.isNull():
self.pixmap = pixmap
self.update()
return
elif not self.isEnabled():
self.setEnabled(True)
self.pixmap = pixmap
self.m_rect = self.pixmap.rect()
self.m_rect.translate(-self.m_rect.center())
#FIXME refactored
# if pixmap.isNull():
# if not self._pixmap.isNull():
# self._pixmap = pixmap
# self.update()
# return
# elif not self.isEnabled():
# self.setEnabled(True)
# self._pixmap = pixmap
self.label.setPixmap(pixmap)
self._rect = self._pixmap.rect()
self._rect.translate(-self._rect.center())
self.update()
def scale(self, factor):
self.scalefactor = factor
# self.label.resize(self.scalefactor * self.label.size())
self._scaleFactor = factor
self.label.resize(self._scaleFactor * self.label.pixmap().size())
self.adjustScrollBar(self.scrollarea.horizontalScrollBar(), factor)
self.adjustScrollBar(self.scrollarea.verticalScrollBar(), factor)
self.update()
def adjustScrollBar(self, scrollBar, factor):
scrollBar.setValue(int(factor * scrollBar.value() + ((factor - 1) * scrollBar.pageStep()/2)))
def sizeHint(self):
return QSize(400, 400)
@pyqtSlot()
def pixmapReset(self):
"""Called when the pixmap is set back to original size."""
self.scalefactor = 1.0
self.update()
self._scaleFactor = 1.0
self.update()
@pyqtSlot(QPointF)
def slot_paint_event(self, delta):
self._delta = delta
self.update()
print(f"{self} received signal from {self.sender()}")
from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
class SceneImageViewer(QGraphicsView):
"""Re-Implementation test"""
def __init__(self, parent):
super().__init__(parent)
self._scene = QGraphicsScene()
self._item = QGraphicsPixmapItem()
self.setScene(_scene)
self._scene.addItem(self.item)
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setResizeAnchor(QGraphicsView.AnchorViewCenter)
def setPixmap(self, pixmap):
self._item.setPixmap(pixmap)
offset = -QRectF(pixmap.rect()).center()
self._item.setOffset(offset)
self.setSceneRect(offset.x()*4, offset.y()*4, -offset.x()*8, -offset.y()*8)
self.translate(1, 1)
def scale(self, factor):
self.scale(factor, factor)
def sizeHint():
return QSize(400, 400)