Some type-hinting for various qt base objects

This commit is contained in:
Andrew Senetar 2022-06-30 22:52:54 -05:00
parent b9aabb8545
commit ade5d7f8c1
Signed by: arsenetar
GPG Key ID: C63300DCE48AB2F1
5 changed files with 136 additions and 123 deletions

View File

@ -15,7 +15,7 @@ tr = trget("ui")
class DetailsDialog(DetailsDialogBase): class DetailsDialog(DetailsDialogBase):
def _setupUi(self): def _setupUi(self) -> None:
self.setWindowTitle(tr("Details")) self.setWindowTitle(tr("Details"))
self.resize(502, 295) self.resize(502, 295)
self.setMinimumSize(QSize(250, 250)) self.setMinimumSize(QSize(250, 250))

View File

@ -4,27 +4,22 @@
# 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 typing import Callable
from PyQt5.QtCore import QSize from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget, QCheckBox
QVBoxLayout,
QHBoxLayout,
QLabel,
QSizePolicy,
QSpacerItem,
QWidget,
)
from hscommon.trans import trget from hscommon.trans import trget
from core.app import AppMode from core.app import AppMode
from core.scanner import ScanType from core.scanner import ScanType
from qt.preferences import Preferences
from qt.preferences_dialog import PreferencesDialogBase from qt.preferences_dialog import PreferencesDialogBase, Sections
tr = trget("ui") tr = trget("ui")
class PreferencesDialog(PreferencesDialogBase): class PreferencesDialog(PreferencesDialogBase):
def _setupPreferenceWidgets(self): def _setupPreferenceWidgets(self) -> None:
self._setupFilterHardnessBox() self._setupFilterHardnessBox()
self.widgetsVLayout.addLayout(self.filterHardnessHLayout) self.widgetsVLayout.addLayout(self.filterHardnessHLayout)
self.widget = QWidget(self) self.widget = QWidget(self)
@ -70,7 +65,7 @@ class PreferencesDialog(PreferencesDialogBase):
self.widgetsVLayout.addWidget(self.ignoreHardlinkMatches) self.widgetsVLayout.addWidget(self.ignoreHardlinkMatches)
self._setupBottomPart() self._setupBottomPart()
def _load(self, prefs, setchecked, section): def _load(self, prefs: Preferences, setchecked: Callable[[QCheckBox, bool], None], section: Sections) -> None:
setchecked(self.tagTrackBox, prefs.scan_tag_track) setchecked(self.tagTrackBox, prefs.scan_tag_track)
setchecked(self.tagArtistBox, prefs.scan_tag_artist) setchecked(self.tagArtistBox, prefs.scan_tag_artist)
setchecked(self.tagAlbumBox, prefs.scan_tag_album) setchecked(self.tagAlbumBox, prefs.scan_tag_album)
@ -99,7 +94,7 @@ class PreferencesDialog(PreferencesDialogBase):
self.tagGenreBox.setEnabled(tag_based) self.tagGenreBox.setEnabled(tag_based)
self.tagYearBox.setEnabled(tag_based) self.tagYearBox.setEnabled(tag_based)
def _save(self, prefs, ischecked): def _save(self, prefs: Preferences, ischecked: Callable[[QCheckBox], bool]) -> None:
prefs.scan_tag_track = ischecked(self.tagTrackBox) prefs.scan_tag_track = ischecked(self.tagTrackBox)
prefs.scan_tag_artist = ischecked(self.tagArtistBox) prefs.scan_tag_artist = ischecked(self.tagArtistBox)
prefs.scan_tag_album = ischecked(self.tagAlbumBox) prefs.scan_tag_album = ischecked(self.tagAlbumBox)

View File

