diff --git a/.travis.yml b/.travis.yml index 7cde9668..e8d96966 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,11 +5,6 @@ install: script: tox matrix: include: - - os: "linux" - dist: "xenial" - python: "3.8" - script: - - tox -e style - os: "linux" dist: "xenial" python: "3.6" @@ -17,18 +12,16 @@ matrix: dist: "xenial" python: "3.7" - os: "linux" - dist: "xenial" + dist: "focal" python: "3.8" + - os: "linux" + dist: "focal" + python: "3.9" - os: "windows" language: shell - python: "3.7" - env: "PATH=/c/python37:/c/python37/Scripts:$PATH" + python: "3.8" + env: "PATH=/c/python38:/c/python38/Scripts:$PATH" before_install: - - choco install python --version=3.7.6 - - choco install make - - cp /c/python37/python.exe /c/python37/python3.exe - before_script: - - pip3 install -r requirements-windows.txt - - python3 build.py - script: - - tox -e WINDOWS + - choco install python --version=3.8.6 + - cp /c/python38/python.exe /c/python38/python3.exe + script: tox -e py38 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/Windows.md b/Windows.md index 339ec10d..66d2b391 100644 --- a/Windows.md +++ b/Windows.md @@ -21,7 +21,7 @@ To build with a different python version 3.5 vs 3.7 or 32 bit vs 64 bit specify $ cd $ py -3.7 -m venv .\env $ .\env\Scripts\activate - $ pip install -r requirements.txt -r requirements-windows.txt + $ pip install -r requirements.txt $ python build.py $ python run.py @@ -37,8 +37,6 @@ Then the following execution of the makefile should work. Pass the correct valu $ make PYTHON='py -3.7' $ make run -NOTE: Install PyQt5 & cx-Freeze with requirements-windows.txt into the venv before running the packaging scripts in the section below. - ### Generate Windows Installer Packages You need to use the respective x86 or x64 version of python to build the 32 bit and 64 bit versions. The build scripts will automatically detect the python architecture for you. When using build.py make sure the resulting python works before continuing to package.py. NOTE: package.py looks for the 'makensis' executable in the default location for a 64 bit windows system. Run the following in the respective virtual environment. diff --git a/build.py b/build.py index 425f8fc8..a583f959 100644 --- a/build.py +++ b/build.py @@ -54,6 +54,12 @@ def parse_args(): dest="normpo", help="Normalize all PO files (do this before commit).", ) + parser.add_option( + "--modules", + action="store_true", + dest="modules", + help="Build the python modules.", + ) (options, args) = parser.parse_args() return options @@ -182,6 +188,8 @@ def main(): build_mergepot() elif options.normpo: build_normpo() + elif options.modules: + build_pe_modules() else: build_normal() diff --git a/core/exclude.py b/core/exclude.py index eb8ffc08..29b00e6b 100644 --- a/core/exclude.py +++ b/core/exclude.py @@ -12,6 +12,7 @@ from os import sep import logging import functools from hscommon.util import FileOrPath +from hscommon.plat import ISWINDOWS import time default_regexes = [r"^thumbs\.db$", # Obsolete after WindowsXP @@ -19,10 +20,10 @@ default_regexes = [r"^thumbs\.db$", # Obsolete after WindowsXP r"^\.DS_Store$", # MacOS metadata r"^\.Trash\-.*", # Linux trash directories r"^\$Recycle\.Bin$", # Windows - r"^\..*", # Hidden files + r"^\..*", # Hidden files on Unix-like ] # These are too broad -forbidden_regexes = [r".*", r"\/.*", r".*\/.*", r".*\..*"] +forbidden_regexes = [r".*", r"\/.*", r".*\/.*", r".*\\\\.*", r".*\..*"] def timer(func): @@ -169,9 +170,9 @@ class ExcludeList(Markable): def build_compiled_caches(self, union=False): if not union: self._cached_compiled_files =\ - [x for x in self._excluded_compiled if sep not in x.pattern] + [x for x in self._excluded_compiled if not has_sep(x.pattern)] self._cached_compiled_paths =\ - [x for x in self._excluded_compiled if sep in x.pattern] + [x for x in self._excluded_compiled if has_sep(x.pattern)] return marked_count = [x for marked, x in self if marked] # If there is no item, the compiled Pattern will be '' and match everything! @@ -184,13 +185,13 @@ class ExcludeList(Markable): # the same regardless of whether the client asked for union or not self._cached_compiled_union_all =\ (re.compile('|'.join(marked_count)),) - files_marked = [x for x in marked_count if sep not in x] + files_marked = [x for x in marked_count if not has_sep(x)] if not files_marked: self._cached_compiled_union_files = tuple() else: self._cached_compiled_union_files =\ (re.compile('|'.join(files_marked)),) - paths_marked = [x for x in marked_count if sep in x] + paths_marked = [x for x in marked_count if has_sep(x)] if not paths_marked: self._cached_compiled_union_paths = tuple() else: @@ -488,3 +489,11 @@ def ordered_keys(_dict): list_of_items.sort(key=lambda x: x[1].get("index")) for item in list_of_items: yield item[0] + + +if ISWINDOWS: + def has_sep(x): + return '\\' + sep in x +else: + def has_sep(x): + return sep in x diff --git a/core/tests/directories_test.py b/core/tests/directories_test.py index 061e1476..8a5ddcdb 100644 --- a/core/tests/directories_test.py +++ b/core/tests/directories_test.py @@ -12,6 +12,7 @@ import shutil from pytest import raises from hscommon.path import Path from hscommon.testutil import eq_ +from hscommon.plat import ISWINDOWS from ..fs import File from ..directories import ( @@ -428,7 +429,10 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled assert "unwanted_subdirfile.gif" not in files assert "unwanted_subdarfile.png" not in files - regex3 = r".*Recycle\.Bin\/.*unwanted.*subdirfile.*" + if ISWINDOWS: + regex3 = r".*Recycle\.Bin\\.*unwanted.*subdirfile.*" + else: + regex3 = r".*Recycle\.Bin\/.*unwanted.*subdirfile.*" self.d._exclude_list.rename(regex2, regex3) assert self.d._exclude_list.error(regex3) is None # print(f"get_folders(): {[x for x in self.d.get_folders()]}") @@ -516,6 +520,8 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled self.d.set_state(p1["foobar"][".hidden_dir"], DirectoryState.Normal) # The files should still be filtered files = self.get_files_and_expect_num_result(1) + eq_(len(self.d._exclude_list.compiled_paths), 0) + eq_(len(self.d._exclude_list.compiled_files), 1) assert ".hidden_file.txt" not in files assert ".hidden_subfile.png" not in files assert "foobar.jpg" in files diff --git a/core/tests/exclude_test.py b/core/tests/exclude_test.py index 3745ac21..de5e46c5 100644 --- a/core/tests/exclude_test.py +++ b/core/tests/exclude_test.py @@ -11,6 +11,7 @@ from xml.etree import ElementTree as ET # from pytest import raises from hscommon.testutil import eq_ +from hscommon.plat import ISWINDOWS from .base import DupeGuru from ..exclude import ExcludeList, ExcludeDict, default_regexes, AlreadyThereException @@ -245,8 +246,11 @@ class TestCaseCompiledList(): assert expr.pattern in exprs def test_compiled_files(self): - # test is separator is indeed checked properly to yield the output - regex1 = r"test/one/sub" + # is path separator checked properly to yield the output + if ISWINDOWS: + regex1 = r"test\\one\\sub" + else: + regex1 = r"test/one/sub" self.e_separate.add(regex1) self.e_separate.mark(regex1) self.e_union.add(regex1) 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/pkg/arch/dupeguru.json b/pkg/arch/dupeguru.json index 0f83f7bd..9817da61 100644 --- a/pkg/arch/dupeguru.json +++ b/pkg/arch/dupeguru.json @@ -3,5 +3,5 @@ "longname": "dupeGuru", "execname": "dupeguru", "arch": "any", - "iconpath": "/usr/share/dupeguru/dgse_logo_128.png" + "iconpath": "dupeguru" } diff --git a/pkg/debian/Makefile b/pkg/debian/Makefile index 401c3bc9..5309f39e 100644 --- a/pkg/debian/Makefile +++ b/pkg/debian/Makefile @@ -8,4 +8,5 @@ all: chmod +x src/run.py cp -R src/ "$(CURDIR)/debian/{pkgname}/usr/share/{execname}" cp "$(CURDIR)/debian/{execname}.desktop" "$(CURDIR)/debian/{pkgname}/usr/share/applications" + ln -s "/usr/share/{execname}/dgse_logo_128.png" "$(CURDIR)/debian/{pkgname}/usr/pixmaps/{execname}.png" ln -s "/usr/share/{execname}/run.py" "$(CURDIR)/debian/{pkgname}/usr/bin/{execname}" diff --git a/pkg/debian/dupeguru.json b/pkg/debian/dupeguru.json index 0f83f7bd..9817da61 100644 --- a/pkg/debian/dupeguru.json +++ b/pkg/debian/dupeguru.json @@ -3,5 +3,5 @@ "longname": "dupeGuru", "execname": "dupeguru", "arch": "any", - "iconpath": "/usr/share/dupeguru/dgse_logo_128.png" + "iconpath": "dupeguru" } diff --git a/pkg/debian/source/format b/pkg/debian/source/format index 89ae9db8..9f674278 100644 --- a/pkg/debian/source/format +++ b/pkg/debian/source/format @@ -1 +1 @@ -3.0 (native) +3.0 (native) \ No newline at end of file diff --git a/qt/exclude_list_dialog.py b/qt/exclude_list_dialog.py index 3c23b83f..425d4eba 100644 --- a/qt/exclude_list_dialog.py +++ b/qt/exclude_list_dialog.py @@ -80,9 +80,9 @@ class ExcludeListDialog(QDialog): gridlayout.addWidget(self.testLine, 6, 0) layout.addLayout(gridlayout) - self.inputLine.setPlaceholderText("Type a python regular expression here...") + self.inputLine.setPlaceholderText(tr("Type a python regular expression here...")) self.inputLine.setFocus() - self.testLine.setPlaceholderText("Type a file system path or filename here...") + self.testLine.setPlaceholderText(tr("Type a file system path or filename here...")) self.testLine.setClearButtonEnabled(True) # --- model --> view @@ -118,6 +118,8 @@ class ExcludeListDialog(QDialog): return # if at least one row matched, we know whether table is highlighted or not self._row_matched = self.model.test_string(input_text) + # FIXME There is a bug on Windows (7) where the table rows don't get + # repainted until the table receives a mouse click event. self.tableView.update() input_regex = self.inputLine.text() @@ -132,7 +134,7 @@ class ExcludeListDialog(QDialog): match = compiled.match(input_text) if match: self._input_styled = True - self.inputLine.setStyleSheet("background-color: rgb(10, 120, 10);") + self.inputLine.setStyleSheet("background-color: rgb(10, 200, 10);") else: self.reset_input_style() @@ -160,5 +162,6 @@ Example: if you want to filter out .PNG files from the "My Pictures" directory o .*My\\sPictures\\\\.*\\.png

\ You can test the regular expression with the test string feature by pasting a fake path in it:
\ C:\\\\User\\My Pictures\\test.png

-Matching regular expressions will be highlighted.

+Matching regular expressions will be highlighted.
\ +If there is at least one highlight, the path tested will be ignored during scans.

\ Directories and files starting with a period '.' are filtered out by default.

""")) diff --git a/qt/exclude_list_table.py b/qt/exclude_list_table.py index 7c6a3b63..b58e2579 100644 --- a/qt/exclude_list_table.py +++ b/qt/exclude_list_table.py @@ -7,6 +7,8 @@ from PyQt5.QtGui import QFont, QFontMetrics, QIcon, QColor from qtlib.column import Column from qtlib.table import Table +from hscommon.trans import trget +tr = trget("ui") class ExcludeListTable(Table): @@ -31,7 +33,7 @@ class ExcludeListTable(Table): if role == Qt.CheckStateRole and row.markable: return Qt.Checked if row.marked else Qt.Unchecked if role == Qt.ToolTipRole and not row.markable: - return "Compilation error: " + row.get_cell_value("error") + return tr("Compilation error: ") + row.get_cell_value("error") if role == Qt.DecorationRole and not row.markable: return QIcon.fromTheme("dialog-error", QIcon(":/error")) return None @@ -41,7 +43,7 @@ class ExcludeListTable(Table): return QFont(self.view.font()) elif role == Qt.BackgroundRole and column.name == "regex": if row.highlight: - return QColor(10, 120, 10) # green + return QColor(10, 200, 10) # green elif role == Qt.EditRole: if column.name == "regex": return row.data[column.name] 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/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/qt/tabbed_window.py b/qt/tabbed_window.py index 193323ca..2912ddcb 100644 --- a/qt/tabbed_window.py +++ b/qt/tabbed_window.py @@ -230,8 +230,6 @@ class TabWindow(QMainWindow): # menu or shortcut. But this is useless if we don't have a button # set up to make a close request anyway. This check could be removed. return - # current_widget.close() # seems unnecessary - # self.tabWidget.widget(index).hide() self.removeTab(index) @pyqtSlot() 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 diff --git a/requirements-extra.txt b/requirements-extra.txt index cf2b4ad8..c7eedf21 100644 --- a/requirements-extra.txt +++ b/requirements-extra.txt @@ -2,3 +2,4 @@ pytest>=5,<6 flake8 tox-travis black +pyinstaller>=4.0,<5.0; sys_platform == 'win32' \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 97fa8f44..e53b5793 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,6 @@ Send2Trash>=1.3.0 sphinx>=1.2.2 polib>=1.0.4 hsaudiotag3k>=1.1.3 -distro>=1.5.0 \ No newline at end of file +distro>=1.5.0 +PyQt5 >=5.4,<6.0; sys_platform == 'win32' +pywin32>=200; sys_platform == 'win32' \ No newline at end of file diff --git a/tox.ini b/tox.ini index 6a8b14be..1323d36d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,35 +1,19 @@ [tox] -envlist = style,py36,py37,py38 +envlist = py36,py37,py38,py39 skipsdist = True skip_missing_interpreters = True [testenv] -whitelist_externals = - make setenv = PYTHON="{envpython}" commands = - make modules + python build.py --modules + flake8 {posargs:py.test core hscommon} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-extra.txt -[testenv:style] -deps = - {[testenv]deps} - flake8 -commands = - flake8 - -[testenv:WINDOWS] -deps = - {[testenv]deps} - -r{toxinidir}/requirements-windows.txt -commands = - python build.py - python package.py - [flake8] exclude = .tox,env,build,cocoalib,cocoa,help,./qt/dg_rc.py,cocoa/run_template.py,./pkg max-line-length = 120