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( - """Exchange icon - made by Jason Cho (used with permission). -
-Zoom In -Zoom Out -Zoomt Best Fit -Zoom Original - 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