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

Cleanup of details table

This commit is contained in:
glubsy 2020-06-29 22:31:43 +02:00
parent 36ab84423a
commit e7b3252534
3 changed files with 174 additions and 346 deletions

View File

@ -7,7 +7,7 @@
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtCore import Qt, QAbstractTableModel from PyQt5.QtCore import Qt, QAbstractTableModel
from PyQt5.QtWidgets import QHeaderView, QTableView from PyQt5.QtWidgets import QHeaderView, QTableView, QAbstractItemView
from hscommon.trans import trget from hscommon.trans import trget
@ -51,9 +51,11 @@ class DetailsTable(QTableView):
QTableView.__init__(self, *args) QTableView.__init__(self, *args)
self.setAlternatingRowColors(True) self.setAlternatingRowColors(True)
self.setSelectionBehavior(QTableView.SelectRows) self.setSelectionBehavior(QTableView.SelectRows)
self.setSelectionMode(QTableView.SingleSelection)
self.setShowGrid(False) self.setShowGrid(False)
self.setWordWrap(False) self.setWordWrap(False)
def setModel(self, model): def setModel(self, model):
QTableView.setModel(self, model) QTableView.setModel(self, model)
# The model needs to be set to set header stuff # The model needs to be set to set header stuff

View File

@ -4,20 +4,21 @@
# 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, pyqtSlot, pyqtSignal from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import (QLayout, QVBoxLayout, QAbstractItemView, QHBoxLayout, from PyQt5.QtWidgets import (
QSizePolicy, QGridLayout, QWidget, QSpacerItem, QSplitter, QFrame ) QAbstractItemView, QSizePolicy, QGridLayout, QSplitter, QFrame)
from hscommon.trans import trget from hscommon.trans import trget
from hscommon import desktop
from ..details_dialog import DetailsDialog as DetailsDialogBase from ..details_dialog import DetailsDialog as DetailsDialogBase
from ..details_table import DetailsTable from ..details_table import DetailsTable
from qtlib.util import createActions from .image_viewer import (
from qt.pe.image_viewer import ( ViewerToolBar, QWidgetImageViewer,
ViewerToolBar, QWidgetImageViewer, ScrollAreaImageViewer, GraphicsViewViewer, ScrollAreaImageViewer, GraphicsViewViewer,
QWidgetController, ScrollAreaController, GraphicsViewController) QWidgetController, ScrollAreaController,
GraphicsViewController)
tr = trget("ui") tr = trget("ui")
class DetailsDialog(DetailsDialogBase): class DetailsDialog(DetailsDialogBase):
def __init__(self, parent, app): def __init__(self, parent, app):
self.vController = None self.vController = None
@ -27,47 +28,27 @@ class DetailsDialog(DetailsDialogBase):
self.setWindowTitle(tr("Details")) self.setWindowTitle(tr("Details"))
self.resize(502, 502) self.resize(502, 502)
self.setMinimumSize(QSize(250, 250)) self.setMinimumSize(QSize(250, 250))
# self.verticalLayout = QVBoxLayout(self)
# self.verticalLayout.setSpacing(0)
# self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.splitter = QSplitter(Qt.Vertical, self) self.splitter = QSplitter(Qt.Vertical, self)
self.setCentralWidget(self.splitter) self.setCentralWidget(self.splitter)
self.topFrame = QFrame() self.topFrame = QFrame()
self.topFrame.setFrameShape(QFrame.StyledPanel) self.topFrame.setFrameShape(QFrame.StyledPanel)
self.horizontalLayout = QGridLayout() self.horizontalLayout = QGridLayout()
# Minimum width for the toolbar in the middle: # Minimum width for the toolbar in the middle:
self.horizontalLayout.setColumnMinimumWidth(1, 10) self.horizontalLayout.setColumnMinimumWidth(1, 10)
self.horizontalLayout.setContentsMargins(0, 0, 0, 0) self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout.setColumnStretch(0,24) self.horizontalLayout.setColumnStretch(0, 32)
self.horizontalLayout.setColumnStretch(1,1) # Smaller value for the toolbar in the middle to avoid excessive resize
self.horizontalLayout.setColumnStretch(2,24) self.horizontalLayout.setColumnStretch(1, 2)
self.horizontalLayout.setColumnStretch(2, 32)
# This avoids toolbar getting incorrectly partially hidden when window resizes
self.horizontalLayout.setRowStretch(0, 1)
self.horizontalLayout.setRowStretch(1, 24)
self.horizontalLayout.setRowStretch(2, 1)
self.horizontalLayout.setSpacing(1) # probably not important
# This avoids toolbar getting incorrectly resized when window resizes self.selectedImageViewer = ScrollAreaImageViewer(self, "selectedImage")
self.horizontalLayout.setRowStretch(0,1)
self.horizontalLayout.setRowStretch(1,24)
self.horizontalLayout.setRowStretch(2,1)
self.horizontalLayout.setSpacing(1)
self.selectedImageViewer = GraphicsViewViewer(self, "selectedImage")
# self.selectedImage = QLabel(self)
# sizePolicy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
# sizePolicy.setHorizontalStretch(0)
# sizePolicy.setVerticalStretch(0)
# sizePolicy.setHeightForWidth(
# self.selectedImage.sizePolicy().hasHeightForWidth()
# )
# self.selectedImage.setSizePolicy(sizePolicy)
# self.selectedImage.setScaledContents(False)
# self.selectedImage.setAlignment(Qt.AlignCenter)
# # self.horizontalLayout.addWidget(self.selectedImage)
self.horizontalLayout.addWidget(self.selectedImageViewer, 0, 0, 3, 1) self.horizontalLayout.addWidget(self.selectedImageViewer, 0, 0, 3, 1)
# Use a specific type of controller depending on the underlying viewer type
# We use different types of controller depending on the
# underlying widgets we use to display images
# because their interface and methods might differ
if isinstance(self.selectedImageViewer, QWidgetImageViewer): if isinstance(self.selectedImageViewer, QWidgetImageViewer):
self.vController = QWidgetController(self) self.vController = QWidgetController(self)
elif isinstance(self.selectedImageViewer, ScrollAreaImageViewer): elif isinstance(self.selectedImageViewer, ScrollAreaImageViewer):
@ -75,63 +56,34 @@ class DetailsDialog(DetailsDialogBase):
elif isinstance(self.selectedImageViewer, GraphicsViewViewer): elif isinstance(self.selectedImageViewer, GraphicsViewViewer):
self.vController = GraphicsViewController(self) self.vController = GraphicsViewController(self)
# self.horizontalLayout.addItem(QSpacerItem(5,0, QSizePolicy.Minimum),
# 1, 3, 1, 1, Qt.Alignment(Qt.AlignRight))
self.verticalToolBar = ViewerToolBar(self, self.vController) self.verticalToolBar = ViewerToolBar(self, self.vController)
# self.verticalToolBar.setMaximumWidth(10)
self.verticalToolBar.setOrientation(Qt.Orientation(Qt.Vertical)) self.verticalToolBar.setOrientation(Qt.Orientation(Qt.Vertical))
# self.subVLayout = QVBoxLayout(self)
# self.subVLayout.addWidget(self.verticalToolBar)
# self.horizontalLayout.addLayout(self.subVLayout)
self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignCenter) self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignCenter)
self.referenceImageViewer = GraphicsViewViewer(self, "referenceImage") self.referenceImageViewer = ScrollAreaImageViewer(self, "referenceImage")
# self.referenceImage = QLabel(self)
# sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
# sizePolicy.setHorizontalStretch(0)
# sizePolicy.setVerticalStretch(0)
# self.verticalToolBar.setSizePolicy(sizePolicy)
# sizePolicy.setHeightForWidth(
# self.referenceImage.sizePolicy().hasHeightForWidth()
# )
# self.referenceImageViewer.setSizePolicy(sizePolicy)
# self.referenceImageViewer.setAlignment(Qt.AlignCenter)
self.horizontalLayout.addWidget(self.referenceImageViewer, 0, 2, 3, 1) self.horizontalLayout.addWidget(self.referenceImageViewer, 0, 2, 3, 1)
# self.verticalLayout.addLayout(self.horizontalLayout)
self.topFrame.setLayout(self.horizontalLayout) self.topFrame.setLayout(self.horizontalLayout)
self.splitter.addWidget(self.topFrame) self.splitter.addWidget(self.topFrame)
# container = QWidget(self)
# container.setLayout(self.horizontalLayout)
# self.setLayout(self.horizontalLayout)
# self.splitter.addWidget(self)
self.splitter.setStretchFactor(0, 8) self.splitter.setStretchFactor(0, 8)
self.tableView = DetailsTable(self) self.tableView = DetailsTable(self)
sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0) sizePolicy.setVerticalStretch(0)
# sizePolicy.setHeightForWidth(self.tableView.sizePolicy().hasHeightForWidth())
self.tableView.setSizePolicy(sizePolicy) self.tableView.setSizePolicy(sizePolicy)
# self.tableView.setMinimumSize(QSize(0, 190)) # self.tableView.setMinimumSize(QSize(0, 190))
# self.tableView.setMaximumSize(QSize(16777215, 190)) # self.tableView.setMaximumSize(QSize(16777215, 190))
self.tableView.setAlternatingRowColors(True) self.tableView.setAlternatingRowColors(True)
self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
self.tableView.setShowGrid(False) self.tableView.setShowGrid(False)
# self.verticalLayout.addLayout(self.tableView)
self.splitter.addWidget(self.tableView) self.splitter.addWidget(self.tableView)
self.splitter.setStretchFactor(1, 1) self.splitter.setStretchFactor(1, 1)
# Late population needed here for connections to the toolbar
# self.tableView.hide() self.vController.setupViewers(
self.selectedImageViewer, self.referenceImageViewer)
self.vController.setupViewers(self.selectedImageViewer, self.referenceImageViewer)
def _update(self): def _update(self):
if self.vController is None: # Not yet constructed! if self.vController is None: # Not yet constructed!
return return
if not self.app.model.selected_dupes: if not self.app.model.selected_dupes:
# No item from the model, disable and clear everything. # No item from the model, disable and clear everything.
@ -154,14 +106,13 @@ class DetailsDialog(DetailsDialogBase):
0, self.selectedImageViewer.size().width()) 0, self.selectedImageViewer.size().width())
self.horizontalLayout.setColumnMinimumWidth( self.horizontalLayout.setColumnMinimumWidth(
2, self.selectedImageViewer.size().width()) 2, self.selectedImageViewer.size().width())
# This works when expanding but it's ugly: # This works when expanding but it's ugly:
# if self.selectedImageViewer.size().width() > self.referenceImageViewer.size().width(): if self.selectedImageViewer.size().width() > self.referenceImageViewer.size().width():
# print(f"Before selected size: {self.selectedImageViewer.size()}\n\ # print(f"""Before selected size: {self.selectedImageViewer.size()}\n""",
# Before reference size: {self.referenceImageViewer.size()}") # f"""Before reference size: {self.referenceImageViewer.size()}""")
# self.selectedImageViewer.resize(self.referenceImageViewer.size()) self.selectedImageViewer.resize(self.referenceImageViewer.size())
# print(f"After selected size: {self.selectedImageViewer.size()}\n\ # print(f"""After selected size: {self.selectedImageViewer.size()}\n""",
# After reference size: {self.referenceImageViewer.size()}") # f"""After reference size: {self.referenceImageViewer.size()}""")
if self.vController is None or not self.vController.bestFit: if self.vController is None or not self.vController.bestFit:
return return
@ -170,8 +121,10 @@ class DetailsDialog(DetailsDialogBase):
def show(self): def show(self):
# Compute the maximum size the table view can reach # Compute the maximum size the table view can reach
# Assuming all rows below headers have the same height
self.tableView.setMaximumHeight( self.tableView.setMaximumHeight(
self.tableView.rowHeight(1) * self.tableModel.model.row_count()\ self.tableView.rowHeight(1)
* self.tableModel.model.row_count()
+ self.tableView.verticalHeader().sectionSize(0)) + self.tableView.verticalHeader().sectionSize(0))
DetailsDialogBase.show(self) DetailsDialogBase.show(self)
self._update() self._update()
@ -181,4 +134,3 @@ class DetailsDialog(DetailsDialogBase):
DetailsDialogBase.refresh(self) DetailsDialogBase.refresh(self)
if self.isVisible(): if self.isVisible():
self._update() self._update()

View File

@ -3,9 +3,11 @@
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtCore import QObject, Qt, QSize, QRectF, QPointF, QPoint, pyqtSlot, pyqtSignal, QEvent from PyQt5.QtCore import QObject, Qt, QSize, QRectF, QPointF, QPoint, pyqtSlot, pyqtSignal, QEvent
from PyQt5.QtGui import QPixmap, QPainter, QPalette, QCursor, QTransform, QIcon, QKeySequence from PyQt5.QtGui import QPixmap, QPainter, QPalette, QCursor, QIcon, QKeySequence
from PyQt5.QtWidgets import ( QToolBar, QToolButton, QAction, QLabel, QSizePolicy, QWidget, QScrollArea, from PyQt5.QtWidgets import (
QScrollBar, QApplication, QAbstractScrollArea, QStyle) QGraphicsView, QGraphicsScene, QGraphicsPixmapItem,
QToolBar, QToolButton, QAction, QWidget, QScrollArea,
QApplication, QAbstractScrollArea, QStyle)
from hscommon.trans import trget from hscommon.trans import trget
tr = trget("ui") tr = trget("ui")
@ -13,9 +15,22 @@ MAX_SCALE = 12.0
MIN_SCALE = 0.1 MIN_SCALE = 0.1
def createActions(actions, target):
# actions = [(name, shortcut, icon, desc, func)]
for name, shortcut, icon, desc, func in actions:
action = QAction(target)
if icon:
action.setIcon(QIcon.fromTheme(icon))
if shortcut:
action.setShortcut(shortcut)
action.setText(desc)
action.triggered.connect(func)
setattr(target, name, action)
class ViewerToolBar(QToolBar): class ViewerToolBar(QToolBar):
def __init__(self, parent, controller): def __init__(self, parent, controller):
super().__init__() super().__init__(parent)
self.parent = parent self.parent = parent
self.controller = controller self.controller = controller
self.setupActions(controller) self.setupActions(controller)
@ -26,21 +41,8 @@ class ViewerToolBar(QToolBar):
self.buttonNormalSize.setEnabled(False) self.buttonNormalSize.setEnabled(False)
self.buttonBestFit.setEnabled(False) self.buttonBestFit.setEnabled(False)
def createActions(self, actions, target):
# TODO try with QWidgetAction() instead in order to have
# the popup menu work in the toolbar (if resized below minimum height)
# actions = [(name, shortcut, icon, desc, func)]
for name, shortcut, icon, desc, func in actions:
action = QAction(target)
if icon:
action.setIcon(QIcon(QPixmap(":/" + icon)))
if shortcut:
action.setShortcut(shortcut)
action.setText(desc)
action.triggered.connect(func)
setattr(target, name, action)
def setupActions(self, controller): def setupActions(self, controller):
# actions = [(name, shortcut, icon, desc, func)]
ACTIONS = [ ACTIONS = [
( (
"actionZoomIn", "actionZoomIn",
@ -58,27 +60,29 @@ class ViewerToolBar(QToolBar):
), ),
( (
"actionNormalSize", "actionNormalSize",
QKeySequence.Refresh, tr("Ctrl+/"),
"zoom-original", "zoom-original",
tr("Normal size"), tr("Normal size"),
controller.zoomNormalSize, controller.zoomNormalSize,
), ),
( (
"actionBestFit", "actionBestFit",
tr("Ctrl+p"), tr("Ctrl+*"),
"zoom-best-fit", "zoom-best-fit",
tr("Best fit"), tr("Best fit"),
controller.zoomBestFit, controller.zoomBestFit,
) )
] ]
self.createActions(ACTIONS, self.parent) # TODO try with QWidgetAction() instead in order to have
# the popup menu work in the toolbar (if resized below minimum height)
createActions(ACTIONS, self)
def createButtons(self): def createButtons(self):
self.buttonImgSwap = QToolButton(self) self.buttonImgSwap = QToolButton(self)
self.buttonImgSwap.setToolButtonStyle(Qt.ToolButtonIconOnly) self.buttonImgSwap.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.buttonImgSwap.setIcon(QIcon.fromTheme('view-refresh', \ self.buttonImgSwap.setIcon(
self.style().standardIcon(QStyle.SP_BrowserReload))) QIcon.fromTheme('view-refresh',
self.style().standardIcon(QStyle.SP_BrowserReload)))
self.buttonImgSwap.setText('Swap images') self.buttonImgSwap.setText('Swap images')
self.buttonImgSwap.setToolTip('Swap images') self.buttonImgSwap.setToolTip('Swap images')
self.buttonImgSwap.pressed.connect(self.controller.swapImages) self.buttonImgSwap.pressed.connect(self.controller.swapImages)
@ -86,29 +90,30 @@ class ViewerToolBar(QToolBar):
self.buttonZoomIn = QToolButton(self) self.buttonZoomIn = QToolButton(self)
self.buttonZoomIn.setToolButtonStyle(Qt.ToolButtonIconOnly) self.buttonZoomIn.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.buttonZoomIn.setDefaultAction(self.parent.actionZoomIn) self.buttonZoomIn.setDefaultAction(self.actionZoomIn)
self.buttonZoomIn.setText('ZoomIn') # self.buttonZoomIn.setText('ZoomIn')
self.buttonZoomIn.setIcon(QIcon.fromTheme('zoom-in')) # self.buttonZoomIn.setIcon(QIcon.fromTheme('zoom-in'))
self.buttonZoomIn.setEnabled(False)
self.buttonZoomOut = QToolButton(self) self.buttonZoomOut = QToolButton(self)
self.buttonZoomOut.setToolButtonStyle(Qt.ToolButtonIconOnly) self.buttonZoomOut.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.buttonZoomOut.setDefaultAction(self.parent.actionZoomOut) self.buttonZoomOut.setDefaultAction(self.actionZoomOut)
self.buttonZoomOut.setText('ZoomOut') # self.buttonZoomOut.setText('ZoomOut')
self.buttonZoomOut.setIcon(QIcon.fromTheme('zoom-out')) # self.buttonZoomOut.setIcon(QIcon.fromTheme('zoom-out'))
self.buttonZoomOut.setEnabled(False) self.buttonZoomOut.setEnabled(False)
self.buttonNormalSize = QToolButton(self) self.buttonNormalSize = QToolButton(self)
self.buttonNormalSize.setToolButtonStyle(Qt.ToolButtonIconOnly) self.buttonNormalSize.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.buttonNormalSize.setDefaultAction(self.parent.actionNormalSize) self.buttonNormalSize.setDefaultAction(self.actionNormalSize)
self.buttonNormalSize.setText('Normal Size') # self.buttonNormalSize.setText('Normal Size')
self.buttonNormalSize.setIcon(QIcon.fromTheme('zoom-original')) # self.buttonNormalSize.setIcon(QIcon.fromTheme('zoom-original'))
self.buttonNormalSize.setEnabled(True) self.buttonNormalSize.setEnabled(True)
self.buttonBestFit = QToolButton(self) self.buttonBestFit = QToolButton(self)
self.buttonBestFit.setToolButtonStyle(Qt.ToolButtonIconOnly) self.buttonBestFit.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.buttonBestFit.setDefaultAction(self.parent.actionBestFit) self.buttonBestFit.setDefaultAction(self.actionBestFit)
self.buttonBestFit.setText('BestFit') # self.buttonBestFit.setText('BestFit')
self.buttonBestFit.setIcon(QIcon.fromTheme('zoom-best-fit')) # self.buttonBestFit.setIcon(QIcon.fromTheme('zoom-best-fit'))
self.buttonBestFit.setEnabled(False) self.buttonBestFit.setEnabled(False)
self.addWidget(self.buttonImgSwap) self.addWidget(self.buttonImgSwap)
@ -134,8 +139,7 @@ class BaseController(QObject):
self.scaledReferencePixmap = QPixmap() self.scaledReferencePixmap = QPixmap()
self.current_scale = 1.0 self.current_scale = 1.0
self.bestFit = True self.bestFit = True
self.wantScrollBars = True self.parent = parent # To change buttons' states
self.parent = parent #To change buttons' states
self.cached_group = None self.cached_group = None
def setupViewers(self, selectedViewer, referenceViewer): def setupViewers(self, selectedViewer, referenceViewer):
@ -150,17 +154,15 @@ class BaseController(QObject):
self.referenceViewer.connectMouseSignals() self.referenceViewer.connectMouseSignals()
def updateView(self, ref, dupe, group): def updateView(self, ref, dupe, group):
# Keep current scale accross dupes from the same group # To keep current scale accross dupes from the same group
same_group = True same_group = True
if group != self.cached_group: if group != self.cached_group:
same_group = False same_group = False
self.resetState() self.resetState()
# self.current_scale = 1.0
self.cached_group = group self.cached_group = group
self.selectedPixmap = QPixmap(str(dupe.path)) self.selectedPixmap = QPixmap(str(dupe.path))
if ref is dupe: # currently selected file is the actual reference file if ref is dupe: # currently selected file is the actual reference file
self.referencePixmap = QPixmap() self.referencePixmap = QPixmap()
self.scaledReferencePixmap = QPixmap() self.scaledReferencePixmap = QPixmap()
self.parent.verticalToolBar.buttonImgSwap.setEnabled(False) self.parent.verticalToolBar.buttonImgSwap.setEnabled(False)
@ -169,22 +171,20 @@ class BaseController(QObject):
self.referencePixmap = QPixmap(str(ref.path)) self.referencePixmap = QPixmap(str(ref.path))
self.parent.verticalToolBar.buttonImgSwap.setEnabled(True) self.parent.verticalToolBar.buttonImgSwap.setEnabled(True)
self.parent.verticalToolBar.buttonNormalSize.setEnabled(True) self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)
self.updateBothImages(same_group) self.updateBothImages(same_group)
self.centerViews(same_group and self.referencePixmap.isNull()) self.centerViews(same_group and self.referencePixmap.isNull())
def updateBothImages(self, same_group=False): def updateBothImages(self, same_group=False):
# FIXME this is called on every resize event, # WARNING this is called on every resize event,
ignore_update = self.referencePixmap.isNull() ignore_update = self.referencePixmap.isNull()
if ignore_update: if ignore_update:
self.selectedViewer.ignore_signal = True self.selectedViewer.ignore_signal = True
selected_size = self._updateImage(
self.selectedPixmap, self.scaledSelectedPixmap,
self.selectedViewer, None, same_group)
# the SelectedImageViewer widget sometimes ends up being bigger # the SelectedImageViewer widget sometimes ends up being bigger
# than the ReferenceImageViewer by one pixel, which distorts the # than the ReferenceImageViewer by one pixel, which distorts the
# scaled down pixmap for the reference, hence we'll reuse its size here. # scaled down pixmap for the reference, hence we'll reuse its size here.
selected_size = self._updateImage(
self.selectedPixmap, self.scaledSelectedPixmap,
self.selectedViewer, None, same_group)
self._updateImage( self._updateImage(
self.referencePixmap, self.scaledReferencePixmap, self.referencePixmap, self.scaledReferencePixmap,
self.referenceViewer, selected_size, same_group) self.referenceViewer, selected_size, same_group)
@ -192,12 +192,12 @@ class BaseController(QObject):
self.selectedViewer.ignore_signal = False self.selectedViewer.ignore_signal = False
def _updateImage(self, pixmap, scaledpixmap, viewer, target_size=None, same_group=False): def _updateImage(self, pixmap, scaledpixmap, viewer, target_size=None, same_group=False):
# FIXME this is called on every resize event, split into a separate function # WARNING this is called on every resize event, might need to split
# into a separate function depending on the implementation used
if pixmap.isNull(): if pixmap.isNull():
# disable the blank widget. # This should disable the blank widget
viewer.setImage(pixmap) viewer.setImage(pixmap)
return return
target_size = viewer.size() target_size = viewer.size()
if not viewer.bestFit: if not viewer.bestFit:
if same_group: if same_group:
@ -225,20 +225,15 @@ class BaseController(QObject):
self.current_scale = 1.0 self.current_scale = 1.0
self.selectedViewer.current_scale = 1.0 self.selectedViewer.current_scale = 1.0
self.referenceViewer.current_scale = 1.0 self.referenceViewer.current_scale = 1.0
self.selectedViewer.resetCenter() self.selectedViewer.resetCenter()
self.referenceViewer.resetCenter() self.referenceViewer.resetCenter()
self.selectedViewer.scaleAt(1.0) self.selectedViewer.scaleAt(1.0)
self.referenceViewer.scaleAt(1.0) self.referenceViewer.scaleAt(1.0)
self.centerViews() self.centerViews()
#FIXME move buttons somwhere else
self.parent.verticalToolBar.buttonZoomIn.setEnabled(False) self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)
self.parent.verticalToolBar.buttonZoomOut.setEnabled(False) self.parent.verticalToolBar.buttonZoomOut.setEnabled(False)
self.parent.verticalToolBar.buttonBestFit.setEnabled(False) # active mode by default
self.parent.verticalToolBar.buttonNormalSize.setEnabled(True) self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)
self.parent.verticalToolBar.buttonBestFit.setEnabled(False) # active mode by default
def resetViewersState(self): def resetViewersState(self):
"""No item from the model, disable and clear everything.""" """No item from the model, disable and clear everything."""
@ -257,16 +252,15 @@ class BaseController(QObject):
self.referenceViewer.scaleAt(1.0) self.referenceViewer.scaleAt(1.0)
self.centerViews() self.centerViews()
#FIXME move buttons somwhere else self.parent.verticalToolBar.buttonImgSwap.setEnabled(False)
self.parent.verticalToolBar.buttonZoomIn.setEnabled(False) self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)
self.parent.verticalToolBar.buttonZoomOut.setEnabled(False) self.parent.verticalToolBar.buttonZoomOut.setEnabled(False)
self.parent.verticalToolBar.buttonBestFit.setEnabled(False) # active mode by default
self.parent.verticalToolBar.buttonImgSwap.setEnabled(False)
self.parent.verticalToolBar.buttonNormalSize.setEnabled(False) self.parent.verticalToolBar.buttonNormalSize.setEnabled(False)
self.parent.verticalToolBar.buttonBestFit.setEnabled(False) # active mode by default
self.selectedViewer.setImage(self.selectedPixmap) # null self.selectedViewer.setImage(self.selectedPixmap) # null
self.selectedViewer.setEnabled(False) self.selectedViewer.setEnabled(False)
self.referenceViewer.setImage(self.referencePixmap) # null self.referenceViewer.setImage(self.referencePixmap) # null
self.referenceViewer.setEnabled(False) self.referenceViewer.setEnabled(False)
@pyqtSlot() @pyqtSlot()
@ -296,8 +290,8 @@ class BaseController(QObject):
def updateButtons(self): def updateButtons(self):
self.parent.verticalToolBar.buttonZoomIn.setEnabled(self.current_scale < MAX_SCALE) self.parent.verticalToolBar.buttonZoomIn.setEnabled(self.current_scale < MAX_SCALE)
self.parent.verticalToolBar.buttonZoomOut.setEnabled(self.current_scale > MIN_SCALE) self.parent.verticalToolBar.buttonZoomOut.setEnabled(self.current_scale > MIN_SCALE)
self.parent.verticalToolBar.buttonBestFit.setEnabled(self.bestFit is False)
self.parent.verticalToolBar.buttonNormalSize.setEnabled(round(self.current_scale, 1) != 1.0) self.parent.verticalToolBar.buttonNormalSize.setEnabled(round(self.current_scale, 1) != 1.0)
self.parent.verticalToolBar.buttonBestFit.setEnabled(self.bestFit is False)
@pyqtSlot() @pyqtSlot()
def zoomBestFit(self): def zoomBestFit(self):
@ -313,14 +307,18 @@ class BaseController(QObject):
self.selectedViewer.resetCenter() self.selectedViewer.resetCenter()
self.referenceViewer.resetCenter() self.referenceViewer.resetCenter()
target_size = self._updateImage(self.selectedPixmap, self.scaledSelectedPixmap, self.selectedViewer, None, True) target_size = self._updateImage(
self._updateImage(self.referencePixmap, self.scaledReferencePixmap, self.referenceViewer, target_size, True) self.selectedPixmap, self.scaledSelectedPixmap,
self.selectedViewer, None, True)
self._updateImage(
self.referencePixmap, self.scaledReferencePixmap,
self.referenceViewer, target_size, True)
self.centerViews() self.centerViews()
self.parent.verticalToolBar.buttonBestFit.setEnabled(False)
self.parent.verticalToolBar.buttonZoomOut.setEnabled(False)
self.parent.verticalToolBar.buttonZoomIn.setEnabled(False) self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)
self.parent.verticalToolBar.buttonZoomOut.setEnabled(False)
self.parent.verticalToolBar.buttonNormalSize.setEnabled(True) self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)
self.parent.verticalToolBar.buttonBestFit.setEnabled(False)
def setBestFit(self, value): def setBestFit(self, value):
self.bestFit = value self.bestFit = value
@ -340,9 +338,9 @@ class BaseController(QObject):
self.selectedViewer.scaleToNormalSize() self.selectedViewer.scaleToNormalSize()
self.referenceViewer.scaleToNormalSize() self.referenceViewer.scaleToNormalSize()
self.parent.verticalToolBar.buttonNormalSize.setEnabled(False)
self.parent.verticalToolBar.buttonZoomIn.setEnabled(True) self.parent.verticalToolBar.buttonZoomIn.setEnabled(True)
self.parent.verticalToolBar.buttonZoomOut.setEnabled(True) self.parent.verticalToolBar.buttonZoomOut.setEnabled(True)
self.parent.verticalToolBar.buttonNormalSize.setEnabled(False)
self.parent.verticalToolBar.buttonBestFit.setEnabled(True) self.parent.verticalToolBar.buttonBestFit.setEnabled(True)
def centerViews(self, only_selected=False): def centerViews(self, only_selected=False):
@ -371,7 +369,7 @@ class QWidgetController(BaseController):
@pyqtSlot() @pyqtSlot()
def swapImages(self): def swapImages(self):
self.selectedViewer.getPixmap().swap(self.referenceViewer.getPixmap()) self.selectedViewer._pixmap.swap(self.referenceViewer._pixmap)
self.selectedViewer.centerViewAndUpdate() self.selectedViewer.centerViewAndUpdate()
self.referenceViewer.centerViewAndUpdate() self.referenceViewer.centerViewAndUpdate()
super().swapImages() super().swapImages()
@ -443,18 +441,16 @@ class ScrollAreaController(BaseController):
def scaleImagesBy(self, factor): def scaleImagesBy(self, factor):
super().scaleImagesBy(factor) super().scaleImagesBy(factor)
# The other is automatically updated via sigals # The other is automatically updated via sigals
# self.selectedViewer.adjustScrollBarsFactor(factor) self.selectedViewer.adjustScrollBarsFactor(factor)
@pyqtSlot() @pyqtSlot()
def zoomBestFit(self): def zoomBestFit(self):
# Disable scrollbars to avoid GridLayout size rounding "error" # Disable scrollbars to avoid GridLayout size rounding glitch
super().zoomBestFit() super().zoomBestFit()
print("toggling scrollbars")
self.selectedViewer.toggleScrollBars() self.selectedViewer.toggleScrollBars()
self.referenceViewer.toggleScrollBars() self.referenceViewer.toggleScrollBars()
class GraphicsViewController(BaseController): class GraphicsViewController(BaseController):
"""Specialized version fro QGraphicsView-based viewers.""" """Specialized version fro QGraphicsView-based viewers."""
def __init__(self, parent): def __init__(self, parent):
@ -464,27 +460,26 @@ class GraphicsViewController(BaseController):
super()._setupConnections() super()._setupConnections()
self.selectedViewer.connectScrollBars() self.selectedViewer.connectScrollBars()
self.referenceViewer.connectScrollBars() self.referenceViewer.connectScrollBars()
# Special case for mouse wheel event conflicting with scrollbar adjustments
self.selectedViewer.other_viewer = self.referenceViewer
self.referenceViewer.other_viewer = self.selectedViewer
@pyqtSlot() @pyqtSlot()
def syncCenters(self): def syncCenters(self):
if self.sender() is self.referenceViewer: if self.sender() is self.referenceViewer:
self.selectedViewer.setCenter(self.referenceViewer.getCenter()) self.selectedViewer.setCenter(self.referenceViewer._centerPoint)
else: else:
self.referenceViewer.setCenter(self.selectedViewer.getCenter()) self.referenceViewer.setCenter(self.selectedViewer._centerPoint)
@pyqtSlot(float, QPointF) @pyqtSlot(float, QPointF)
def onMouseWheel(self, factor, newCenter): def onMouseWheel(self, factor, newCenter):
self.current_scale *= factor self.current_scale *= factor
if self.sender() is self.referenceViewer: if self.sender() is self.referenceViewer:
self.selectedViewer.scaleBy(factor) self.selectedViewer.scaleBy(factor)
self.selectedViewer.setCenter(self.referenceViewer.getCenter()) self.selectedViewer.setCenter(newCenter)
else: else:
self.referenceViewer.scaleBy(factor) self.referenceViewer.scaleBy(factor)
self.referenceViewer.setCenter(self.selectedViewer.getCenter()) self.referenceViewer.setCenter(newCenter)
# self.selectedViewer.adjustScrollBarsScaled(delta)
# Signal from scrollbars will automatically change the other:
# self.referenceViewer.adjustScrollBarsScaled(delta)
@pyqtSlot(int) @pyqtSlot(int)
def onVScrollBarChanged(self, value): def onVScrollBarChanged(self, value):
@ -516,10 +511,8 @@ class GraphicsViewController(BaseController):
"""Setup before scaling to bestfit""" """Setup before scaling to bestfit"""
self.setBestFit(True) self.setBestFit(True)
self.current_scale = 1.0 self.current_scale = 1.0
self.selectedViewer.fitScale() self.selectedViewer.fitScale()
self.referenceViewer.fitScale() self.referenceViewer.fitScale()
self.parent.verticalToolBar.buttonBestFit.setEnabled(False) self.parent.verticalToolBar.buttonBestFit.setEnabled(False)
self.parent.verticalToolBar.buttonZoomOut.setEnabled(False) self.parent.verticalToolBar.buttonZoomOut.setEnabled(False)
self.parent.verticalToolBar.buttonZoomIn.setEnabled(False) self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)
@ -531,11 +524,10 @@ class GraphicsViewController(BaseController):
if group != self.cached_group: if group != self.cached_group:
same_group = False same_group = False
self.resetState() self.resetState()
self.cached_group = group self.cached_group = group
self.selectedPixmap = QPixmap(str(dupe.path)) self.selectedPixmap = QPixmap(str(dupe.path))
if ref is dupe: # currently selected file is the actual reference file if ref is dupe: # currently selected file is the actual reference file
self.referencePixmap = QPixmap() self.referencePixmap = QPixmap()
self.parent.verticalToolBar.buttonImgSwap.setEnabled(False) self.parent.verticalToolBar.buttonImgSwap.setEnabled(False)
self.parent.verticalToolBar.buttonNormalSize.setEnabled(True) self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)
@ -547,7 +539,6 @@ class GraphicsViewController(BaseController):
self.selectedViewer.setImage(self.selectedPixmap) self.selectedViewer.setImage(self.selectedPixmap)
self.referenceViewer.setImage(self.referencePixmap) self.referenceViewer.setImage(self.referencePixmap)
self.updateBothImages(same_group) self.updateBothImages(same_group)
self.centerViews(same_group and self.referencePixmap.isNull())
def updateBothImages(self, same_group=False): def updateBothImages(self, same_group=False):
"""This is called only during resize events and while bestFit.""" """This is called only during resize events and while bestFit."""
@ -585,13 +576,10 @@ class GraphicsViewController(BaseController):
self.selectedViewer.fitScale() self.selectedViewer.fitScale()
self.referenceViewer.fitScale() self.referenceViewer.fitScale()
# self.centerViews() # self.centerViews()
#FIXME move buttons somwhere else
self.parent.verticalToolBar.buttonZoomIn.setEnabled(False) self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)
self.parent.verticalToolBar.buttonZoomOut.setEnabled(False) self.parent.verticalToolBar.buttonZoomOut.setEnabled(False)
self.parent.verticalToolBar.buttonBestFit.setEnabled(False) # active mode by default self.parent.verticalToolBar.buttonBestFit.setEnabled(False)
self.parent.verticalToolBar.buttonNormalSize.setEnabled(True) self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)
def resetViewersState(self): def resetViewersState(self):
@ -608,17 +596,15 @@ class GraphicsViewController(BaseController):
self.selectedViewer.resetCenter() self.selectedViewer.resetCenter()
self.referenceViewer.resetCenter() self.referenceViewer.resetCenter()
# self.centerViews() # self.centerViews()
#FIXME move buttons somwhere else
self.parent.verticalToolBar.buttonZoomIn.setEnabled(False) self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)
self.parent.verticalToolBar.buttonZoomOut.setEnabled(False) self.parent.verticalToolBar.buttonZoomOut.setEnabled(False)
self.parent.verticalToolBar.buttonBestFit.setEnabled(False) # active mode by default self.parent.verticalToolBar.buttonBestFit.setEnabled(False)
self.parent.verticalToolBar.buttonImgSwap.setEnabled(False) self.parent.verticalToolBar.buttonImgSwap.setEnabled(False)
self.parent.verticalToolBar.buttonNormalSize.setEnabled(False) self.parent.verticalToolBar.buttonNormalSize.setEnabled(False)
self.selectedViewer.setImage(self.selectedPixmap) # null self.selectedViewer.setImage(self.selectedPixmap) # null
self.selectedViewer.setEnabled(False) self.selectedViewer.setEnabled(False)
self.referenceViewer.setImage(self.referencePixmap) # null self.referenceViewer.setImage(self.referencePixmap) # null
self.referenceViewer.setEnabled(False) self.referenceViewer.setEnabled(False)
@pyqtSlot(float) @pyqtSlot(float)
@ -626,24 +612,13 @@ class GraphicsViewController(BaseController):
self.selectedViewer.updateCenterPoint() self.selectedViewer.updateCenterPoint()
self.referenceViewer.updateCenterPoint() self.referenceViewer.updateCenterPoint()
super().scaleImagesBy(factor) super().scaleImagesBy(factor)
# self.selectedViewer.setNewCenter(self.selectedViewer._scene.sceneRect().center())
# self.selectedViewer._centerPoint = self.selectedViewer.viewport().rect().center()
# self.referenceViewer._mousePanningDelta = self.selectedViewer._mousePanningDelta
# # self.selectedViewer._mousePanningDelta = self.referenceViewer._mousePanningDelta
# self.selectedViewer.adjustScrollBarsAuto()
# self.referenceViewer.adjustScrollBarsAuto()
self.selectedViewer.centerOn(self.selectedViewer._centerPoint) self.selectedViewer.centerOn(self.selectedViewer._centerPoint)
# Scrollbars sync themselves here
class QWidgetImageViewer(QWidget): class QWidgetImageViewer(QWidget):
"""Use a QPixmap, but no scrollbars.""" """Use a QPixmap, but no scrollbars and no keyboard key sequence for navigation."""
#FIXME: panning while zoomed-in is broken (due to delta not interpolated right?) # FIXME: panning while zoomed-in is broken (due to delta not interpolated right?
#TODO: keyboard shortcuts for navigation
mouseDragged = pyqtSignal(QPointF) mouseDragged = pyqtSignal(QPointF)
mouseWheeled = pyqtSignal(float) mouseWheeled = pyqtSignal(float)
@ -666,16 +641,13 @@ class QWidgetImageViewer(QWidget):
def __repr__(self): def __repr__(self):
return f'{self._instance_name}' return f'{self._instance_name}'
def getPixmap(self):
return self._pixmap
def connectMouseSignals(self): def connectMouseSignals(self):
if not self._dragConnection: if not self._dragConnection:
self._dragConnection = self.mouseDragged.connect( self._dragConnection = self.mouseDragged.connect(
self.controller.onDraggedMouse) self.controller.onDraggedMouse)
if not self._wheelConnection: if not self._wheelConnection:
self._wheelConnection = self.mouseWheeled.connect( self._wheelConnection = self.mouseWheeled.connect(
self.controller.scaleImagesBy) self.controller.scaleImagesBy)
def disconnectMouseSignals(self): def disconnectMouseSignals(self):
if self._dragConnection: if self._dragConnection:
@ -700,7 +672,6 @@ class QWidgetImageViewer(QWidget):
def changeEvent(self, event): def changeEvent(self, event):
if event.type() == QEvent.EnabledChange: if event.type() == QEvent.EnabledChange:
# print(f"{self} is now {'enabled' if self.isEnabled() else 'disabled'}")
if self.isEnabled(): if self.isEnabled():
self.connectMouseSignals() self.connectMouseSignals()
return return
@ -727,8 +698,8 @@ class QWidgetImageViewer(QWidget):
event.ignore() event.ignore()
return return
self._mousePanningDelta += (event.pos() - self._lastMouseClickPoint) \ self._mousePanningDelta += (
* 1.0 / self.current_scale event.pos() - self._lastMouseClickPoint) * 1.0 / self.current_scale
self._lastMouseClickPoint = event.pos() self._lastMouseClickPoint = event.pos()
if self._drag: if self._drag:
self.mouseDragged.emit(self._mousePanningDelta) self.mouseDragged.emit(self._mousePanningDelta)
@ -752,11 +723,11 @@ class QWidgetImageViewer(QWidget):
if event.angleDelta().y() > 0: if event.angleDelta().y() > 0:
if self.current_scale > MAX_SCALE: if self.current_scale > MAX_SCALE:
return return
self.mouseWheeled.emit(1.25) # zoom-in self.mouseWheeled.emit(1.25) # zoom-in
else: else:
if self.current_scale < MIN_SCALE: if self.current_scale < MIN_SCALE:
return return
self.mouseWheeled.emit(0.8) # zoom-out self.mouseWheeled.emit(0.8) # zoom-out
def setImage(self, pixmap): def setImage(self, pixmap):
if pixmap.isNull(): if pixmap.isNull():
@ -800,11 +771,10 @@ class QWidgetImageViewer(QWidget):
def onDraggedMouse(self, delta): def onDraggedMouse(self, delta):
self._mousePanningDelta = delta self._mousePanningDelta = delta
self.update() self.update()
# print(f"{self} received drag signal from {self.sender()}")
class ScalablePixmap(QWidget): class ScalablePixmap(QWidget):
"""Container for a pixmap that scales up very fast""" """Container for a pixmap that scales up very fast, used in ScrollAreaImageViewer."""
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)
self._pixmap = QPixmap() self._pixmap = QPixmap()
@ -812,30 +782,20 @@ class ScalablePixmap(QWidget):
def paintEvent(self, event): def paintEvent(self, event):
painter = QPainter(self) painter = QPainter(self)
# painter.translate(self.rect().center())
# painter.setRenderHint(QPainter.Antialiasing, False)
# scale the coordinate system:
painter.scale(self.current_scale, self.current_scale) painter.scale(self.current_scale, self.current_scale)
painter.drawPixmap(self.rect().topLeft(), self._pixmap) #same as (0,0, self.pixmap) # painter.drawPixmap(self.rect().topLeft(), self._pixmap)
# print(f"ScalableWidget paintEvent scale {self.current_scale}") # should be the same as:
painter.drawPixmap(0, 0, self._pixmap)
def setPixmap(self, pixmap):
self._pixmap = pixmap
# self.update()
def sizeHint(self): def sizeHint(self):
return self._pixmap.size() * self.current_scale return self._pixmap.size() * self.current_scale
# return self._pixmap.size()
def minimumSizeHint(self): def minimumSizeHint(self):
return self.sizeHint() return self.sizeHint()
# def moveEvent(self, event):
# print(f"{self} moved by {event.pos()}")
class ScrollAreaImageViewer(QScrollArea): class ScrollAreaImageViewer(QScrollArea):
"""Version with Qlabel for testing""" """Implementation using a pixmap container in a simple scroll area."""
mouseDragged = pyqtSignal(QPoint) mouseDragged = pyqtSignal(QPoint)
mouseWheeled = pyqtSignal(float, QPointF) mouseWheeled = pyqtSignal(float, QPointF)
@ -852,8 +812,6 @@ class ScrollAreaImageViewer(QScrollArea):
self._drag = False self._drag = False
self._dragConnection = None self._dragConnection = None
self._wheelConnection = None self._wheelConnection = None
self._vBarConnection = None
self._hBarConnection = None
self._instance_name = name self._instance_name = name
self.wantScrollBars = True self.wantScrollBars = True
self.bestFit = True self.bestFit = True
@ -861,35 +819,26 @@ class ScrollAreaImageViewer(QScrollArea):
self.label = ScalablePixmap(self) self.label = ScalablePixmap(self)
# This is to avoid sending signals twice on scrollbar updates # This is to avoid sending signals twice on scrollbar updates
self.ignore_signal = False self.ignore_signal = False
self.setBackgroundRole(QPalette.Dark) self.setBackgroundRole(QPalette.Dark)
self.setWidgetResizable(False) self.setWidgetResizable(False)
self.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents) self.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents)
# self.viewport().setAttribute(Qt.WA_StaticContents)
self.setAlignment(Qt.AlignCenter) self.setAlignment(Qt.AlignCenter)
self._verticalScrollBar = self.verticalScrollBar() self._verticalScrollBar = self.verticalScrollBar()
self._horizontalScrollBar = self.horizontalScrollBar() self._horizontalScrollBar = self.horizontalScrollBar()
if self.wantScrollBars: if self.wantScrollBars:
self.toggleScrollBars() self.toggleScrollBars()
else: else:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setWidget(self.label) self.setWidget(self.label)
self.setVisible(True) self.setVisible(True)
def __repr__(self): def __repr__(self):
return f'{self._instance_name}' return f'{self._instance_name}'
def getPixmap(self):
return self._pixmap
def toggleScrollBars(self, forceOn=False): def toggleScrollBars(self, forceOn=False):
if not self.wantScrollBars: if not self.wantScrollBars:
return return
# Ensure that it's off on the first run # Ensure that it's off on the first run
if self.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded: if self.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded:
if forceOn: if forceOn:
@ -919,7 +868,6 @@ class ScrollAreaImageViewer(QScrollArea):
def connectScrollBars(self): def connectScrollBars(self):
"""Only call once controller is connected.""" """Only call once controller is connected."""
# Cyclic connections are handled by Qt # Cyclic connections are handled by Qt
return
self._verticalScrollBar.valueChanged.connect( self._verticalScrollBar.valueChanged.connect(
self.controller.onVScrollBarChanged, Qt.UniqueConnection) self.controller.onVScrollBarChanged, Qt.UniqueConnection)
self._horizontalScrollBar.valueChanged.connect( self._horizontalScrollBar.valueChanged.connect(
@ -949,7 +897,6 @@ class ScrollAreaImageViewer(QScrollArea):
self._lastMouseClickPoint = event.pos() self._lastMouseClickPoint = event.pos()
self.mouseDragged.emit(delta) self.mouseDragged.emit(delta)
super().mouseMoveEvent(event) super().mouseMoveEvent(event)
# self.update()
def mouseReleaseEvent(self, event): def mouseReleaseEvent(self, event):
if self.bestFit: if self.bestFit:
@ -966,11 +913,11 @@ class ScrollAreaImageViewer(QScrollArea):
event.ignore() event.ignore()
return return
oldScale = self.current_scale oldScale = self.current_scale
if event.angleDelta().y() > 0: # zoom-in if event.angleDelta().y() > 0: # zoom-in
if oldScale < MAX_SCALE: if oldScale < MAX_SCALE:
self.current_scale *= 1.25 self.current_scale *= 1.25
else: else:
if oldScale > MIN_SCALE: # zoom-out if oldScale > MIN_SCALE: # zoom-out
self.current_scale *= 0.8 self.current_scale *= 0.8
if oldScale == self.current_scale: if oldScale == self.current_scale:
return return
@ -981,7 +928,7 @@ class ScrollAreaImageViewer(QScrollArea):
def setImage(self, pixmap): def setImage(self, pixmap):
self._pixmap = pixmap self._pixmap = pixmap
self.label.setPixmap(pixmap) self.label._pixmap = pixmap
self.label.update() self.label.update()
self.label.adjustSize() self.label.adjustSize()
if pixmap.isNull(): if pixmap.isNull():
@ -1000,7 +947,7 @@ class ScrollAreaImageViewer(QScrollArea):
def setCachedPixmap(self): def setCachedPixmap(self):
"""In case we have changed the cached pixmap, reset it.""" """In case we have changed the cached pixmap, reset it."""
self.label.setPixmap(self._pixmap) self.label._pixmap = self._pixmap
self.label.update() self.label.update()
def shouldBeActive(self): def shouldBeActive(self):
@ -1008,42 +955,10 @@ class ScrollAreaImageViewer(QScrollArea):
def scaleBy(self, factor): def scaleBy(self, factor):
self.current_scale *= factor self.current_scale *= factor
# print(f"scaleBy(factor={factor}) current_scale={self.current_scale}") # factor has to be either 1.25 or 0.8 here
# This kills my computer when scaling up! DO NOT USE!
# self._pixmap = self._pixmap.scaled(
# self._pixmap.size().__mul__(factor),
# Qt.KeepAspectRatio, Qt.FastTransformation)
# pointBeforeScale = QPoint(self.viewport().width() / 2,
# self.viewport().height() / 2)
pointBeforeScale = self.label.rect().center()
screenCenter = self.rect().center()
screenCenter.setX(screenCenter.x() + self.horizontalScrollBar().value())
screenCenter.setY(screenCenter.y() + self.verticalScrollBar().value())
# WARNING: factor has to be either 1.25 or 0.8 here!
self.label.resize(self.label.size().__imul__(factor)) self.label.resize(self.label.size().__imul__(factor))
# self.label.updateGeometry()
self.label.current_scale = self.current_scale self.label.current_scale = self.current_scale
self.label.update() self.label.update()
# Center view on zoom change(?) same as imageLabel->resize(imageLabel->pixmap()->size())
# self.label.adjustSize()
# pointAfterScale = QPoint(self.viewport().width() / 2,
# self.viewport().height() / 2)
pointAfterScale = self.label.rect().center()
# print(f"label.newsize: {self.label.size()}\npointAfter: {pointAfterScale}")
offset = pointBeforeScale - pointAfterScale
newCenter = screenCenter - offset #FIXME need factor here somewhere
# print(f"offset: {offset} newCenter: {newCenter}\n-----------------")
# self.centerOn(newCenter)
# self.adjustScrollBarCentered()
def scaleAt(self, scale): def scaleAt(self, scale):
self.current_scale = scale self.current_scale = scale
@ -1052,26 +967,15 @@ class ScrollAreaImageViewer(QScrollArea):
self.label.update() self.label.update()
# self.label.adjustSize() # self.label.adjustSize()
def centerOn(self, position):
# TODO here make widget move without the scrollbars if possible
self.ensureWidgetVisible(self.label) # moves back to center of label
# self.ensureVisible(position.x(), position.y())
# self.scrollContentsBy(position.x(), position.y())
# hvalue = self.horizontalScrollBar().value()
# vvalue = self.verticalScrollBar().value()
# topLeft = self.viewport().rect().topLeft()
# self.label.move(topLeft.x() - hvalue, topLeft.y() - vvalue)
# self.label.updateGeometry()
def adjustScrollBarsFactor(self, factor): def adjustScrollBarsFactor(self, factor):
"""After scaling, no mouse position, default to center.""" """After scaling, no mouse position, default to center."""
# scrollBar.setMaximum(scrollBar.maximum() - scrollBar.minimum() + scrollBar.pageStep()) # scrollBar.setMaximum(scrollBar.maximum() - scrollBar.minimum() + scrollBar.pageStep())
self._horizontalScrollBar.setValue(int(factor * self._horizontalScrollBar.value() + \ self._horizontalScrollBar.setValue(
((factor - 1) * self._horizontalScrollBar.pageStep()/2))) int(factor * self._horizontalScrollBar.value()
self._verticalScrollBar.setValue(int(factor * self._verticalScrollBar.value() + \ + ((factor - 1) * self._horizontalScrollBar.pageStep() / 2)))
((factor - 1) * self._verticalScrollBar.pageStep()/2))) self._verticalScrollBar.setValue(
int(factor * self._verticalScrollBar.value()
+ ((factor - 1) * self._verticalScrollBar.pageStep() / 2)))
def adjustScrollBarsScaled(self, delta): def adjustScrollBarsScaled(self, delta):
"""After scaling with the mouse, update relative to mouse position.""" """After scaling with the mouse, update relative to mouse position."""
@ -1098,48 +1002,36 @@ class ScrollAreaImageViewer(QScrollArea):
""" Resets origin """ """ Resets origin """
self._mousePanningDelta = QPoint() self._mousePanningDelta = QPoint()
self.current_scale = 1.0 self.current_scale = 1.0
# self.scaleBy(1.0) # self.scaleAt(1.0)
# self.label.update() # already called in scaleBy
def setCenter(self, point): def setCenter(self, point):
self._lastMouseClickPoint = point self._lastMouseClickPoint = point
def getCenter(self):
return self._lastMouseClickPoint
def sizeHint(self): def sizeHint(self):
return self.viewport().rect().size() return self.viewport().rect().size()
# def viewportSizeHint(self):
# return self.viewport().rect().size()
@pyqtSlot() @pyqtSlot()
def scaleToNormalSize(self): def scaleToNormalSize(self):
"""Called when the pixmap is set back to original size.""" """Called when the pixmap is set back to original size."""
self.scaleAt(1.0) self.scaleAt(1.0)
self.ensureWidgetVisible(self.label) # needed for centering self.ensureWidgetVisible(self.label) # needed for centering
# self.label.update()
self.toggleScrollBars(True) self.toggleScrollBars(True)
@pyqtSlot(QPoint) @pyqtSlot(QPoint)
def onDraggedMouse(self, delta): def onDraggedMouse(self, delta):
"""Update position from mouse delta sent by the other panel.""" """Update position from mouse delta sent by the other panel."""
self._mousePanningDelta = delta self._mousePanningDelta = delta
# self.label.move(self.label.pos() + delta)
# self.label.update()
# Signal from scrollbars had already synced the values here # Signal from scrollbars had already synced the values here
self.adjustScrollBarsAuto() self.adjustScrollBarsAuto()
# print(f"{self} onDraggedMouse slot with delta {delta}") # def viewportSizeHint(self):
# return self.viewport().rect().size()
def changeEvent(self, event): # def changeEvent(self, event):
if event.type() == QEvent.EnabledChange: # if event.type() == QEvent.EnabledChange:
print(f"{self} is now {'enabled' if self.isEnabled() else 'disabled'}") # print(f"{self} is now {'enabled' if self.isEnabled() else 'disabled'}")
from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
class GraphicsViewViewer(QGraphicsView): class GraphicsViewViewer(QGraphicsView):
"""Re-Implementation using a more full fledged class.""" """Re-Implementation using a more full fledged class."""
mouseDragged = pyqtSignal() mouseDragged = pyqtSignal()
@ -1167,7 +1059,7 @@ class GraphicsViewViewer(QGraphicsView):
self.controller = None self.controller = None
self._centerPoint = QPointF() self._centerPoint = QPointF()
self.centerOn(self._centerPoint) self.centerOn(self._centerPoint)
self.other_viewer = None
# specific to this class # specific to this class
self._scene = QGraphicsScene() self._scene = QGraphicsScene()
self._scene.setBackgroundBrush(Qt.black) self._scene.setBackgroundBrush(Qt.black)
@ -1175,7 +1067,7 @@ class GraphicsViewViewer(QGraphicsView):
self.setScene(self._scene) self.setScene(self._scene)
self._scene.addItem(self._item) self._scene.addItem(self._item)
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag) self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
self.matrix = QTransform() # self.matrix = QTransform()
self._horizontalScrollBar = self.horizontalScrollBar() self._horizontalScrollBar = self.horizontalScrollBar()
self._verticalScrollBar = self.verticalScrollBar() self._verticalScrollBar = self.verticalScrollBar()
self.ignore_signal = False self.ignore_signal = False
@ -1188,16 +1080,16 @@ class GraphicsViewViewer(QGraphicsView):
self.setResizeAnchor(QGraphicsView.AnchorViewCenter) self.setResizeAnchor(QGraphicsView.AnchorViewCenter)
self.setAlignment(Qt.AlignCenter) self.setAlignment(Qt.AlignCenter)
self.setViewportUpdateMode (QGraphicsView.FullViewportUpdate) self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
self.setMouseTracking(True) self.setMouseTracking(True)
def connectMouseSignals(self): def connectMouseSignals(self):
if not self._dragConnection: if not self._dragConnection:
self._dragConnection = self.mouseDragged.connect( self._dragConnection = self.mouseDragged.connect(
self.controller.syncCenters) self.controller.syncCenters)
if not self._wheelConnection: if not self._wheelConnection:
self._wheelConnection = self.mouseWheeled.connect( self._wheelConnection = self.mouseWheeled.connect(
self.controller.onMouseWheel) self.controller.onMouseWheel)
def disconnectMouseSignals(self): def disconnectMouseSignals(self):
if self._dragConnection: if self._dragConnection:
@ -1243,7 +1135,6 @@ class GraphicsViewViewer(QGraphicsView):
self.setMouseTracking(True) self.setMouseTracking(True)
# We need to propagate to scrollbars, so we send back up # We need to propagate to scrollbars, so we send back up
super().mousePressEvent(event) super().mousePressEvent(event)
# event.accept()
def mouseReleaseEvent(self, event): def mouseReleaseEvent(self, event):
if self.bestFit: if self.bestFit:
@ -1261,7 +1152,6 @@ class GraphicsViewViewer(QGraphicsView):
event.ignore() event.ignore()
return return
if self._drag: if self._drag:
delta = (event.pos() - self._lastMouseClickPoint)
self._lastMouseClickPoint = event.pos() self._lastMouseClickPoint = event.pos()
# We can simply rely on the scrollbar updating each other here # We can simply rely on the scrollbar updating each other here
# self.mouseDragged.emit() # self.mouseDragged.emit()
@ -1269,7 +1159,7 @@ class GraphicsViewViewer(QGraphicsView):
super().mouseMoveEvent(event) super().mouseMoveEvent(event)
def updateCenterPoint(self): def updateCenterPoint(self):
self._centerPoint = self.mapToScene( self.rect().center()) self._centerPoint = self.mapToScene(self.rect().center())
def wheelEvent(self, event): def wheelEvent(self, event):
if self.bestFit or MIN_SCALE > self.current_scale > MAX_SCALE: if self.bestFit or MIN_SCALE > self.current_scale > MAX_SCALE:
@ -1277,52 +1167,40 @@ class GraphicsViewViewer(QGraphicsView):
return return
pointBeforeScale = QPointF(self.mapToScene(self.mapFromGlobal(QCursor.pos()))) pointBeforeScale = QPointF(self.mapToScene(self.mapFromGlobal(QCursor.pos())))
# Get the original screen centerpoint # Get the original screen centerpoint
screenCenter = QPointF(self.mapToScene( self.rect().center() )) screenCenter = QPointF(self.mapToScene(self.rect().center()))
if event.angleDelta().y() > 0: if event.angleDelta().y() > 0:
factor = self.zoomInFactor factor = self.zoomInFactor
else: else:
factor = self.zoomOutFactor factor = self.zoomOutFactor
# Avoid scrollbars conflict:
self.other_viewer.ignore_signal = True
self.scaleBy(factor) self.scaleBy(factor)
pointAfterScale = QPointF(self.mapToScene( self.mapFromGlobal(QCursor.pos()))) pointAfterScale = QPointF(self.mapToScene(self.mapFromGlobal(QCursor.pos())))
#Get the offset of how the screen moved # Get the offset of how the screen moved
offset = pointBeforeScale - pointAfterScale offset = pointBeforeScale - pointAfterScale
#Adjust to the new center for correct zooming # Adjust to the new center for correct zooming
newCenter = screenCenter + offset newCenter = screenCenter + offset
self.setCenter(newCenter) self.setCenter(newCenter)
self.mouseWheeled.emit(factor, newCenter) self.mouseWheeled.emit(factor, newCenter)
self.other_viewer.ignore_signal = False
def setImage(self, pixmap): def setImage(self, pixmap):
self._pixmap = pixmap self._pixmap = pixmap
self._item.setPixmap(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) self.translate(1, 1)
# self._scene.setSceneRect(QRectF(self._pixmap.rect())) # not sure if this works
def centerViewAndUpdate(self): def centerViewAndUpdate(self):
# self._rect = self.sceneRect() # Called from the base controller for Normal Size
# self._rect.translate(-self._rect.center())
# self._item.update()
# self.viewport().update()
pass pass
def setCenter(self, point): def setCenter(self, point):
self._centerPoint = point self._centerPoint = point
self.centerOn(self._centerPoint) self.centerOn(self._centerPoint)
def getCenter(self):
return self._centerPoint
def resetCenter(self): def resetCenter(self):
""" Resets origin """ """ Resets origin """
self._mousePanningDelta = QPointF() self._mousePanningDelta = QPointF()
self.current_scale = 1.0 self.current_scale = 1.0
# self.update()
# self.setCenter(self._scene.sceneRect().center())
def setNewCenter(self, position): def setNewCenter(self, position):
self._centerPoint = position self._centerPoint = position
@ -1337,7 +1215,6 @@ class GraphicsViewViewer(QGraphicsView):
# self.current_scale = scale # self.current_scale = scale
if scale == 1.0: if scale == 1.0:
self.resetScale() self.resetScale()
# self.setTransform( QTransform() ) # self.setTransform( QTransform() )
self.scale(scale, scale) self.scale(scale, scale)
@ -1350,13 +1227,12 @@ class GraphicsViewViewer(QGraphicsView):
def resetScale(self): def resetScale(self):
# self.setTransform( QTransform() ) # self.setTransform( QTransform() )
self.resetTransform() # probably same as above self.resetTransform() # probably same as above
self.setCenter( self.scene().sceneRect().center() ) self.setCenter(self.scene().sceneRect().center())
# self.scaleChanged.emit( self.transform().m22() )
def fitScale(self): def fitScale(self):
self.bestFit = True self.bestFit = True
super().fitInView(self._scene.sceneRect(), Qt.KeepAspectRatio ) super().fitInView(self._scene.sceneRect(), Qt.KeepAspectRatio)
self.setNewCenter(self._scene.sceneRect().center()) self.setNewCenter(self._scene.sceneRect().center())
@pyqtSlot() @pyqtSlot()
@ -1377,20 +1253,18 @@ class GraphicsViewViewer(QGraphicsView):
def sizeHint(self): def sizeHint(self):
return self.viewport().rect().size() return self.viewport().rect().size()
# def viewportSizeHint(self):
# return self.viewport().rect().size()
def adjustScrollBarsFactor(self, factor): def adjustScrollBarsFactor(self, factor):
"""After scaling, no mouse position, default to center.""" """After scaling, no mouse position, default to center."""
# scrollBar.setMaximum(scrollBar.maximum() - scrollBar.minimum() + scrollBar.pageStep()) self._horizontalScrollBar.setValue(
self._horizontalScrollBar.setValue(int(factor * self._horizontalScrollBar.value() + \ int(factor * self._horizontalScrollBar.value()
((factor - 1) * self._horizontalScrollBar.pageStep()/2))) + ((factor - 1) * self._horizontalScrollBar.pageStep() / 2)))
self._verticalScrollBar.setValue(int(factor * self._verticalScrollBar.value() + \ self._verticalScrollBar.setValue(
((factor - 1) * self._verticalScrollBar.pageStep()/2))) int(factor * self._verticalScrollBar.value()
+ ((factor - 1) * self._verticalScrollBar.pageStep() / 2)))
def adjustScrollBarsAuto(self): def adjustScrollBarsAuto(self):
"""After panning, update accordingly.""" """After panning, update accordingly."""
self.horizontalScrollBar().setValue( self.horizontalScrollBar().setValue(
self.horizontalScrollBar().value() - self._mousePanningDelta.x()) self.horizontalScrollBar().value() - self._mousePanningDelta.x())
self.verticalScrollBar().setValue( self.verticalScrollBar().setValue(
self.verticalScrollBar().value() - self._mousePanningDelta.y()) self.verticalScrollBar().value() - self._mousePanningDelta.y())