diff --git a/qt/pe/details_dialog.py b/qt/pe/details_dialog.py index 7348918d..a3a51641 100644 --- a/qt/pe/details_dialog.py +++ b/qt/pe/details_dialog.py @@ -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() diff --git a/qt/pe/image_viewer.py b/qt/pe/image_viewer.py index bdc18dc1..f7b85c76 100644 --- a/qt/pe/image_viewer.py +++ b/qt/pe/image_viewer.py @@ -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() \ No newline at end of file + 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)