@ -4,10 +4,12 @@
# 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, pyqtSignal, pyqtSlot from typing import Union
from PyQt5.QtWidgets import QAbstractItemView, QSizePolicy, QGridLayout, QSplitter, QFrame from PyQt5.QtCore import Qt, QSize, pyqtSignal
from PyQt5.QtWidgets import QWidget, QAbstractItemView, QSizePolicy, QGridLayout, QSplitter, QFrame
from PyQt5.QtGui import QResizeEvent from PyQt5.QtGui import QResizeEvent
from hscommon.trans import trget from hscommon.trans import trget
from qt import app
from qt.details_dialog import DetailsDialog as DetailsDialogBase from qt.details_dialog import DetailsDialog as DetailsDialogBase
from qt.details_table import DetailsTable from qt.details_table import DetailsTable
from qt.pe.image_viewer import ViewerToolBar, ScrollAreaImageViewer, ScrollAreaController from qt.pe.image_viewer import ViewerToolBar, ScrollAreaImageViewer, ScrollAreaController
@ -16,15 +18,15 @@ tr = trget("ui")
class DetailsDialog(DetailsDialogBase): class DetailsDialog(DetailsDialogBase):
def __init__(self, parent, app): def __init__(self, parent: QWidget, app: "app.DupeGuru") -> None:
self.vController = None self.vController: Union[ScrollAreaController, None] = None
super().__init__(parent, app) super().__init__(parent, app)
def _setupUi(self): def _setupUi(self) -> None:
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.splitter = QSplitter(Qt.Vertical) self.splitter = QSplitter(Qt.Orientation.Vertical)
self.topFrame = EmittingFrame() self.topFrame = EmittingFrame()
self.topFrame.setFrameShape(QFrame.StyledPanel) self.topFrame.setFrameShape(QFrame.StyledPanel)
self.horizontalLayout = QGridLayout() self.horizontalLayout = QGridLayout()
@ -47,8 +49,8 @@ class DetailsDialog(DetailsDialogBase):
self.vController = ScrollAreaController(self) self.vController = ScrollAreaController(self)
self.verticalToolBar = ViewerToolBar(self, self.vController) self.verticalToolBar = ViewerToolBar(self, self.vController)
self.verticalToolBar.setOrientation(Qt.Orientation(Qt.Vertical)) self.verticalToolBar.setOrientation(Qt.Orientation.Vertical)
self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignCenter) self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignmentFlag.AlignCenter)
self.referenceImageViewer = ScrollAreaImageViewer(self, "referenceImage") self.referenceImageViewer = ScrollAreaImageViewer(self, "referenceImage")
self.horizontalLayout.addWidget(self.referenceImageViewer, 0, 2, 3, 1) self.horizontalLayout.addWidget(self.referenceImageViewer, 0, 2, 3, 1)
@ -73,7 +75,7 @@ class DetailsDialog(DetailsDialogBase):
self.topFrame.resized.connect(self.resizeEvent) self.topFrame.resized.connect(self.resizeEvent)
def _update(self): def _update(self) -> None:
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:
@ -87,15 +89,14 @@ class DetailsDialog(DetailsDialogBase):
self.vController.updateView(ref, dupe, group) self.vController.updateView(ref, dupe, group)
# --- Override # --- Override
@pyqtSlot(QResizeEvent) def resizeEvent(self, event: QResizeEvent) -> None:
def resizeEvent(self, event):
self.ensure_same_sizes() self.ensure_same_sizes()
if self.vController is None or not self.vController.bestFit: if self.vController is None or not self.vController.bestFit:
return return
# Only update the scaled down pixmaps # Only update the scaled down pixmaps
self.vController.updateBothImages() self.vController.updateBothImages()
def show(self): def show(self) -> None:
# Give the splitter a maximum height to reach. This is assuming that # Give the splitter a maximum height to reach. This is assuming that
# all rows below their headers have the same height # all rows below their headers have the same height
self.tableView.setMaximumHeight( self.tableView.setMaximumHeight(
@ -108,7 +109,7 @@ class DetailsDialog(DetailsDialogBase):
self.ensure_same_sizes() self.ensure_same_sizes()
self._update() self._update()
def ensure_same_sizes(self): def ensure_same_sizes(self) -> None:
# HACK This ensures same size while shrinking. # HACK This ensures same size while shrinking.
# ReferenceViewer might be 1 pixel shorter in width # ReferenceViewer might be 1 pixel shorter in width
# due to the toolbar in the middle keeping the same width, # due to the toolbar in the middle keeping the same width,
@ -126,7 +127,7 @@ class DetailsDialog(DetailsDialogBase):
self.selectedImageViewer.resize(self.referenceImageViewer.size()) self.selectedImageViewer.resize(self.referenceImageViewer.size())
# model --> view # model --> view
def refresh(self): def refresh(self) -> None:
DetailsDialogBase.refresh(self) DetailsDialogBase.refresh(self)
if self.isVisible(): if self.isVisible():
self._update() self._update()
@ -137,5 +138,5 @@ class EmittingFrame(QFrame):
resized = pyqtSignal(QResizeEvent) resized = pyqtSignal(QResizeEvent)
def resizeEvent(self, event): def resizeEvent(self, event: QResizeEvent) -> None:
self.resized.emit(event) self.resized.emit(event)

View File

@ -2,6 +2,7 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
from typing import Union, cast
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, QIcon, QKeySequence from PyQt5.QtGui import QPixmap, QPainter, QPalette, QCursor, QIcon, QKeySequence
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
@ -17,6 +18,7 @@ from PyQt5.QtWidgets import (
QAbstractScrollArea, QAbstractScrollArea,
QStyle, QStyle,
) )
from qt.details_dialog import DetailsDialog
from hscommon.trans import trget from hscommon.trans import trget
from hscommon.plat import ISLINUX from hscommon.plat import ISLINUX
@ -26,7 +28,7 @@ MAX_SCALE = 12.0
MIN_SCALE = 0.1 MIN_SCALE = 0.1
def create_actions(actions, target): def create_actions(actions: list, target: QObject) -> None:
# actions are list of (name, shortcut, icon, desc, func) # actions are list of (name, shortcut, icon, desc, func)
for name, shortcut, icon, desc, func in actions: for name, shortcut, icon, desc, func in actions:
action = QAction(target) action = QAction(target)
@ -40,9 +42,9 @@ def create_actions(actions, target):
class ViewerToolBar(QToolBar): class ViewerToolBar(QToolBar):
def __init__(self, parent, controller): def __init__(self, parent: DetailsDialog, controller: "BaseController") -> None:
super().__init__(parent) super().__init__(parent)
self.parent = parent self.setParent(parent)
self.controller = controller self.controller = controller
self.setupActions(controller) self.setupActions(controller)
self.createButtons() self.createButtons()
@ -52,24 +54,21 @@ class ViewerToolBar(QToolBar):
self.buttonNormalSize.setEnabled(False) self.buttonNormalSize.setEnabled(False)
self.buttonBestFit.setEnabled(False) self.buttonBestFit.setEnabled(False)
def setupActions(self, controller): def setupActions(self, controller: "BaseController") -> None:
override_icons = cast(DetailsDialog, self.parent()).app.prefs.details_dialog_override_theme_icons
# actions are list of (name, shortcut, icon, desc, func) # actions are list of (name, shortcut, icon, desc, func)
ACTIONS = [ ACTIONS = [
( (
"actionZoomIn", "actionZoomIn",
QKeySequence.ZoomIn, QKeySequence.ZoomIn,
QIcon.fromTheme("zoom-in") QIcon.fromTheme("zoom-in") if ISLINUX and not override_icons else QIcon(QPixmap(":/" + "zoom_in")),
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
else QIcon(QPixmap(":/" + "zoom_in")),
tr("Increase zoom"), tr("Increase zoom"),
controller.zoomIn, controller.zoomIn,
), ),
( (
"actionZoomOut", "actionZoomOut",
QKeySequence.ZoomOut, QKeySequence.ZoomOut,
QIcon.fromTheme("zoom-out") QIcon.fromTheme("zoom-out") if ISLINUX and not override_icons else QIcon(QPixmap(":/" + "zoom_out")),
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
else QIcon(QPixmap(":/" + "zoom_out")),
tr("Decrease zoom"), tr("Decrease zoom"),
controller.zoomOut, controller.zoomOut,
), ),
@ -77,7 +76,7 @@ class ViewerToolBar(QToolBar):
"actionNormalSize", "actionNormalSize",
tr("Ctrl+/"), tr("Ctrl+/"),
QIcon.fromTheme("zoom-original") QIcon.fromTheme("zoom-original")
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons if ISLINUX and not override_icons
else QIcon(QPixmap(":/" + "zoom_original")), else QIcon(QPixmap(":/" + "zoom_original")),
tr("Normal size"), tr("Normal size"),
controller.zoomNormalSize, controller.zoomNormalSize,
@ -86,7 +85,7 @@ class ViewerToolBar(QToolBar):
"actionBestFit", "actionBestFit",
tr("Ctrl+*"), tr("Ctrl+*"),
QIcon.fromTheme("zoom-best-fit") QIcon.fromTheme("zoom-best-fit")
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons if ISLINUX and not override_icons
else QIcon(QPixmap(":/" + "zoom_best_fit")), else QIcon(QPixmap(":/" + "zoom_best_fit")),
tr("Best fit"), tr("Best fit"),
controller.zoomBestFit, controller.zoomBestFit,
@ -96,12 +95,12 @@ class ViewerToolBar(QToolBar):
# the popup menu work in the toolbar (if resized below minimum height) # the popup menu work in the toolbar (if resized below minimum height)
create_actions(ACTIONS, self) create_actions(ACTIONS, self)
def createButtons(self): def createButtons(self) -> None:
self.buttonImgSwap = QToolButton(self) self.buttonImgSwap = QToolButton(self)
self.buttonImgSwap.setToolButtonStyle(Qt.ToolButtonIconOnly) self.buttonImgSwap.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.buttonImgSwap.setIcon( self.buttonImgSwap.setIcon(
QIcon.fromTheme("view-refresh", self.style().standardIcon(QStyle.SP_BrowserReload)) QIcon.fromTheme("view-refresh", self.style().standardIcon(QStyle.StandardPixmap.SP_BrowserReload))
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons if ISLINUX and not cast(DetailsDialog, self.parent()).app.prefs.details_dialog_override_theme_icons
else QIcon(QPixmap(":/" + "exchange")) else QIcon(QPixmap(":/" + "exchange"))
) )
self.buttonImgSwap.setText("Swap images") self.buttonImgSwap.setText("Swap images")
@ -110,22 +109,22 @@ class ViewerToolBar(QToolBar):
self.buttonImgSwap.released.connect(self.controller.swapImages) self.buttonImgSwap.released.connect(self.controller.swapImages)
self.buttonZoomIn = QToolButton(self) self.buttonZoomIn = QToolButton(self)
self.buttonZoomIn.setToolButtonStyle(Qt.ToolButtonIconOnly) self.buttonZoomIn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.buttonZoomIn.setDefaultAction(self.actionZoomIn) self.buttonZoomIn.setDefaultAction(self.actionZoomIn)
self.buttonZoomIn.setEnabled(False) self.buttonZoomIn.setEnabled(False)
self.buttonZoomOut = QToolButton(self) self.buttonZoomOut = QToolButton(self)
self.buttonZoomOut.setToolButtonStyle(Qt.ToolButtonIconOnly) self.buttonZoomOut.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.buttonZoomOut.setDefaultAction(self.actionZoomOut) self.buttonZoomOut.setDefaultAction(self.actionZoomOut)
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.ToolButtonStyle.ToolButtonIconOnly)
self.buttonNormalSize.setDefaultAction(self.actionNormalSize) self.buttonNormalSize.setDefaultAction(self.actionNormalSize)
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.ToolButtonStyle.ToolButtonIconOnly)
self.buttonBestFit.setDefaultAction(self.actionBestFit) self.buttonBestFit.setDefaultAction(self.actionBestFit)
self.buttonBestFit.setEnabled(False) self.buttonBestFit.setEnabled(False)
@ -141,10 +140,10 @@ class BaseController(QObject):
Base proxy interface to keep image viewers synchronized. Base proxy interface to keep image viewers synchronized.
Relays function calls, keep tracks of things.""" Relays function calls, keep tracks of things."""
def __init__(self, parent): def __init__(self, parent: QObject) -> None:
super().__init__() super().__init__()
self.selectedViewer = None self.selectedViewer: Union[ScrollAreaImageViewer, None] = None
self.referenceViewer = None self.referenceViewer: Union[ScrollAreaImageViewer, None] = None
# cached pixmaps # cached pixmaps
self.selectedPixmap = QPixmap() self.selectedPixmap = QPixmap()
self.referencePixmap = QPixmap() self.referencePixmap = QPixmap()
@ -152,22 +151,24 @@ 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.parent = parent # To change buttons' states self.setParent(parent) # To change buttons' states
self.cached_group = None self.cached_group = None
self.same_dimensions = True self.same_dimensions = True
def setupViewers(self, selected_viewer, reference_viewer): def setupViewers(self, selected_viewer: "ScrollAreaImageViewer", reference_viewer: "ScrollAreaImageViewer") -> None:
self.selectedViewer = selected_viewer self.selectedViewer = selected_viewer
self.referenceViewer = reference_viewer self.referenceViewer = reference_viewer
self.selectedViewer.controller = self self.selectedViewer.controller = self
self.referenceViewer.controller = self self.referenceViewer.controller = self
self._setupConnections() self._setupConnections()
def _setupConnections(self): def _setupConnections(self) -> None:
self.selectedViewer.connectMouseSignals() if self.selectedViewer is not None:
self.referenceViewer.connectMouseSignals() self.selectedViewer.connectMouseSignals()
if self.referenceViewer is not None:
self.referenceViewer.connectMouseSignals()
def updateView(self, ref, dupe, group): def updateView(self, ref, dupe, group) -> None:
# To keep current scale accross dupes from the same group # To keep current scale accross dupes from the same group
previous_same_dimensions = self.same_dimensions previous_same_dimensions = self.same_dimensions
self.same_dimensions = True self.same_dimensions = True
@ -206,13 +207,15 @@ class BaseController(QObject):
if ignore_update: if ignore_update:
self.selectedViewer.ignore_signal = False self.selectedViewer.ignore_signal = False
def _updateImage(self, pixmap, viewer, same_group=False): def _updateImage(
self, pixmap: QPixmap, viewer: "ScrollAreaImageViewer", same_group: bool = False
) -> Union[QSize, None]:
# WARNING this is called on every resize event, might need to split # WARNING this is called on every resize event, might need to split
# into a separate function depending on the implementation used # into a separate function depending on the implementation used
if pixmap.isNull(): if pixmap.isNull():
# This should disable the blank widget # This should disable the blank widget
viewer.setImage(pixmap) viewer.setImage(pixmap)
return return None
target_size = viewer.size() target_size = viewer.size()
if not viewer.bestFit: if not viewer.bestFit:
if same_group: if same_group:
@ -220,14 +223,18 @@ class BaseController(QObject):
return target_size return target_size
# zoomed in state, expand # zoomed in state, expand
# only if not same_group, we need full update # only if not same_group, we need full update
scaledpixmap = pixmap.scaled(target_size, Qt.KeepAspectRatioByExpanding, Qt.FastTransformation) scaledpixmap = pixmap.scaled(
target_size, Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.FastTransformation
)
else: else:
# best fit, keep ratio always # best fit, keep ratio always
scaledpixmap = pixmap.scaled(target_size, Qt.KeepAspectRatio, Qt.FastTransformation) scaledpixmap = pixmap.scaled(
target_size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.FastTransformation
)
viewer.setImage(scaledpixmap) viewer.setImage(scaledpixmap)
return target_size return target_size
def resetState(self): def resetState(self) -> None:
"""Only called when the group of dupes has changed. We reset our """Only called when the group of dupes has changed. We reset our
controller internal state and buttons, center view on viewers.""" controller internal state and buttons, center view on viewers."""
self.selectedPixmap = QPixmap() self.selectedPixmap = QPixmap()
@ -248,7 +255,7 @@ class BaseController(QObject):
self.parent.verticalToolBar.buttonNormalSize.setEnabled(True) self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)
self.parent.verticalToolBar.buttonBestFit.setEnabled(False) # active mode by default self.parent.verticalToolBar.buttonBestFit.setEnabled(False) # active mode by default
def resetViewersState(self): def resetViewersState(self) -> None:
"""No item from the model, disable and clear everything.""" """No item from the model, disable and clear everything."""
# only called by the details dialog # only called by the details dialog
self.selectedPixmap = QPixmap() self.selectedPixmap = QPixmap()
@ -277,36 +284,40 @@ class BaseController(QObject):
self.referenceViewer.setEnabled(False) self.referenceViewer.setEnabled(False)
@pyqtSlot() @pyqtSlot()
def zoomIn(self): def zoomIn(self) -> None:
self.scaleImagesBy(1.25) self.scaleImagesBy(1.25)
@pyqtSlot() @pyqtSlot()
def zoomOut(self): def zoomOut(self) -> None:
self.scaleImagesBy(0.8) self.scaleImagesBy(0.8)
@pyqtSlot(float) @pyqtSlot(float)
def scaleImagesBy(self, factor): def scaleImagesBy(self, factor: float) -> None:
"""Compute new scale from factor and scale.""" """Compute new scale from factor and scale."""
self.current_scale *= factor self.current_scale *= factor
self.selectedViewer.scaleBy(factor) if self.selectedViewer is not None:
self.referenceViewer.scaleBy(factor) self.selectedViewer.scaleBy(factor)
if self.referenceViewer is not None:
self.referenceViewer.scaleBy(factor)
self.updateButtons() self.updateButtons()
@pyqtSlot(float) @pyqtSlot(float)
def scaleImagesAt(self, scale): def scaleImagesAt(self, scale: float) -> None:
"""Scale at a pre-computed scale.""" """Scale at a pre-computed scale."""
self.current_scale = scale self.current_scale = scale
self.selectedViewer.scaleAt(scale) if self.selectedViewer is not None:
self.referenceViewer.scaleAt(scale) self.selectedViewer.scaleAt(scale)
if self.referenceViewer is not None:
self.referenceViewer.scaleAt(scale)
self.updateButtons() self.updateButtons()
def updateButtons(self): def updateButtons(self) -> None:
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.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) self.parent.verticalToolBar.buttonBestFit.setEnabled(self.bestFit is False)
def updateButtonsAsPerDimensions(self, previous_same_dimensions): def updateButtonsAsPerDimensions(self, previous_same_dimensions: bool) -> None:
if not self.same_dimensions: if not self.same_dimensions:
self.parent.verticalToolBar.buttonZoomIn.setEnabled(False) self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)
self.parent.verticalToolBar.buttonZoomOut.setEnabled(False) self.parent.verticalToolBar.buttonZoomOut.setEnabled(False)
@ -323,7 +334,7 @@ class BaseController(QObject):
self.parent.verticalToolBar.buttonImgSwap.setEnabled(False) self.parent.verticalToolBar.buttonImgSwap.setEnabled(False)
@pyqtSlot() @pyqtSlot()
def zoomBestFit(self): def zoomBestFit(self) -> None:
"""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
@ -352,7 +363,7 @@ class BaseController(QObject):
self.referenceViewer.bestFit = value self.referenceViewer.bestFit = value
@pyqtSlot() @pyqtSlot()
def zoomNormalSize(self): def zoomNormalSize(self) -> None:
self.setBestFit(False) self.setBestFit(False)
self.current_scale = 1.0 self.current_scale = 1.0
@ -373,14 +384,14 @@ class BaseController(QObject):
self.parent.verticalToolBar.buttonNormalSize.setEnabled(False) 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: bool = False) -> None:
self.selectedViewer.centerViewAndUpdate() self.selectedViewer.centerViewAndUpdate()
if only_selected: if only_selected:
return return
self.referenceViewer.centerViewAndUpdate() self.referenceViewer.centerViewAndUpdate()
@pyqtSlot() @pyqtSlot()
def swapImages(self): def swapImages(self) -> None:
# swap the columns in the details table as well # swap the columns in the details table as well
self.parent.tableView.horizontalHeader().swapSections(0, 1) self.parent.tableView.horizontalHeader().swapSections(0, 1)
@ -388,17 +399,17 @@ class BaseController(QObject):
class QWidgetController(BaseController): class QWidgetController(BaseController):
"""Specialized version for QWidget-based viewers.""" """Specialized version for QWidget-based viewers."""
def __init__(self, parent): def __init__(self, parent: QObject) -> None:
super().__init__(parent) super().__init__(parent)
def _updateImage(self, *args): def _updateImage(self, *args) -> Union[QSize, None]:
ret = super()._updateImage(*args) ret = super()._updateImage(*args)
# Fix alignment when resizing window # Fix alignment when resizing window
self.centerViews() self.centerViews()
return ret return ret
@pyqtSlot(QPointF) @pyqtSlot(QPointF)
def onDraggedMouse(self, delta): def onDraggedMouse(self, delta) -> None:
if not self.same_dimensions: if not self.same_dimensions:
return return
if self.sender() is self.referenceViewer: if self.sender() is self.referenceViewer:
@ -407,7 +418,7 @@ class QWidgetController(BaseController):
self.referenceViewer.onDraggedMouse(delta) self.referenceViewer.onDraggedMouse(delta)
@pyqtSlot() @pyqtSlot()
def swapImages(self): def swapImages(self) -> None:
self.selectedViewer._pixmap.swap(self.referenceViewer._pixmap) self.selectedViewer._pixmap.swap(self.referenceViewer._pixmap)
self.selectedViewer.centerViewAndUpdate() self.selectedViewer.centerViewAndUpdate()
self.referenceViewer.centerViewAndUpdate() self.referenceViewer.centerViewAndUpdate()
@ -417,15 +428,15 @@ class QWidgetController(BaseController):
class ScrollAreaController(BaseController): class ScrollAreaController(BaseController):
"""Specialized version fro QLabel-based viewers.""" """Specialized version fro QLabel-based viewers."""
def __init__(self, parent): def __init__(self, parent: QObject) -> None:
super().__init__(parent) super().__init__(parent)
def _setupConnections(self): def _setupConnections(self) -> None:
super()._setupConnections() super()._setupConnections()
self.selectedViewer.connectScrollBars() self.selectedViewer.connectScrollBars()
self.referenceViewer.connectScrollBars() self.referenceViewer.connectScrollBars()
def updateBothImages(self, same_group=False): def updateBothImages(self, same_group: bool = False) -> None:
super().updateBothImages(same_group) super().updateBothImages(same_group)
if not self.referenceViewer.isEnabled(): if not self.referenceViewer.isEnabled():
return return
@ -433,7 +444,7 @@ class ScrollAreaController(BaseController):
self.referenceViewer._verticalScrollBar.setValue(self.selectedViewer._verticalScrollBar.value()) self.referenceViewer._verticalScrollBar.setValue(self.selectedViewer._verticalScrollBar.value())
@pyqtSlot(QPoint) @pyqtSlot(QPoint)
def onDraggedMouse(self, delta): def onDraggedMouse(self, delta) -> None:
self.selectedViewer.ignore_signal = True self.selectedViewer.ignore_signal = True
self.referenceViewer.ignore_signal = True self.referenceViewer.ignore_signal = True
@ -450,21 +461,21 @@ class ScrollAreaController(BaseController):
self.referenceViewer.ignore_signal = False self.referenceViewer.ignore_signal = False
@pyqtSlot() @pyqtSlot()
def swapImages(self): def swapImages(self) -> None:
self.referenceViewer._pixmap.swap(self.selectedViewer._pixmap) self.referenceViewer._pixmap.swap(self.selectedViewer._pixmap)
self.referenceViewer.setCachedPixmap() self.referenceViewer.setCachedPixmap()
self.selectedViewer.setCachedPixmap() self.selectedViewer.setCachedPixmap()
super().swapImages() super().swapImages()
@pyqtSlot(float, QPointF) @pyqtSlot(float, QPointF)
def onMouseWheel(self, scale, delta): def onMouseWheel(self, scale: float, delta: QPointF) -> None:
self.scaleImagesAt(scale) self.scaleImagesAt(scale)
self.selectedViewer.adjustScrollBarsScaled(delta) self.selectedViewer.adjustScrollBarsScaled(delta)
# Signal from scrollbars will automatically change the other: # Signal from scrollbars will automatically change the other:
# self.referenceViewer.adjustScrollBarsScaled(delta) # self.referenceViewer.adjustScrollBarsScaled(delta)
@pyqtSlot(int) @pyqtSlot(int)
def onVScrollBarChanged(self, value): def onVScrollBarChanged(self, value: int) -> None:
if not self.same_dimensions: if not self.same_dimensions:
return return
if self.sender() is self.referenceViewer._verticalScrollBar: if self.sender() is self.referenceViewer._verticalScrollBar:
@ -475,7 +486,7 @@ class ScrollAreaController(BaseController):
self.referenceViewer._verticalScrollBar.setValue(value) self.referenceViewer._verticalScrollBar.setValue(value)
@pyqtSlot(int) @pyqtSlot(int)
def onHScrollBarChanged(self, value): def onHScrollBarChanged(self, value: int) -> None:
if not self.same_dimensions: if not self.same_dimensions:
return return
if self.sender() is self.referenceViewer._horizontalScrollBar: if self.sender() is self.referenceViewer._horizontalScrollBar:
@ -486,13 +497,13 @@ class ScrollAreaController(BaseController):
self.referenceViewer._horizontalScrollBar.setValue(value) self.referenceViewer._horizontalScrollBar.setValue(value)
@pyqtSlot(float) @pyqtSlot(float)
def scaleImagesBy(self, factor): def scaleImagesBy(self, factor: float) -> None:
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) -> None:
# Disable scrollbars to avoid GridLayout size rounding glitch # Disable scrollbars to avoid GridLayout size rounding glitch
super().zoomBestFit() super().zoomBestFit()
if self.referencePixmap.isNull(): if self.referencePixmap.isNull():

View File

@ -4,6 +4,7 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html # http://www.gnu.org/licenses/gpl-3.0.html
from typing import Callable, Union, cast
from PyQt5.QtCore import Qt, QSize, pyqtSlot from PyQt5.QtCore import Qt, QSize, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QDialog, QDialog,
@ -28,11 +29,12 @@ from PyQt5.QtWidgets import (
QGroupBox, QGroupBox,
QFormLayout, QFormLayout,
) )
from PyQt5.QtGui import QPixmap, QIcon from PyQt5.QtGui import QPixmap, QIcon, QShowEvent
from hscommon import desktop, plat from hscommon import desktop, plat
from hscommon.trans import trget from hscommon.trans import trget
from hscommon.plat import ISLINUX from hscommon.plat import ISLINUX
from qt import app
from qt.util import horizontal_wrap, move_to_screen_center from qt.util import horizontal_wrap, move_to_screen_center
from qt.preferences import get_langnames from qt.preferences import get_langnames
from enum import Flag, auto from enum import Flag, auto
@ -52,8 +54,10 @@ class Sections(Flag):
class PreferencesDialogBase(QDialog): class PreferencesDialogBase(QDialog):
def __init__(self, parent, app, **kwargs): def __init__(self, parent: QWidget, app: "app.DupeGuru", **kwargs) -> None:
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint flags = Qt.WindowType(
Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowSystemMenuHint
)
super().__init__(parent, flags, **kwargs) super().__init__(parent, flags, **kwargs)
self.app = app self.app = app
self.supportedLanguages = dict(sorted(get_langnames().items(), key=lambda item: item[1])) self.supportedLanguages = dict(sorted(get_langnames().items(), key=lambda item: item[1]))
@ -65,7 +69,7 @@ class PreferencesDialogBase(QDialog):
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
def _setupFilterHardnessBox(self): def _setupFilterHardnessBox(self) -> None:
self.filterHardnessHLayout = QHBoxLayout() self.filterHardnessHLayout = QHBoxLayout()
self.filterHardnessLabel = QLabel(self) self.filterHardnessLabel = QLabel(self)
self.filterHardnessLabel.setText(tr("Filter Hardness:")) self.filterHardnessLabel.setText(tr("Filter Hardness:"))
@ -84,7 +88,7 @@ class PreferencesDialogBase(QDialog):
self.filterHardnessSlider.setMinimum(1) self.filterHardnessSlider.setMinimum(1)
self.filterHardnessSlider.setMaximum(100) self.filterHardnessSlider.setMaximum(100)
self.filterHardnessSlider.setTracking(True) self.filterHardnessSlider.setTracking(True)
self.filterHardnessSlider.setOrientation(Qt.Horizontal) self.filterHardnessSlider.setOrientation(Qt.Orientation.Horizontal)
self.filterHardnessHLayoutSub1.addWidget(self.filterHardnessSlider) self.filterHardnessHLayoutSub1.addWidget(self.filterHardnessSlider)
self.filterHardnessLabel = QLabel(self) self.filterHardnessLabel = QLabel(self)
self.filterHardnessLabel.setText("100") self.filterHardnessLabel.setText("100")
@ -104,7 +108,7 @@ class PreferencesDialogBase(QDialog):
self.filterHardnessVLayout.addLayout(self.filterHardnessHLayoutSub2) self.filterHardnessVLayout.addLayout(self.filterHardnessHLayoutSub2)
self.filterHardnessHLayout.addLayout(self.filterHardnessVLayout) self.filterHardnessHLayout.addLayout(self.filterHardnessVLayout)
def _setupBottomPart(self): def _setupBottomPart(self) -> None:
# The bottom part of the pref panel is always the same in all editions. # The bottom part of the pref panel is always the same in all editions.
self.copyMoveLabel = QLabel(self) self.copyMoveLabel = QLabel(self)
self.copyMoveLabel.setText(tr("Copy and Move:")) self.copyMoveLabel.setText(tr("Copy and Move:"))
@ -120,7 +124,7 @@ class PreferencesDialogBase(QDialog):
self.customCommandEdit = QLineEdit(self) self.customCommandEdit = QLineEdit(self)
self.widgetsVLayout.addWidget(self.customCommandEdit) self.widgetsVLayout.addWidget(self.customCommandEdit)
def _setupDisplayPage(self): def _setupDisplayPage(self) -> None:
self.ui_groupbox = QGroupBox("&" + tr("General Interface")) self.ui_groupbox = QGroupBox("&" + tr("General Interface"))
layout = QVBoxLayout() layout = QVBoxLayout()
self.languageLabel = QLabel(tr("Language:"), self) self.languageLabel = QLabel(tr("Language:"), self)
@ -171,7 +175,7 @@ On MacOS, the tab bar will fill up the window's width instead."
formlayout.addRow(tr("Reference background color:"), self.result_table_ref_background_color) formlayout.addRow(tr("Reference background color:"), self.result_table_ref_background_color)
self.result_table_delta_foreground_color = ColorPickerButton(self) self.result_table_delta_foreground_color = ColorPickerButton(self)
formlayout.addRow(tr("Delta foreground color:"), self.result_table_delta_foreground_color) formlayout.addRow(tr("Delta foreground color:"), self.result_table_delta_foreground_color)
formlayout.setLabelAlignment(Qt.AlignLeft) formlayout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft)
# Keep same vertical spacing as parent layout for consistency # Keep same vertical spacing as parent layout for consistency
formlayout.setVerticalSpacing(self.displayVLayout.spacing()) formlayout.setVerticalSpacing(self.displayVLayout.spacing())
@ -213,7 +217,7 @@ use the modifier key to drag the floating window around"
details_groupbox.setLayout(self.details_groupbox_layout) details_groupbox.setLayout(self.details_groupbox_layout)
self.displayVLayout.addWidget(details_groupbox) self.displayVLayout.addWidget(details_groupbox)
def _setupDebugPage(self): def _setupDebugPage(self) -> None:
self._setupAddCheckbox("debugModeBox", tr("Debug mode (restart required)")) self._setupAddCheckbox("debugModeBox", tr("Debug mode (restart required)"))
self._setupAddCheckbox("profile_scan_box", tr("Profile scan operation")) self._setupAddCheckbox("profile_scan_box", tr("Profile scan operation"))
self.profile_scan_box.setToolTip(tr("Profile the scan operation and save logs for optimization.")) self.profile_scan_box.setToolTip(tr("Profile the scan operation and save logs for optimization."))
@ -221,22 +225,22 @@ use the modifier key to drag the floating window around"
self.debugVLayout.addWidget(self.profile_scan_box) self.debugVLayout.addWidget(self.profile_scan_box)
self.debug_location_label = QLabel( self.debug_location_label = QLabel(
tr('Logs located in: <a href="{}">{}</a>').format(self.app.model.appdata, self.app.model.appdata), tr('Logs located in: <a href="{}">{}</a>').format(self.app.model.appdata, self.app.model.appdata),
wordWrap=True,
) )
self.debug_location_label.setWordWrap(True)
self.debugVLayout.addWidget(self.debug_location_label) self.debugVLayout.addWidget(self.debug_location_label)
def _setupAddCheckbox(self, name, label, parent=None): def _setupAddCheckbox(self, name: str, label: str, parent: Union[QWidget, None] = None) -> None:
if parent is None: if parent is None:
parent = self parent = self
cb = QCheckBox(parent) cb = QCheckBox(parent)
cb.setText(label) cb.setText(label)
setattr(self, name, cb) setattr(self, name, cb)
def _setupPreferenceWidgets(self): def _setupPreferenceWidgets(self) -> None:
# Edition-specific # Edition-specific
pass pass
def _setupUi(self): def _setupUi(self) -> None:
self.setWindowTitle(tr("Options")) self.setWindowTitle(tr("Options"))
self.setSizeGripEnabled(False) self.setSizeGripEnabled(False)
self.setModal(True) self.setModal(True)
@ -262,7 +266,7 @@ use the modifier key to drag the floating window around"
) )
self.mainVLayout.addWidget(self.tabwidget) self.mainVLayout.addWidget(self.tabwidget)
self.mainVLayout.addWidget(self.buttonBox) self.mainVLayout.addWidget(self.buttonBox)
self.layout().setSizeConstraint(QLayout.SetFixedSize) self.layout().setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
self.tabwidget.addTab(self.page_general, tr("General")) self.tabwidget.addTab(self.page_general, tr("General"))
self.tabwidget.addTab(self.page_display, tr("Display")) self.tabwidget.addTab(self.page_display, tr("Display"))
self.tabwidget.addTab(self.page_debug, tr("Debug")) self.tabwidget.addTab(self.page_debug, tr("Debug"))
@ -270,20 +274,20 @@ use the modifier key to drag the floating window around"
self.widgetsVLayout.addStretch(0) self.widgetsVLayout.addStretch(0)
self.debugVLayout.addStretch(0) self.debugVLayout.addStretch(0)
def _load(self, prefs, setchecked, section): def _load(self, prefs: Preferences, setchecked: Callable[[QCheckBox, bool], None], section: Sections) -> None:
# Edition-specific # Edition-specific
pass pass
def _save(self, prefs, ischecked): def _save(self, prefs: Preferences, ischecked: Callable[[QCheckBox], bool]) -> None:
# Edition-specific # Edition-specific
pass pass
def load(self, prefs=None, section=Sections.ALL): def load(self, prefs: Union[Preferences, None] = None, section: Sections = Sections.ALL) -> None:
if prefs is None: if prefs is None:
prefs = self.app.prefs prefs = self.app.prefs
def setchecked(cb, b): def setchecked(cb: QCheckBox, b: bool) -> None:
cb.setCheckState(Qt.Checked if b else Qt.Unchecked) cb.setCheckState(Qt.CheckState.Checked if b else Qt.CheckState.Unchecked)
if section & Sections.GENERAL: if section & Sections.GENERAL:
self.filterHardnessSlider.setValue(prefs.filter_hardness) self.filterHardnessSlider.setValue(prefs.filter_hardness)
@ -323,12 +327,12 @@ use the modifier key to drag the floating window around"
setchecked(self.profile_scan_box, prefs.profile_scan) setchecked(self.profile_scan_box, prefs.profile_scan)
self._load(prefs, setchecked, section) self._load(prefs, setchecked, section)
def save(self): def save(self) -> None:
prefs = self.app.prefs prefs = self.app.prefs
prefs.filter_hardness = self.filterHardnessSlider.value() prefs.filter_hardness = self.filterHardnessSlider.value()
def ischecked(cb): def ischecked(cb: QCheckBox) -> bool:
return cb.checkState() == Qt.Checked return cb.checkState() == Qt.CheckState.Checked
prefs.mix_file_kind = ischecked(self.mixFileKindBox) prefs.mix_file_kind = ischecked(self.mixFileKindBox)
prefs.use_regexp = ischecked(self.useRegexpBox) prefs.use_regexp = ischecked(self.useRegexpBox)
@ -363,11 +367,11 @@ use the modifier key to drag the floating window around"
self.app.prefs.language = lang_code self.app.prefs.language = lang_code
self._save(prefs, ischecked) self._save(prefs, ischecked)
def resetToDefaults(self, section_to_update): def resetToDefaults(self, section_to_update: Sections) -> None:
self.load(Preferences(), section_to_update) self.load(Preferences(), section_to_update)
# --- Events # --- Events
def buttonClicked(self, button): def buttonClicked(self, button: QDialogButtonBox) -> None:
role = self.buttonBox.buttonRole(button) role = self.buttonBox.buttonRole(button)
if role == QDialogButtonBox.ResetRole: if role == QDialogButtonBox.ResetRole:
current_tab = self.tabwidget.currentWidget() current_tab = self.tabwidget.currentWidget()
@ -380,30 +384,32 @@ use the modifier key to drag the floating window around"
section_to_update = Sections.DEBUG section_to_update = Sections.DEBUG
self.resetToDefaults(section_to_update) self.resetToDefaults(section_to_update)
def showEvent(self, event): def showEvent(self, event: QShowEvent) -> None:
# have to do this here as the frameGeometry is not correct until shown # have to do this here as the frameGeometry is not correct until shown
move_to_screen_center(self) move_to_screen_center(self)
super().showEvent(event) super().showEvent(event)
class ColorPickerButton(QPushButton): class ColorPickerButton(QPushButton):
def __init__(self, parent): def __init__(self, parent: QWidget) -> None:
super().__init__(parent) super().__init__(parent)
self.parent = parent self.setParent(parent)
self.color = None self.color = None
self.clicked.connect(self.onClicked) self.clicked.connect(self.onClicked)
@pyqtSlot() @pyqtSlot()
def onClicked(self): def onClicked(self) -> None:
color = QColorDialog.getColor(self.color if self.color is not None else Qt.white, self.parent) color = QColorDialog.getColor(
self.color if self.color is not None else Qt.GlobalColor.white, cast(QWidget, self.parent())
)
self.setColor(color) self.setColor(color)
def setColor(self, color): def setColor(self, color) -> None:
size = QSize(16, 16) size = QSize(16, 16)
px = QPixmap(size) px = QPixmap(size)
if color is None: if color is None:
size.width = 0 size.setWidth(0)
size.height = 0 size.setHeight(0)
elif not color.isValid(): elif not color.isValid():
return return
else: else: