diff --git a/CREDITS b/CREDITS
index 731c46e4..8e333f0e 100644
--- a/CREDITS
+++ b/CREDITS
@@ -1,6 +1,8 @@
To know who contributed to dupeGuru, you can look at the commit log, but not all contributions
result in a commit. This file lists contributors who don't necessarily appear in the commit log.
+* Jason Cho, Exchange icon
+* schollidesign (https://findicons.com/pack/1035/human_o2), Zoom-in, Zoom-out, Zoom-best-fit, Zoom-original icons
* Jérôme Cantin, Main icon
* Gregor Tätzner, German localization
* Frank Weber, German localization
diff --git a/core/app.py b/core/app.py
index 3f4c3266..fc36787b 100644
--- a/core/app.py
+++ b/core/app.py
@@ -595,6 +595,12 @@ class DupeGuru(Broadcaster):
self.exclude_list.load_from_xml(p)
self.exclude_list_dialog.refresh()
+ def load_directories(self, filepath):
+ # Clear out previous entries
+ self.directories.__init__()
+ self.directories.load_from_file(filepath)
+ self.notify("directories_changed")
+
def load_from(self, filename):
"""Start an async job to load results from ``filename``.
@@ -794,6 +800,16 @@ class DupeGuru(Broadcaster):
except OSError as e:
self.view.show_message(tr("Couldn't write to file: {}").format(str(e)))
+ def save_directories_as(self, filename):
+ """Save directories in ``filename``.
+
+ :param str filename: path of the file to save directories (as XML) to.
+ """
+ try:
+ self.directories.save_to_file(filename)
+ except OSError as e:
+ self.view.show_message(tr("Couldn't write to file: {}").format(str(e)))
+
def start_scanning(self):
"""Starts an async job to scan for duplicates.
diff --git a/images/exchange_purple_upscaled.png b/images/exchange_purple_upscaled.png
index 1f31231c..f351af13 100644
Binary files a/images/exchange_purple_upscaled.png and b/images/exchange_purple_upscaled.png differ
diff --git a/images/exchange_purple_waifu_s4_tta8.xcf b/images/exchange_purple_waifu_s4_tta8.xcf
index f3c922cb..89e3ecdf 100644
Binary files a/images/exchange_purple_waifu_s4_tta8.xcf and b/images/exchange_purple_waifu_s4_tta8.xcf differ
diff --git a/qt/directories_dialog.py b/qt/directories_dialog.py
index 82f2896d..351ee377 100644
--- a/qt/directories_dialog.py
+++ b/qt/directories_dialog.py
@@ -92,12 +92,14 @@ class DirectoriesDialog(QMainWindow):
self.app.showResultsWindow,
),
("actionAddFolder", "", "", tr("Add Folder..."), self.addFolderTriggered),
+ ("actionLoadDirectories", "", "", tr("Load Directories..."), self.loadDirectoriesTriggered),
+ ("actionSaveDirectories", "", "", tr("Save Directories..."), self.saveDirectoriesTriggered),
]
createActions(ACTIONS, self)
- # if self.app.use_tabs:
- # # Keep track of actions which should only be accessible from this class
- # for action, _, _, _, _ in ACTIONS:
- # self.specific_actions.add(getattr(self, action))
+ if self.app.use_tabs:
+ # Keep track of actions which should only be accessible from this window
+ self.specific_actions.add(self.actionLoadDirectories)
+ self.specific_actions.add(self.actionSaveDirectories)
def _setupMenu(self):
if not self.app.use_tabs:
@@ -127,6 +129,9 @@ class DirectoriesDialog(QMainWindow):
self.menuFile.addSeparator()
self.menuFile.addAction(self.app.actionClearPictureCache)
self.menuFile.addSeparator()
+ self.menuFile.addAction(self.actionLoadDirectories)
+ self.menuFile.addAction(self.actionSaveDirectories)
+ self.menuFile.addSeparator()
self.menuFile.addAction(self.app.actionQuit)
self.menuView.addAction(self.app.actionDirectoriesWindow)
@@ -328,9 +333,25 @@ class DirectoriesDialog(QMainWindow):
self.app.model.load_from(destination)
self.app.recentResults.insertItem(destination)
+ def loadDirectoriesTriggered(self):
+ title = tr("Select a directories file to load")
+ files = ";;".join([tr("dupeGuru Results (*.dupegurudirs)"), tr("All Files (*.*)")])
+ destination = QFileDialog.getOpenFileName(self, title, "", files)[0]
+ if destination:
+ self.app.model.load_directories(destination)
+
def removeFolderButtonClicked(self):
self.directoriesModel.model.remove_selected()
+ def saveDirectoriesTriggered(self):
+ title = tr("Select a file to save your directories to")
+ files = tr("dupeGuru Directories (*.dupegurudirs)")
+ destination, chosen_filter = QFileDialog.getSaveFileName(self, title, "", files)
+ if destination:
+ if not destination.endswith(".dupegurudirs"):
+ destination = "{}.dupegurudirs".format(destination)
+ self.app.model.save_directories_as(destination)
+
def scanButtonClicked(self):
if self.app.model.results.is_modified:
title = tr("Start a new scan")
diff --git a/qt/me/details_dialog.py b/qt/me/details_dialog.py
index 61c452e7..ff36c37b 100644
--- a/qt/me/details_dialog.py
+++ b/qt/me/details_dialog.py
@@ -19,14 +19,8 @@ class DetailsDialog(DetailsDialogBase):
self.setWindowTitle(tr("Details"))
self.resize(502, 295)
self.setMinimumSize(QSize(250, 250))
- # self.verticalLayout = QVBoxLayout(self)
- # self.verticalLayout.setSpacing(0)
- # self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.tableView = DetailsTable(self)
self.tableView.setAlternatingRowColors(True)
self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
self.tableView.setShowGrid(False)
- # self.verticalLayout.addWidget(self.tableView)
- # self.centralWidget = QWidget()
- # self.centralWidget.setLayout(self.verticalLayout)
self.setWidget(self.tableView)
diff --git a/qt/pe/details_dialog.py b/qt/pe/details_dialog.py
index 324a1307..7fe89aa0 100644
--- a/qt/pe/details_dialog.py
+++ b/qt/pe/details_dialog.py
@@ -128,11 +128,7 @@ class DetailsDialog(DetailsDialogBase):
# This works when expanding but it's ugly:
if self.selectedImageViewer.size().width() > self.referenceImageViewer.size().width():
- # print(f"""Before selected size: {self.selectedImageViewer.size()}\n""",
- # f"""Before reference size: {self.referenceImageViewer.size()}""")
self.selectedImageViewer.resize(self.referenceImageViewer.size())
- # print(f"""After selected size: {self.selectedImageViewer.size()}\n""",
- # f"""After reference size: {self.referenceImageViewer.size()}""")
# model --> view
def refresh(self):
diff --git a/qt/pe/image_viewer.py b/qt/pe/image_viewer.py
index 32719077..ef379361 100644
--- a/qt/pe/image_viewer.py
+++ b/qt/pe/image_viewer.py
@@ -758,11 +758,15 @@ class QWidgetImageViewer(QWidget):
return
self.disconnectMouseSignals()
+ def contextMenuEvent(self, event):
+ """Block parent's (main window) context menu on right click."""
+ event.accept()
+
def mousePressEvent(self, event):
if self.bestFit or not self.isEnabled():
event.ignore()
return
- if event.button() == Qt.LeftButton:
+ if event.button() & (Qt.LeftButton | Qt.MidButton | Qt.RightButton):
self._drag = True
else:
self._drag = False
@@ -790,8 +794,8 @@ class QWidgetImageViewer(QWidget):
if self.bestFit or not self.isEnabled():
event.ignore()
return
- if event.button() == Qt.LeftButton:
- self._drag = False
+ # if event.button() == Qt.LeftButton:
+ self._drag = False
self._app.restoreOverrideCursor()
self.setMouseTracking(False)
@@ -956,11 +960,18 @@ class ScrollAreaImageViewer(QScrollArea):
self._horizontalScrollBar.valueChanged.connect(
self.controller.onHScrollBarChanged, Qt.UniqueConnection)
+ def contextMenuEvent(self, event):
+ """Block parent's (main window) context menu on right click."""
+ # Even though we don't have a context menu right now, and the default
+ # contextMenuPolicy is DefaultContextMenu, we leverage that handler to
+ # avoid raising the Result window's Actions menu
+ event.accept()
+
def mousePressEvent(self, event):
if self.bestFit:
event.ignore()
return
- if event.button() == Qt.LeftButton:
+ if event.button() & (Qt.LeftButton | Qt.MidButton | Qt.RightButton):
self._drag = True
else:
self._drag = False
@@ -985,8 +996,7 @@ class ScrollAreaImageViewer(QScrollArea):
if self.bestFit:
event.ignore()
return
- if event.button() == Qt.LeftButton:
- self._drag = False
+ self._drag = False
self._app.restoreOverrideCursor()
self.setMouseTracking(False)
super().mouseReleaseEvent(event)
@@ -1203,11 +1213,15 @@ class GraphicsViewViewer(QGraphicsView):
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+ def contextMenuEvent(self, event):
+ """Block parent's (main window) context menu on right click."""
+ event.accept()
+
def mousePressEvent(self, event):
if self.bestFit:
event.ignore()
return
- if event.button() == Qt.LeftButton:
+ if event.button() & (Qt.LeftButton | Qt.MidButton | Qt.RightButton):
self._drag = True
else:
self._drag = False
@@ -1223,8 +1237,7 @@ class GraphicsViewViewer(QGraphicsView):
if self.bestFit:
event.ignore()
return
- if event.button() == Qt.LeftButton:
- self._drag = False
+ self._drag = False
self._app.restoreOverrideCursor()
self.setMouseTracking(False)
self.updateCenterPoint()
diff --git a/qt/pe/photo.py b/qt/pe/photo.py
index dcf71c41..a829f21e 100644
--- a/qt/pe/photo.py
+++ b/qt/pe/photo.py
@@ -29,6 +29,15 @@ class File(PhotoBase):
def _plat_get_blocks(self, block_count_per_side, orientation):
image = QImage(str(self.path))
image = image.convertToFormat(QImage.Format_RGB888)
+ if type(orientation) == str:
+ logging.warning("Orientation for file '%s' was a str '%s', not an int.",
+ str(self.path), orientation)
+ try:
+ orientation = int(orientation)
+ except Exception as e:
+ logging.exception("Skipping transformation because could not \
+convert str to int. %s", e)
+ return getblocks(image, block_count_per_side)
# MYSTERY TO SOLVE: For reasons I cannot explain, orientations 5 and 7 don't work for
# duplicate scanning. The transforms seems to work fine (if I try to save the image after
# the transform, we see that the image has been correctly flipped and rotated), but the
diff --git a/qt/preferences.py b/qt/preferences.py
index 210bb019..24027484 100644
--- a/qt/preferences.py
+++ b/qt/preferences.py
@@ -49,6 +49,8 @@ class Preferences(PreferencesBase):
self.result_table_ref_foreground_color =\
get("ResultTableRefForegroundColor", self.result_table_ref_foreground_color)
+ self.result_table_ref_background_color =\
+ get("ResultTableRefBackgroundColor", self.result_table_ref_background_color)
self.result_table_delta_foreground_color =\
get("ResultTableDeltaForegroundColor", self.result_table_delta_foreground_color)
@@ -100,6 +102,7 @@ class Preferences(PreferencesBase):
self.details_dialog_override_theme_icons = False if not ISLINUX else True
self.details_dialog_viewers_show_scrollbars = True
self.result_table_ref_foreground_color = QColor(Qt.blue)
+ self.result_table_ref_background_color = QColor(Qt.darkGray)
self.result_table_delta_foreground_color = QColor(255, 142, 40) # orange
self.resultWindowIsMaximized = False
self.resultWindowRect = None
@@ -143,6 +146,7 @@ class Preferences(PreferencesBase):
set_("DetailsDialogViewersShowScrollbars", self.details_dialog_viewers_show_scrollbars)
set_("DetailsTableDeltaForegroundColor", self.details_table_delta_foreground_color)
set_("ResultTableRefForegroundColor", self.result_table_ref_foreground_color)
+ set_("ResultTableRefBackgroundColor", self.result_table_ref_background_color)
set_("ResultTableDeltaForegroundColor", self.result_table_delta_foreground_color)
set_("ResultWindowIsMaximized", self.resultWindowIsMaximized)
set_("MainWindowIsMaximized", self.mainWindowIsMaximized)
diff --git a/qt/preferences_dialog.py b/qt/preferences_dialog.py
index 3897a77e..70eeae36 100644
--- a/qt/preferences_dialog.py
+++ b/qt/preferences_dialog.py
@@ -175,6 +175,9 @@ On MacOS, the tab bar will fill up the window's width instead."))
self.result_table_ref_foreground_color = ColorPickerButton(self)
formlayout.addRow(tr("Reference foreground color:"),
self.result_table_ref_foreground_color)
+ self.result_table_ref_background_color = ColorPickerButton(self)
+ gridlayout.addRow(tr("Reference background color:"),
+ self.result_table_ref_background_color)
self.result_table_delta_foreground_color = ColorPickerButton(self)
formlayout.addRow(tr("Delta foreground color:"),
self.result_table_delta_foreground_color)
@@ -291,6 +294,8 @@ use the modifier key to drag the floating window around") if ISLINUX else
prefs.details_table_delta_foreground_color)
self.result_table_ref_foreground_color.setColor(
prefs.result_table_ref_foreground_color)
+ self.result_table_ref_background_color.setColor(
+ prefs.result_table_ref_background_color)
self.result_table_delta_foreground_color.setColor(
prefs.result_table_delta_foreground_color)
try:
@@ -314,6 +319,7 @@ use the modifier key to drag the floating window around") if ISLINUX else
prefs.details_dialog_vertical_titlebar = ischecked(self.details_dialog_vertical_titlebar)
prefs.details_table_delta_foreground_color = self.details_table_delta_foreground_color.color
prefs.result_table_ref_foreground_color = self.result_table_ref_foreground_color.color
+ prefs.result_table_ref_background_color = self.result_table_ref_background_color.color
prefs.result_table_delta_foreground_color = self.result_table_delta_foreground_color.color
prefs.destination_type = self.copyMoveDestinationComboBox.currentIndex()
prefs.custom_command = str(self.customCommandEdit.text())
diff --git a/qt/results_model.py b/qt/results_model.py
index ecc0ee59..1a6fa326 100644
--- a/qt/results_model.py
+++ b/qt/results_model.py
@@ -29,6 +29,8 @@ class ResultsModel(Table):
def _getData(self, row, column, role):
if column.name == "marked":
+ if role == Qt.BackgroundRole and row.isref:
+ return QBrush(self.prefs.result_table_ref_background_color)
if role == Qt.CheckStateRole and row.markable:
return Qt.Checked if row.marked else Qt.Unchecked
return None
@@ -40,6 +42,9 @@ class ResultsModel(Table):
return QBrush(self.prefs.result_table_ref_foreground_color)
elif row.is_cell_delta(column.name):
return QBrush(self.prefs.result_table_delta_foreground_color)
+ elif role == Qt.BackgroundRole:
+ if row.isref:
+ return QBrush(self.prefs.result_table_ref_background_color)
elif role == Qt.FontRole:
font = QFont(self.view.font())
if self.prefs.reference_bold_font:
diff --git a/qt/se/details_dialog.py b/qt/se/details_dialog.py
index 8b910bc3..c30c4b90 100644
--- a/qt/se/details_dialog.py
+++ b/qt/se/details_dialog.py
@@ -19,14 +19,8 @@ class DetailsDialog(DetailsDialogBase):
self.setWindowTitle(tr("Details"))
self.resize(502, 186)
self.setMinimumSize(QSize(200, 0))
- # self.verticalLayout = QVBoxLayout()
- # self.verticalLayout.setSpacing(0)
- # self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.tableView = DetailsTable(self)
self.tableView.setAlternatingRowColors(True)
self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
self.tableView.setShowGrid(False)
- # self.verticalLayout.addWidget(self.tableView)
- # self.centralWidget = QWidget()
- # self.centralWidget.setLayout(self.verticalLayout)
self.setWidget(self.tableView)
diff --git a/qtlib/about_box.py b/qtlib/about_box.py
index 99c3a059..7982cc6f 100644
--- a/qtlib/about_box.py
+++ b/qtlib/about_box.py
@@ -69,21 +69,6 @@ class AboutBox(QDialog):
self.verticalLayout.addWidget(self.label_3)
self.label_3.setText(tr("Licensed under GPLv3"))
self.label = QLabel(self)
- self.label_4 = QLabel(self)
- self.label_4.setWordWrap(True)
- self.label_4.setTextFormat(Qt.RichText)
- self.label_4.setOpenExternalLinks(True)
- self.label_4.setText(tr(
- """ icon
- made by Jason Cho (used with permission).
-
-
-
-
-
- icons made by schollidesign
- (licensed under GPL)."""))
- self.verticalLayout.addWidget(self.label_4)
font = QFont()
font.setWeight(75)
font.setBold(True)
diff --git a/qtlib/preferences.py b/qtlib/preferences.py
index 7e3ba864..3f33705a 100644
--- a/qtlib/preferences.py
+++ b/qtlib/preferences.py
@@ -121,8 +121,10 @@ class Preferences(QObject):
self._settings.setValue(name, normalize_for_serialization(value))
def saveGeometry(self, name, widget):
- # We save geometry under a 5-sized int array: first item is a flag for whether the widget
- # is maximized and the other 4 are (x, y, w, h).
+ # We save geometry under a 7-sized int array: first item is a flag
+ # for whether the widget is maximized, second item is a flag for whether
+ # the widget is docked, third item is a Qt::DockWidgetArea enum value,
+ # and the other 4 are (x, y, w, h).
m = 1 if widget.isMaximized() else 0
d = 1 if isinstance(widget, QDockWidget) and not widget.isFloating() else 0
area = widget.parent.dockWidgetArea(widget) if d else 0