mirror of
https://github.com/arsenetar/dupeguru.git
synced 2024-10-29 21:05:57 +00:00
Format all files with black correcting line length
This commit is contained in:
parent
9446f37fad
commit
ffe6b7047c
16
build.py
16
build.py
@ -30,12 +30,8 @@ def parse_args():
|
|||||||
dest="clean",
|
dest="clean",
|
||||||
help="Clean build folder before building",
|
help="Clean build folder before building",
|
||||||
)
|
)
|
||||||
parser.add_option(
|
parser.add_option("--doc", action="store_true", dest="doc", help="Build only the help file")
|
||||||
"--doc", action="store_true", dest="doc", help="Build only the help file"
|
parser.add_option("--loc", action="store_true", dest="loc", help="Build only localization")
|
||||||
)
|
|
||||||
parser.add_option(
|
|
||||||
"--loc", action="store_true", dest="loc", help="Build only localization"
|
|
||||||
)
|
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
"--updatepot",
|
"--updatepot",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@ -96,9 +92,7 @@ def build_localizations():
|
|||||||
locale_dest = op.join("build", "locale")
|
locale_dest = op.join("build", "locale")
|
||||||
if op.exists(locale_dest):
|
if op.exists(locale_dest):
|
||||||
shutil.rmtree(locale_dest)
|
shutil.rmtree(locale_dest)
|
||||||
shutil.copytree(
|
shutil.copytree("locale", locale_dest, ignore=shutil.ignore_patterns("*.po", "*.pot"))
|
||||||
"locale", locale_dest, ignore=shutil.ignore_patterns("*.po", "*.pot")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_updatepot():
|
def build_updatepot():
|
||||||
@ -165,9 +159,7 @@ def build_normal():
|
|||||||
print("Building localizations")
|
print("Building localizations")
|
||||||
build_localizations()
|
build_localizations()
|
||||||
print("Building Qt stuff")
|
print("Building Qt stuff")
|
||||||
print_and_do(
|
print_and_do("pyrcc5 {0} > {1}".format(op.join("qt", "dg.qrc"), op.join("qt", "dg_rc.py")))
|
||||||
"pyrcc5 {0} > {1}".format(op.join("qt", "dg.qrc"), op.join("qt", "dg_rc.py"))
|
|
||||||
)
|
|
||||||
fix_qt_resource_file(op.join("qt", "dg_rc.py"))
|
fix_qt_resource_file(op.join("qt", "dg_rc.py"))
|
||||||
build_help()
|
build_help()
|
||||||
|
|
||||||
|
114
core/app.py
114
core/app.py
@ -132,9 +132,7 @@ class DupeGuru(Broadcaster):
|
|||||||
logging.debug("Debug mode enabled")
|
logging.debug("Debug mode enabled")
|
||||||
Broadcaster.__init__(self)
|
Broadcaster.__init__(self)
|
||||||
self.view = view
|
self.view = view
|
||||||
self.appdata = desktop.special_folder_path(
|
self.appdata = desktop.special_folder_path(desktop.SpecialFolder.AppData, appname=self.NAME)
|
||||||
desktop.SpecialFolder.AppData, appname=self.NAME
|
|
||||||
)
|
|
||||||
if not op.exists(self.appdata):
|
if not op.exists(self.appdata):
|
||||||
os.makedirs(self.appdata)
|
os.makedirs(self.appdata)
|
||||||
self.app_mode = AppMode.Standard
|
self.app_mode = AppMode.Standard
|
||||||
@ -182,17 +180,13 @@ class DupeGuru(Broadcaster):
|
|||||||
|
|
||||||
def _get_picture_cache_path(self):
|
def _get_picture_cache_path(self):
|
||||||
cache_type = self.options["picture_cache_type"]
|
cache_type = self.options["picture_cache_type"]
|
||||||
cache_name = (
|
cache_name = "cached_pictures.shelve" if cache_type == "shelve" else "cached_pictures.db"
|
||||||
"cached_pictures.shelve" if cache_type == "shelve" else "cached_pictures.db"
|
|
||||||
)
|
|
||||||
return op.join(self.appdata, cache_name)
|
return op.join(self.appdata, cache_name)
|
||||||
|
|
||||||
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
|
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
|
||||||
if self.app_mode in (AppMode.Music, AppMode.Picture):
|
if self.app_mode in (AppMode.Music, AppMode.Picture):
|
||||||
if key == "folder_path":
|
if key == "folder_path":
|
||||||
dupe_folder_path = getattr(
|
dupe_folder_path = getattr(dupe, "display_folder_path", dupe.folder_path)
|
||||||
dupe, "display_folder_path", dupe.folder_path
|
|
||||||
)
|
|
||||||
return str(dupe_folder_path).lower()
|
return str(dupe_folder_path).lower()
|
||||||
if self.app_mode == AppMode.Picture:
|
if self.app_mode == AppMode.Picture:
|
||||||
if delta and key == "dimensions":
|
if delta and key == "dimensions":
|
||||||
@ -220,9 +214,7 @@ class DupeGuru(Broadcaster):
|
|||||||
def _get_group_sort_key(self, group, key):
|
def _get_group_sort_key(self, group, key):
|
||||||
if self.app_mode in (AppMode.Music, AppMode.Picture):
|
if self.app_mode in (AppMode.Music, AppMode.Picture):
|
||||||
if key == "folder_path":
|
if key == "folder_path":
|
||||||
dupe_folder_path = getattr(
|
dupe_folder_path = getattr(group.ref, "display_folder_path", group.ref.folder_path)
|
||||||
group.ref, "display_folder_path", group.ref.folder_path
|
|
||||||
)
|
|
||||||
return str(dupe_folder_path).lower()
|
return str(dupe_folder_path).lower()
|
||||||
if key == "percentage":
|
if key == "percentage":
|
||||||
return group.percentage
|
return group.percentage
|
||||||
@ -235,9 +227,7 @@ class DupeGuru(Broadcaster):
|
|||||||
def _do_delete(self, j, link_deleted, use_hardlinks, direct_deletion):
|
def _do_delete(self, j, link_deleted, use_hardlinks, direct_deletion):
|
||||||
def op(dupe):
|
def op(dupe):
|
||||||
j.add_progress()
|
j.add_progress()
|
||||||
return self._do_delete_dupe(
|
return self._do_delete_dupe(dupe, link_deleted, use_hardlinks, direct_deletion)
|
||||||
dupe, link_deleted, use_hardlinks, direct_deletion
|
|
||||||
)
|
|
||||||
|
|
||||||
j.start_job(self.results.mark_count)
|
j.start_job(self.results.mark_count)
|
||||||
self.results.perform_on_marked(op, True)
|
self.results.perform_on_marked(op, True)
|
||||||
@ -277,11 +267,7 @@ class DupeGuru(Broadcaster):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_export_data(self):
|
def _get_export_data(self):
|
||||||
columns = [
|
columns = [col for col in self.result_table.columns.ordered_columns if col.visible and col.name != "marked"]
|
||||||
col
|
|
||||||
for col in self.result_table.columns.ordered_columns
|
|
||||||
if col.visible and col.name != "marked"
|
|
||||||
]
|
|
||||||
colnames = [col.display for col in columns]
|
colnames = [col.display for col in columns]
|
||||||
rows = []
|
rows = []
|
||||||
for group_id, group in enumerate(self.results.groups):
|
for group_id, group in enumerate(self.results.groups):
|
||||||
@ -293,11 +279,7 @@ class DupeGuru(Broadcaster):
|
|||||||
return colnames, rows
|
return colnames, rows
|
||||||
|
|
||||||
def _results_changed(self):
|
def _results_changed(self):
|
||||||
self.selected_dupes = [
|
self.selected_dupes = [d for d in self.selected_dupes if self.results.get_group_of_duplicate(d) is not None]
|
||||||
d
|
|
||||||
for d in self.selected_dupes
|
|
||||||
if self.results.get_group_of_duplicate(d) is not None
|
|
||||||
]
|
|
||||||
self.notify("results_changed")
|
self.notify("results_changed")
|
||||||
|
|
||||||
def _start_job(self, jobid, func, args=()):
|
def _start_job(self, jobid, func, args=()):
|
||||||
@ -332,9 +314,7 @@ class DupeGuru(Broadcaster):
|
|||||||
msg = {
|
msg = {
|
||||||
JobType.Copy: tr("All marked files were copied successfully."),
|
JobType.Copy: tr("All marked files were copied successfully."),
|
||||||
JobType.Move: tr("All marked files were moved successfully."),
|
JobType.Move: tr("All marked files were moved successfully."),
|
||||||
JobType.Delete: tr(
|
JobType.Delete: tr("All marked files were successfully sent to Trash."),
|
||||||
"All marked files were successfully sent to Trash."
|
|
||||||
),
|
|
||||||
}[jobid]
|
}[jobid]
|
||||||
self.view.show_message(msg)
|
self.view.show_message(msg)
|
||||||
|
|
||||||
@ -401,15 +381,12 @@ class DupeGuru(Broadcaster):
|
|||||||
self.view.show_message(tr("'{}' does not exist.").format(d))
|
self.view.show_message(tr("'{}' does not exist.").format(d))
|
||||||
|
|
||||||
def add_selected_to_ignore_list(self):
|
def add_selected_to_ignore_list(self):
|
||||||
"""Adds :attr:`selected_dupes` to :attr:`ignore_list`.
|
"""Adds :attr:`selected_dupes` to :attr:`ignore_list`."""
|
||||||
"""
|
|
||||||
dupes = self.without_ref(self.selected_dupes)
|
dupes = self.without_ref(self.selected_dupes)
|
||||||
if not dupes:
|
if not dupes:
|
||||||
self.view.show_message(MSG_NO_SELECTED_DUPES)
|
self.view.show_message(MSG_NO_SELECTED_DUPES)
|
||||||
return
|
return
|
||||||
msg = tr(
|
msg = tr("All selected %d matches are going to be ignored in all subsequent scans. Continue?")
|
||||||
"All selected %d matches are going to be ignored in all subsequent scans. Continue?"
|
|
||||||
)
|
|
||||||
if not self.view.ask_yes_no(msg % len(dupes)):
|
if not self.view.ask_yes_no(msg % len(dupes)):
|
||||||
return
|
return
|
||||||
for dupe in dupes:
|
for dupe in dupes:
|
||||||
@ -483,16 +460,17 @@ class DupeGuru(Broadcaster):
|
|||||||
self.view.show_message(MSG_NO_MARKED_DUPES)
|
self.view.show_message(MSG_NO_MARKED_DUPES)
|
||||||
return
|
return
|
||||||
destination = self.view.select_dest_folder(
|
destination = self.view.select_dest_folder(
|
||||||
tr("Select a directory to copy marked files to") if copy
|
tr("Select a directory to copy marked files to")
|
||||||
else tr("Select a directory to move marked files to"))
|
if copy
|
||||||
|
else tr("Select a directory to move marked files to")
|
||||||
|
)
|
||||||
if destination:
|
if destination:
|
||||||
desttype = self.options["copymove_dest_type"]
|
desttype = self.options["copymove_dest_type"]
|
||||||
jobid = JobType.Copy if copy else JobType.Move
|
jobid = JobType.Copy if copy else JobType.Move
|
||||||
self._start_job(jobid, do)
|
self._start_job(jobid, do)
|
||||||
|
|
||||||
def delete_marked(self):
|
def delete_marked(self):
|
||||||
"""Start an async job to send marked duplicates to the trash.
|
"""Start an async job to send marked duplicates to the trash."""
|
||||||
"""
|
|
||||||
if not self.results.mark_count:
|
if not self.results.mark_count:
|
||||||
self.view.show_message(MSG_NO_MARKED_DUPES)
|
self.view.show_message(MSG_NO_MARKED_DUPES)
|
||||||
return
|
return
|
||||||
@ -523,9 +501,7 @@ class DupeGuru(Broadcaster):
|
|||||||
The columns and their order in the resulting CSV file is determined in the same way as in
|
The columns and their order in the resulting CSV file is determined in the same way as in
|
||||||
:meth:`export_to_xhtml`.
|
:meth:`export_to_xhtml`.
|
||||||
"""
|
"""
|
||||||
dest_file = self.view.select_dest_file(
|
dest_file = self.view.select_dest_file(tr("Select a destination for your exported CSV"), "csv")
|
||||||
tr("Select a destination for your exported CSV"), "csv"
|
|
||||||
)
|
|
||||||
if dest_file:
|
if dest_file:
|
||||||
colnames, rows = self._get_export_data()
|
colnames, rows = self._get_export_data()
|
||||||
try:
|
try:
|
||||||
@ -542,9 +518,7 @@ class DupeGuru(Broadcaster):
|
|||||||
try:
|
try:
|
||||||
return dupe.get_display_info(group, delta)
|
return dupe.get_display_info(group, delta)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(
|
logging.warning("Exception (type: %s) on GetDisplayInfo for %s: %s", type(e), str(dupe.path), str(e))
|
||||||
"Exception (type: %s) on GetDisplayInfo for %s: %s",
|
|
||||||
type(e), str(dupe.path), str(e))
|
|
||||||
return empty_data()
|
return empty_data()
|
||||||
|
|
||||||
def invoke_custom_command(self):
|
def invoke_custom_command(self):
|
||||||
@ -556,9 +530,7 @@ class DupeGuru(Broadcaster):
|
|||||||
"""
|
"""
|
||||||
cmd = self.view.get_default("CustomCommand")
|
cmd = self.view.get_default("CustomCommand")
|
||||||
if not cmd:
|
if not cmd:
|
||||||
msg = tr(
|
msg = tr("You have no custom command set up. Set it up in your preferences.")
|
||||||
"You have no custom command set up. Set it up in your preferences."
|
|
||||||
)
|
|
||||||
self.view.show_message(msg)
|
self.view.show_message(msg)
|
||||||
return
|
return
|
||||||
if not self.selected_dupes:
|
if not self.selected_dupes:
|
||||||
@ -634,9 +606,7 @@ class DupeGuru(Broadcaster):
|
|||||||
if not self.result_table.power_marker:
|
if not self.result_table.power_marker:
|
||||||
if changed_groups:
|
if changed_groups:
|
||||||
self.selected_dupes = [
|
self.selected_dupes = [
|
||||||
d
|
d for d in self.selected_dupes if self.results.get_group_of_duplicate(d).ref is d
|
||||||
for d in self.selected_dupes
|
|
||||||
if self.results.get_group_of_duplicate(d).ref is d
|
|
||||||
]
|
]
|
||||||
self.notify("results_changed")
|
self.notify("results_changed")
|
||||||
else:
|
else:
|
||||||
@ -648,20 +618,17 @@ class DupeGuru(Broadcaster):
|
|||||||
self.notify("results_changed_but_keep_selection")
|
self.notify("results_changed_but_keep_selection")
|
||||||
|
|
||||||
def mark_all(self):
|
def mark_all(self):
|
||||||
"""Set all dupes in the results as marked.
|
"""Set all dupes in the results as marked."""
|
||||||
"""
|
|
||||||
self.results.mark_all()
|
self.results.mark_all()
|
||||||
self.notify("marking_changed")
|
self.notify("marking_changed")
|
||||||
|
|
||||||
def mark_none(self):
|
def mark_none(self):
|
||||||
"""Set all dupes in the results as unmarked.
|
"""Set all dupes in the results as unmarked."""
|
||||||
"""
|
|
||||||
self.results.mark_none()
|
self.results.mark_none()
|
||||||
self.notify("marking_changed")
|
self.notify("marking_changed")
|
||||||
|
|
||||||
def mark_invert(self):
|
def mark_invert(self):
|
||||||
"""Invert the marked state of all dupes in the results.
|
"""Invert the marked state of all dupes in the results."""
|
||||||
"""
|
|
||||||
self.results.mark_invert()
|
self.results.mark_invert()
|
||||||
self.notify("marking_changed")
|
self.notify("marking_changed")
|
||||||
|
|
||||||
@ -679,8 +646,7 @@ class DupeGuru(Broadcaster):
|
|||||||
self.notify("marking_changed")
|
self.notify("marking_changed")
|
||||||
|
|
||||||
def open_selected(self):
|
def open_selected(self):
|
||||||
"""Open :attr:`selected_dupes` with their associated application.
|
"""Open :attr:`selected_dupes` with their associated application."""
|
||||||
"""
|
|
||||||
if len(self.selected_dupes) > 10:
|
if len(self.selected_dupes) > 10:
|
||||||
if not self.view.ask_yes_no(MSG_MANY_FILES_TO_OPEN):
|
if not self.view.ask_yes_no(MSG_MANY_FILES_TO_OPEN):
|
||||||
return
|
return
|
||||||
@ -688,8 +654,7 @@ class DupeGuru(Broadcaster):
|
|||||||
desktop.open_path(dupe.path)
|
desktop.open_path(dupe.path)
|
||||||
|
|
||||||
def purge_ignore_list(self):
|
def purge_ignore_list(self):
|
||||||
"""Remove files that don't exist from :attr:`ignore_list`.
|
"""Remove files that don't exist from :attr:`ignore_list`."""
|
||||||
"""
|
|
||||||
self.ignore_list.Filter(lambda f, s: op.exists(f) and op.exists(s))
|
self.ignore_list.Filter(lambda f, s: op.exists(f) and op.exists(s))
|
||||||
self.ignore_list_dialog.refresh()
|
self.ignore_list_dialog.refresh()
|
||||||
|
|
||||||
@ -719,8 +684,7 @@ class DupeGuru(Broadcaster):
|
|||||||
self.notify("results_changed_but_keep_selection")
|
self.notify("results_changed_but_keep_selection")
|
||||||
|
|
||||||
def remove_marked(self):
|
def remove_marked(self):
|
||||||
"""Removed marked duplicates from the results (without touching the files themselves).
|
"""Removed marked duplicates from the results (without touching the files themselves)."""
|
||||||
"""
|
|
||||||
if not self.results.mark_count:
|
if not self.results.mark_count:
|
||||||
self.view.show_message(MSG_NO_MARKED_DUPES)
|
self.view.show_message(MSG_NO_MARKED_DUPES)
|
||||||
return
|
return
|
||||||
@ -731,8 +695,7 @@ class DupeGuru(Broadcaster):
|
|||||||
self._results_changed()
|
self._results_changed()
|
||||||
|
|
||||||
def remove_selected(self):
|
def remove_selected(self):
|
||||||
"""Removed :attr:`selected_dupes` from the results (without touching the files themselves).
|
"""Removed :attr:`selected_dupes` from the results (without touching the files themselves)."""
|
||||||
"""
|
|
||||||
dupes = self.without_ref(self.selected_dupes)
|
dupes = self.without_ref(self.selected_dupes)
|
||||||
if not dupes:
|
if not dupes:
|
||||||
self.view.show_message(MSG_NO_SELECTED_DUPES)
|
self.view.show_message(MSG_NO_SELECTED_DUPES)
|
||||||
@ -773,9 +736,7 @@ class DupeGuru(Broadcaster):
|
|||||||
if count:
|
if count:
|
||||||
self.results.refresh_required = True
|
self.results.refresh_required = True
|
||||||
self._results_changed()
|
self._results_changed()
|
||||||
msg = tr("{} duplicate groups were changed by the re-prioritization.").format(
|
msg = tr("{} duplicate groups were changed by the re-prioritization.").format(count)
|
||||||
count
|
|
||||||
)
|
|
||||||
self.view.show_message(msg)
|
self.view.show_message(msg)
|
||||||
|
|
||||||
def reveal_selected(self):
|
def reveal_selected(self):
|
||||||
@ -819,9 +780,7 @@ class DupeGuru(Broadcaster):
|
|||||||
"""
|
"""
|
||||||
scanner = self.SCANNER_CLASS()
|
scanner = self.SCANNER_CLASS()
|
||||||
if not self.directories.has_any_file():
|
if not self.directories.has_any_file():
|
||||||
self.view.show_message(
|
self.view.show_message(tr("The selected directories contain no scannable file."))
|
||||||
tr("The selected directories contain no scannable file.")
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
# Send relevant options down to the scanner instance
|
# Send relevant options down to the scanner instance
|
||||||
for k, v in self.options.items():
|
for k, v in self.options.items():
|
||||||
@ -836,13 +795,9 @@ class DupeGuru(Broadcaster):
|
|||||||
def do(j):
|
def do(j):
|
||||||
j.set_progress(0, tr("Collecting files to scan"))
|
j.set_progress(0, tr("Collecting files to scan"))
|
||||||
if scanner.scan_type == ScanType.Folders:
|
if scanner.scan_type == ScanType.Folders:
|
||||||
files = list(
|
files = list(self.directories.get_folders(folderclass=se.fs.Folder, j=j))
|
||||||
self.directories.get_folders(folderclass=se.fs.Folder, j=j)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
files = list(
|
files = list(self.directories.get_files(fileclasses=self.fileclasses, j=j))
|
||||||
self.directories.get_files(fileclasses=self.fileclasses, j=j)
|
|
||||||
)
|
|
||||||
if self.options["ignore_hardlink_matches"]:
|
if self.options["ignore_hardlink_matches"]:
|
||||||
files = self._remove_hardlink_dupes(files)
|
files = self._remove_hardlink_dupes(files)
|
||||||
logging.info("Scanning %d files" % len(files))
|
logging.info("Scanning %d files" % len(files))
|
||||||
@ -864,13 +819,8 @@ class DupeGuru(Broadcaster):
|
|||||||
self.notify("marking_changed")
|
self.notify("marking_changed")
|
||||||
|
|
||||||
def without_ref(self, dupes):
|
def without_ref(self, dupes):
|
||||||
"""Returns ``dupes`` with all reference elements removed.
|
"""Returns ``dupes`` with all reference elements removed."""
|
||||||
"""
|
return [dupe for dupe in dupes if self.results.get_group_of_duplicate(dupe).ref is not dupe]
|
||||||
return [
|
|
||||||
dupe
|
|
||||||
for dupe in dupes
|
|
||||||
if self.results.get_group_of_duplicate(dupe).ref is not dupe
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_default(self, key, fallback_value=None):
|
def get_default(self, key, fallback_value=None):
|
||||||
result = nonone(self.view.get_default(key), fallback_value)
|
result = nonone(self.view.get_default(key), fallback_value)
|
||||||
|
@ -109,8 +109,7 @@ class Directories:
|
|||||||
# print(f"len of files: {len(files)} {files}")
|
# print(f"len of files: {len(files)} {files}")
|
||||||
for f in files:
|
for f in files:
|
||||||
if not self._exclude_list.is_excluded(root, f):
|
if not self._exclude_list.is_excluded(root, f):
|
||||||
found_files.append(fs.get_file(rootPath + f,
|
found_files.append(fs.get_file(rootPath + f, fileclasses=fileclasses))
|
||||||
fileclasses=fileclasses))
|
|
||||||
found_files = [f for f in found_files if f is not None]
|
found_files = [f for f in found_files if f is not None]
|
||||||
# In some cases, directories can be considered as files by dupeGuru, which is
|
# In some cases, directories can be considered as files by dupeGuru, which is
|
||||||
# why we have this line below. In fact, there only one case: Bundle files under
|
# why we have this line below. In fact, there only one case: Bundle files under
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
from .markable import Markable
|
from .markable import Markable
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
# TODO: perhaps use regex module for better Unicode support? https://pypi.org/project/regex/
|
# TODO: perhaps use regex module for better Unicode support? https://pypi.org/project/regex/
|
||||||
# also https://pypi.org/project/re2/
|
# also https://pypi.org/project/re2/
|
||||||
# TODO update the Result list with newly added regexes if possible
|
# TODO update the Result list with newly added regexes if possible
|
||||||
@ -15,7 +16,8 @@ from hscommon.util import FileOrPath
|
|||||||
from hscommon.plat import ISWINDOWS
|
from hscommon.plat import ISWINDOWS
|
||||||
import time
|
import time
|
||||||
|
|
||||||
default_regexes = [r"^thumbs\.db$", # Obsolete after WindowsXP
|
default_regexes = [
|
||||||
|
r"^thumbs\.db$", # Obsolete after WindowsXP
|
||||||
r"^desktop\.ini$", # Windows metadata
|
r"^desktop\.ini$", # Windows metadata
|
||||||
r"^\.DS_Store$", # MacOS metadata
|
r"^\.DS_Store$", # MacOS metadata
|
||||||
r"^\.Trash\-.*", # Linux trash directories
|
r"^\.Trash\-.*", # Linux trash directories
|
||||||
@ -34,6 +36,7 @@ def timer(func):
|
|||||||
end = time.perf_counter_ns()
|
end = time.perf_counter_ns()
|
||||||
print(f"DEBUG: func {func.__name__!r} took {end - start} ns.")
|
print(f"DEBUG: func {func.__name__!r} took {end - start} ns.")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
return wrapper_timer
|
return wrapper_timer
|
||||||
|
|
||||||
|
|
||||||
@ -45,11 +48,13 @@ def memoize(func):
|
|||||||
if args not in func.cache:
|
if args not in func.cache:
|
||||||
func.cache[args] = func(*args)
|
func.cache[args] = func(*args)
|
||||||
return func.cache[args]
|
return func.cache[args]
|
||||||
|
|
||||||
return _memoize
|
return _memoize
|
||||||
|
|
||||||
|
|
||||||
class AlreadyThereException(Exception):
|
class AlreadyThereException(Exception):
|
||||||
"""Expression already in the list"""
|
"""Expression already in the list"""
|
||||||
|
|
||||||
def __init__(self, arg="Expression is already in excluded list."):
|
def __init__(self, arg="Expression is already in excluded list."):
|
||||||
super().__init__(arg)
|
super().__init__(arg)
|
||||||
|
|
||||||
@ -169,10 +174,8 @@ class ExcludeList(Markable):
|
|||||||
|
|
||||||
def build_compiled_caches(self, union=False):
|
def build_compiled_caches(self, union=False):
|
||||||
if not union:
|
if not union:
|
||||||
self._cached_compiled_files =\
|
self._cached_compiled_files = [x for x in self._excluded_compiled if not has_sep(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 has_sep(x.pattern)]
|
||||||
self._cached_compiled_paths =\
|
|
||||||
[x for x in self._excluded_compiled if has_sep(x.pattern)]
|
|
||||||
self._dirty = False
|
self._dirty = False
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -185,20 +188,17 @@ class ExcludeList(Markable):
|
|||||||
else:
|
else:
|
||||||
# HACK returned as a tuple to get a free iterator and keep interface
|
# HACK returned as a tuple to get a free iterator and keep interface
|
||||||
# the same regardless of whether the client asked for union or not
|
# the same regardless of whether the client asked for union or not
|
||||||
self._cached_compiled_union_all =\
|
self._cached_compiled_union_all = (re.compile("|".join(marked_count)),)
|
||||||
(re.compile('|'.join(marked_count)),)
|
|
||||||
files_marked = [x for x in marked_count if not has_sep(x)]
|
files_marked = [x for x in marked_count if not has_sep(x)]
|
||||||
if not files_marked:
|
if not files_marked:
|
||||||
self._cached_compiled_union_files = tuple()
|
self._cached_compiled_union_files = tuple()
|
||||||
else:
|
else:
|
||||||
self._cached_compiled_union_files =\
|
self._cached_compiled_union_files = (re.compile("|".join(files_marked)),)
|
||||||
(re.compile('|'.join(files_marked)),)
|
|
||||||
paths_marked = [x for x in marked_count if has_sep(x)]
|
paths_marked = [x for x in marked_count if has_sep(x)]
|
||||||
if not paths_marked:
|
if not paths_marked:
|
||||||
self._cached_compiled_union_paths = tuple()
|
self._cached_compiled_union_paths = tuple()
|
||||||
else:
|
else:
|
||||||
self._cached_compiled_union_paths =\
|
self._cached_compiled_union_paths = (re.compile("|".join(paths_marked)),)
|
||||||
(re.compile('|'.join(paths_marked)),)
|
|
||||||
self._dirty = False
|
self._dirty = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -218,16 +218,14 @@ class ExcludeList(Markable):
|
|||||||
one item (one Pattern in the union case)."""
|
one item (one Pattern in the union case)."""
|
||||||
if self._dirty:
|
if self._dirty:
|
||||||
self.build_compiled_caches(self._use_union)
|
self.build_compiled_caches(self._use_union)
|
||||||
return self._cached_compiled_union_files if self._use_union\
|
return self._cached_compiled_union_files if self._use_union else self._cached_compiled_files
|
||||||
else self._cached_compiled_files
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def compiled_paths(self):
|
def compiled_paths(self):
|
||||||
"""Returns patterns with only separators in them, for more precise filtering."""
|
"""Returns patterns with only separators in them, for more precise filtering."""
|
||||||
if self._dirty:
|
if self._dirty:
|
||||||
self.build_compiled_caches(self._use_union)
|
self.build_compiled_caches(self._use_union)
|
||||||
return self._cached_compiled_union_paths if self._use_union\
|
return self._cached_compiled_union_paths if self._use_union else self._cached_compiled_paths
|
||||||
else self._cached_compiled_paths
|
|
||||||
|
|
||||||
# ---Public
|
# ---Public
|
||||||
def add(self, regex, forced=False):
|
def add(self, regex, forced=False):
|
||||||
@ -295,8 +293,7 @@ class ExcludeList(Markable):
|
|||||||
was_marked = self.is_marked(regex)
|
was_marked = self.is_marked(regex)
|
||||||
is_compilable, exception, compiled = self.compile_re(newregex)
|
is_compilable, exception, compiled = self.compile_re(newregex)
|
||||||
# We overwrite the found entry
|
# We overwrite the found entry
|
||||||
self._excluded[self._excluded.index(item)] =\
|
self._excluded[self._excluded.index(item)] = [newregex, is_compilable, exception, compiled]
|
||||||
[newregex, is_compilable, exception, compiled]
|
|
||||||
self._remove_compiled(regex)
|
self._remove_compiled(regex)
|
||||||
break
|
break
|
||||||
if not found:
|
if not found:
|
||||||
@ -343,8 +340,10 @@ class ExcludeList(Markable):
|
|||||||
# "forced" avoids compilation exceptions and adds anyway
|
# "forced" avoids compilation exceptions and adds anyway
|
||||||
self.add(regex_string, forced=True)
|
self.add(regex_string, forced=True)
|
||||||
except AlreadyThereException:
|
except AlreadyThereException:
|
||||||
logging.error(f"Regex \"{regex_string}\" \
|
logging.error(
|
||||||
loaded from XML was already present in the list.")
|
f'Regex "{regex_string}" \
|
||||||
|
loaded from XML was already present in the list.'
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
if exclude_item.get("marked") == "y":
|
if exclude_item.get("marked") == "y":
|
||||||
marked.add(regex_string)
|
marked.add(regex_string)
|
||||||
@ -369,6 +368,7 @@ loaded from XML was already present in the list.")
|
|||||||
class ExcludeDict(ExcludeList):
|
class ExcludeDict(ExcludeList):
|
||||||
"""Exclusion list holding a set of regular expressions as keys, the compiled
|
"""Exclusion list holding a set of regular expressions as keys, the compiled
|
||||||
Pattern, compilation error and compilable boolean as values."""
|
Pattern, compilation error and compilable boolean as values."""
|
||||||
|
|
||||||
# Implemntation around a dictionary instead of a list, which implies
|
# Implemntation around a dictionary instead of a list, which implies
|
||||||
# to keep the index of each string-key as its sub-element and keep it updated
|
# to keep the index of each string-key as its sub-element and keep it updated
|
||||||
# whenever insert/remove is done.
|
# whenever insert/remove is done.
|
||||||
@ -435,12 +435,7 @@ class ExcludeDict(ExcludeList):
|
|||||||
# and other indices should be pushed by one
|
# and other indices should be pushed by one
|
||||||
for value in self._excluded.values():
|
for value in self._excluded.values():
|
||||||
value["index"] += 1
|
value["index"] += 1
|
||||||
self._excluded[regex] = {
|
self._excluded[regex] = {"index": 0, "compilable": iscompilable, "error": exception, "compiled": compiled}
|
||||||
"index": 0,
|
|
||||||
"compilable": iscompilable,
|
|
||||||
"error": exception,
|
|
||||||
"compiled": compiled
|
|
||||||
}
|
|
||||||
|
|
||||||
def has_entry(self, regex):
|
def has_entry(self, regex):
|
||||||
if regex in self._excluded.keys():
|
if regex in self._excluded.keys():
|
||||||
@ -468,10 +463,10 @@ class ExcludeDict(ExcludeList):
|
|||||||
previous = self._excluded.pop(regex)
|
previous = self._excluded.pop(regex)
|
||||||
iscompilable, error, compiled = self.compile_re(newregex)
|
iscompilable, error, compiled = self.compile_re(newregex)
|
||||||
self._excluded[newregex] = {
|
self._excluded[newregex] = {
|
||||||
"index": previous.get('index'),
|
"index": previous.get("index"),
|
||||||
"compilable": iscompilable,
|
"compilable": iscompilable,
|
||||||
"error": error,
|
"error": error,
|
||||||
"compiled": compiled
|
"compiled": compiled,
|
||||||
}
|
}
|
||||||
self._remove_compiled(regex)
|
self._remove_compiled(regex)
|
||||||
if iscompilable:
|
if iscompilable:
|
||||||
@ -511,8 +506,12 @@ def ordered_keys(_dict):
|
|||||||
|
|
||||||
|
|
||||||
if ISWINDOWS:
|
if ISWINDOWS:
|
||||||
|
|
||||||
def has_sep(regexp):
|
def has_sep(regexp):
|
||||||
return '\\' + sep in regexp
|
return "\\" + sep in regexp
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
def has_sep(regexp):
|
def has_sep(regexp):
|
||||||
return sep in regexp
|
return sep in regexp
|
||||||
|
@ -131,15 +131,11 @@ def export_to_xhtml(colnames, rows):
|
|||||||
indented = "indented"
|
indented = "indented"
|
||||||
filename = row[1]
|
filename = row[1]
|
||||||
cells = "".join(CELL_TEMPLATE.format(value=value) for value in row[2:])
|
cells = "".join(CELL_TEMPLATE.format(value=value) for value in row[2:])
|
||||||
rendered_rows.append(
|
rendered_rows.append(ROW_TEMPLATE.format(indented=indented, filename=filename, cells=cells))
|
||||||
ROW_TEMPLATE.format(indented=indented, filename=filename, cells=cells)
|
|
||||||
)
|
|
||||||
previous_group_id = row[0]
|
previous_group_id = row[0]
|
||||||
rendered_rows = "".join(rendered_rows)
|
rendered_rows = "".join(rendered_rows)
|
||||||
# The main template can't use format because the css code uses {}
|
# The main template can't use format because the css code uses {}
|
||||||
content = MAIN_TEMPLATE.replace("$colheaders", colheaders).replace(
|
content = MAIN_TEMPLATE.replace("$colheaders", colheaders).replace("$rows", rendered_rows)
|
||||||
"$rows", rendered_rows
|
|
||||||
)
|
|
||||||
folder = mkdtemp()
|
folder = mkdtemp()
|
||||||
destpath = op.join(folder, "export.htm")
|
destpath = op.join(folder, "export.htm")
|
||||||
fp = open(destpath, "wt", encoding="utf-8")
|
fp = open(destpath, "wt", encoding="utf-8")
|
||||||
|
25
core/fs.py
25
core/fs.py
@ -79,16 +79,9 @@ class OperationError(FSError):
|
|||||||
|
|
||||||
|
|
||||||
class File:
|
class File:
|
||||||
"""Represents a file and holds metadata to be used for scanning.
|
"""Represents a file and holds metadata to be used for scanning."""
|
||||||
"""
|
|
||||||
|
|
||||||
INITIAL_INFO = {
|
INITIAL_INFO = {"size": 0, "mtime": 0, "md5": b"", "md5partial": b"", "md5samples": b""}
|
||||||
"size": 0,
|
|
||||||
"mtime": 0,
|
|
||||||
"md5": b"",
|
|
||||||
"md5partial": b"",
|
|
||||||
"md5samples": b""
|
|
||||||
}
|
|
||||||
# Slots for File make us save quite a bit of memory. In a memory test I've made with a lot of
|
# Slots for File make us save quite a bit of memory. In a memory test I've made with a lot of
|
||||||
# files, I saved 35% memory usage with "unread" files (no _read_info() call) and gains become
|
# files, I saved 35% memory usage with "unread" files (no _read_info() call) and gains become
|
||||||
# even greater when we take into account read attributes (70%!). Yeah, it's worth it.
|
# even greater when we take into account read attributes (70%!). Yeah, it's worth it.
|
||||||
@ -108,9 +101,7 @@ class File:
|
|||||||
try:
|
try:
|
||||||
self._read_info(attrname)
|
self._read_info(attrname)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(
|
logging.warning("An error '%s' was raised while decoding '%s'", e, repr(self.path))
|
||||||
"An error '%s' was raised while decoding '%s'", e, repr(self.path)
|
|
||||||
)
|
|
||||||
result = object.__getattribute__(self, attrname)
|
result = object.__getattribute__(self, attrname)
|
||||||
if result is NOT_SET:
|
if result is NOT_SET:
|
||||||
result = self.INITIAL_INFO[attrname]
|
result = self.INITIAL_INFO[attrname]
|
||||||
@ -192,8 +183,7 @@ class File:
|
|||||||
# --- Public
|
# --- Public
|
||||||
@classmethod
|
@classmethod
|
||||||
def can_handle(cls, path):
|
def can_handle(cls, path):
|
||||||
"""Returns whether this file wrapper class can handle ``path``.
|
"""Returns whether this file wrapper class can handle ``path``."""
|
||||||
"""
|
|
||||||
return not path.islink() and path.isfile()
|
return not path.islink() and path.isfile()
|
||||||
|
|
||||||
def rename(self, newname):
|
def rename(self, newname):
|
||||||
@ -211,8 +201,7 @@ class File:
|
|||||||
self.path = destpath
|
self.path = destpath
|
||||||
|
|
||||||
def get_display_info(self, group, delta):
|
def get_display_info(self, group, delta):
|
||||||
"""Returns a display-ready dict of dupe's data.
|
"""Returns a display-ready dict of dupe's data."""
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
# --- Properties
|
# --- Properties
|
||||||
@ -271,9 +260,7 @@ class Folder(File):
|
|||||||
@property
|
@property
|
||||||
def subfolders(self):
|
def subfolders(self):
|
||||||
if self._subfolders is None:
|
if self._subfolders is None:
|
||||||
subfolders = [
|
subfolders = [p for p in self.path.listdir() if not p.islink() and p.isdir()]
|
||||||
p for p in self.path.listdir() if not p.islink() and p.isdir()
|
|
||||||
]
|
|
||||||
self._subfolders = [self.__class__(p) for p in subfolders]
|
self._subfolders = [self.__class__(p) for p in subfolders]
|
||||||
return self._subfolders
|
return self._subfolders
|
||||||
|
|
||||||
|
@ -29,8 +29,7 @@ class DeletionOptionsView:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def update_msg(self, msg: str):
|
def update_msg(self, msg: str):
|
||||||
"""Update the dialog's prompt with ``str``.
|
"""Update the dialog's prompt with ``str``."""
|
||||||
"""
|
|
||||||
|
|
||||||
def show(self):
|
def show(self):
|
||||||
"""Show the dialog in a modal fashion.
|
"""Show the dialog in a modal fashion.
|
||||||
@ -39,8 +38,7 @@ class DeletionOptionsView:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def set_hardlink_option_enabled(self, is_enabled: bool):
|
def set_hardlink_option_enabled(self, is_enabled: bool):
|
||||||
"""Enable or disable the widget controlling :attr:`DeletionOptions.use_hardlinks`.
|
"""Enable or disable the widget controlling :attr:`DeletionOptions.use_hardlinks`."""
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class DeletionOptions(GUIObject):
|
class DeletionOptions(GUIObject):
|
||||||
@ -75,8 +73,7 @@ class DeletionOptions(GUIObject):
|
|||||||
return self.view.show()
|
return self.view.show()
|
||||||
|
|
||||||
def supports_links(self):
|
def supports_links(self):
|
||||||
"""Returns whether our platform supports symlinks.
|
"""Returns whether our platform supports symlinks."""
|
||||||
"""
|
|
||||||
# When on a platform that doesn't implement it, calling os.symlink() (with the wrong number
|
# When on a platform that doesn't implement it, calling os.symlink() (with the wrong number
|
||||||
# of arguments) raises NotImplementedError, which allows us to gracefully check for the
|
# of arguments) raises NotImplementedError, which allows us to gracefully check for the
|
||||||
# feature.
|
# feature.
|
||||||
|
@ -32,9 +32,7 @@ class DetailsPanel(GUIObject, DupeGuruGUIObject):
|
|||||||
# we don't want the two sides of the table to display the stats for the same file
|
# we don't want the two sides of the table to display the stats for the same file
|
||||||
ref = group.ref if group is not None and group.ref is not dupe else None
|
ref = group.ref if group is not None and group.ref is not dupe else None
|
||||||
data2 = self.app.get_display_info(ref, group, False)
|
data2 = self.app.get_display_info(ref, group, False)
|
||||||
columns = self.app.result_table.COLUMNS[
|
columns = self.app.result_table.COLUMNS[1:] # first column is the 'marked' column
|
||||||
1:
|
|
||||||
] # first column is the 'marked' column
|
|
||||||
self._table = [(c.display, data1[c.name], data2[c.name]) for c in columns]
|
self._table = [(c.display, data1[c.name], data2[c.name]) for c in columns]
|
||||||
|
|
||||||
# --- Public
|
# --- Public
|
||||||
|
@ -36,9 +36,7 @@ class DirectoryNode(Node):
|
|||||||
self._loaded = True
|
self._loaded = True
|
||||||
|
|
||||||
def update_all_states(self):
|
def update_all_states(self):
|
||||||
self._state = STATE_ORDER.index(
|
self._state = STATE_ORDER.index(self._tree.app.directories.get_state(self._directory_path))
|
||||||
self._tree.app.directories.get_state(self._directory_path)
|
|
||||||
)
|
|
||||||
for node in self:
|
for node in self:
|
||||||
node.update_all_states()
|
node.update_all_states()
|
||||||
|
|
||||||
|
@ -6,14 +6,12 @@ from .base import DupeGuruGUIObject
|
|||||||
from hscommon.gui.table import GUITable, Row
|
from hscommon.gui.table import GUITable, Row
|
||||||
from hscommon.gui.column import Column, Columns
|
from hscommon.gui.column import Column, Columns
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
|
|
||||||
tr = trget("ui")
|
tr = trget("ui")
|
||||||
|
|
||||||
|
|
||||||
class ExcludeListTable(GUITable, DupeGuruGUIObject):
|
class ExcludeListTable(GUITable, DupeGuruGUIObject):
|
||||||
COLUMNS = [
|
COLUMNS = [Column("marked", ""), Column("regex", tr("Regular Expressions"))]
|
||||||
Column("marked", ""),
|
|
||||||
Column("regex", tr("Regular Expressions"))
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, exclude_list_dialog, app):
|
def __init__(self, exclude_list_dialog, app):
|
||||||
GUITable.__init__(self)
|
GUITable.__init__(self)
|
||||||
|
@ -22,9 +22,7 @@ class IgnoreListDialog:
|
|||||||
def clear(self):
|
def clear(self):
|
||||||
if not self.ignore_list:
|
if not self.ignore_list:
|
||||||
return
|
return
|
||||||
msg = tr(
|
msg = tr("Do you really want to remove all %d items from the ignore list?") % len(self.ignore_list)
|
||||||
"Do you really want to remove all %d items from the ignore list?"
|
|
||||||
) % len(self.ignore_list)
|
|
||||||
if self.app.view.ask_yes_no(msg):
|
if self.app.view.ask_yes_no(msg):
|
||||||
self.ignore_list.Clear()
|
self.ignore_list.Clear()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
@ -45,9 +45,7 @@ class DupeRow(Row):
|
|||||||
return False
|
return False
|
||||||
ref_info = self._group.ref.get_display_info(group=self._group, delta=False)
|
ref_info = self._group.ref.get_display_info(group=self._group, delta=False)
|
||||||
for key, value in dupe_info.items():
|
for key, value in dupe_info.items():
|
||||||
if (key not in self._delta_columns) and (
|
if (key not in self._delta_columns) and (ref_info[key].lower() != value.lower()):
|
||||||
ref_info[key].lower() != value.lower()
|
|
||||||
):
|
|
||||||
self._delta_columns.add(key)
|
self._delta_columns.add(key)
|
||||||
return column_name in self._delta_columns
|
return column_name in self._delta_columns
|
||||||
|
|
||||||
|
@ -33,8 +33,7 @@ CacheRow = namedtuple("CacheRow", "id path blocks mtime")
|
|||||||
|
|
||||||
|
|
||||||
class ShelveCache:
|
class ShelveCache:
|
||||||
"""A class to cache picture blocks in a shelve backend.
|
"""A class to cache picture blocks in a shelve backend."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, db=None, readonly=False):
|
def __init__(self, db=None, readonly=False):
|
||||||
self.istmp = db is None
|
self.istmp = db is None
|
||||||
@ -81,9 +80,7 @@ class ShelveCache:
|
|||||||
self.shelve[wrap_id(rowid)] = wrap_path(path_str)
|
self.shelve[wrap_id(rowid)] = wrap_path(path_str)
|
||||||
|
|
||||||
def _compute_maxid(self):
|
def _compute_maxid(self):
|
||||||
return max(
|
return max((unwrap_id(k) for k in self.shelve if k.startswith("id:")), default=1)
|
||||||
(unwrap_id(k) for k in self.shelve if k.startswith("id:")), default=1
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_new_id(self):
|
def _get_new_id(self):
|
||||||
self.maxid += 1
|
self.maxid += 1
|
||||||
|
@ -13,8 +13,7 @@ from .cache import string_to_colors, colors_to_string
|
|||||||
|
|
||||||
|
|
||||||
class SqliteCache:
|
class SqliteCache:
|
||||||
"""A class to cache picture blocks in a sqlite backend.
|
"""A class to cache picture blocks in a sqlite backend."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, db=":memory:", readonly=False):
|
def __init__(self, db=":memory:", readonly=False):
|
||||||
# readonly is not used in the sqlite version of the cache
|
# readonly is not used in the sqlite version of the cache
|
||||||
@ -71,18 +70,14 @@ class SqliteCache:
|
|||||||
except sqlite.OperationalError:
|
except sqlite.OperationalError:
|
||||||
logging.warning("Picture cache could not set value for key %r", path_str)
|
logging.warning("Picture cache could not set value for key %r", path_str)
|
||||||
except sqlite.DatabaseError as e:
|
except sqlite.DatabaseError as e:
|
||||||
logging.warning(
|
logging.warning("DatabaseError while setting value for key %r: %s", path_str, str(e))
|
||||||
"DatabaseError while setting value for key %r: %s", path_str, str(e)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _create_con(self, second_try=False):
|
def _create_con(self, second_try=False):
|
||||||
def create_tables():
|
def create_tables():
|
||||||
logging.debug("Creating picture cache tables.")
|
logging.debug("Creating picture cache tables.")
|
||||||
self.con.execute("drop table if exists pictures")
|
self.con.execute("drop table if exists pictures")
|
||||||
self.con.execute("drop index if exists idx_path")
|
self.con.execute("drop index if exists idx_path")
|
||||||
self.con.execute(
|
self.con.execute("create table pictures(path TEXT, mtime INTEGER, blocks TEXT)")
|
||||||
"create table pictures(path TEXT, mtime INTEGER, blocks TEXT)"
|
|
||||||
)
|
|
||||||
self.con.execute("create index idx_path on pictures (path)")
|
self.con.execute("create index idx_path on pictures (path)")
|
||||||
|
|
||||||
self.con = sqlite.connect(self.dbname, isolation_level=None)
|
self.con = sqlite.connect(self.dbname, isolation_level=None)
|
||||||
@ -93,9 +88,7 @@ class SqliteCache:
|
|||||||
except sqlite.DatabaseError as e: # corrupted db
|
except sqlite.DatabaseError as e: # corrupted db
|
||||||
if second_try:
|
if second_try:
|
||||||
raise # Something really strange is happening
|
raise # Something really strange is happening
|
||||||
logging.warning(
|
logging.warning("Could not create picture cache because of an error: %s", str(e))
|
||||||
"Could not create picture cache because of an error: %s", str(e)
|
|
||||||
)
|
|
||||||
self.con.close()
|
self.con.close()
|
||||||
os.remove(self.dbname)
|
os.remove(self.dbname)
|
||||||
self._create_con(second_try=True)
|
self._create_con(second_try=True)
|
||||||
@ -125,9 +118,7 @@ class SqliteCache:
|
|||||||
raise ValueError(path)
|
raise ValueError(path)
|
||||||
|
|
||||||
def get_multiple(self, rowids):
|
def get_multiple(self, rowids):
|
||||||
sql = "select rowid, blocks from pictures where rowid in (%s)" % ",".join(
|
sql = "select rowid, blocks from pictures where rowid in (%s)" % ",".join(map(str, rowids))
|
||||||
map(str, rowids)
|
|
||||||
)
|
|
||||||
cur = self.con.execute(sql)
|
cur = self.con.execute(sql)
|
||||||
return ((rowid, string_to_colors(blocks)) for rowid, blocks in cur)
|
return ((rowid, string_to_colors(blocks)) for rowid, blocks in cur)
|
||||||
|
|
||||||
@ -148,7 +139,5 @@ class SqliteCache:
|
|||||||
continue
|
continue
|
||||||
todelete.append(rowid)
|
todelete.append(rowid)
|
||||||
if todelete:
|
if todelete:
|
||||||
sql = "delete from pictures where rowid in (%s)" % ",".join(
|
sql = "delete from pictures where rowid in (%s)" % ",".join(map(str, todelete))
|
||||||
map(str, todelete)
|
|
||||||
)
|
|
||||||
self.con.execute(sql)
|
self.con.execute(sql)
|
||||||
|
@ -256,9 +256,7 @@ class TIFF_file:
|
|||||||
for j in range(count):
|
for j in range(count):
|
||||||
if type in {5, 10}:
|
if type in {5, 10}:
|
||||||
# The type is either 5 or 10
|
# The type is either 5 or 10
|
||||||
value_j = Fraction(
|
value_j = Fraction(self.s2n(offset, 4, signed), self.s2n(offset + 4, 4, signed))
|
||||||
self.s2n(offset, 4, signed), self.s2n(offset + 4, 4, signed)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# Not a fraction
|
# Not a fraction
|
||||||
value_j = self.s2n(offset, typelen, signed)
|
value_j = self.s2n(offset, typelen, signed)
|
||||||
@ -296,9 +294,7 @@ def get_fields(fp):
|
|||||||
logging.debug("Exif header length: %d bytes", length)
|
logging.debug("Exif header length: %d bytes", length)
|
||||||
data = fp.read(length - 8)
|
data = fp.read(length - 8)
|
||||||
data_format = data[0]
|
data_format = data[0]
|
||||||
logging.debug(
|
logging.debug("%s format", {INTEL_ENDIAN: "Intel", MOTOROLA_ENDIAN: "Motorola"}[data_format])
|
||||||
"%s format", {INTEL_ENDIAN: "Intel", MOTOROLA_ENDIAN: "Motorola"}[data_format]
|
|
||||||
)
|
|
||||||
T = TIFF_file(data)
|
T = TIFF_file(data)
|
||||||
# There may be more than one IFD per file, but we only read the first one because others are
|
# There may be more than one IFD per file, but we only read the first one because others are
|
||||||
# most likely thumbnails.
|
# most likely thumbnails.
|
||||||
|
@ -95,9 +95,7 @@ def prepare_pictures(pictures, cache_path, with_dimensions, j=job.nulljob):
|
|||||||
picture.unicode_path,
|
picture.unicode_path,
|
||||||
picture.size,
|
picture.size,
|
||||||
)
|
)
|
||||||
if (
|
if picture.size < 10 * 1024 * 1024: # We're really running out of memory
|
||||||
picture.size < 10 * 1024 * 1024
|
|
||||||
): # We're really running out of memory
|
|
||||||
raise
|
raise
|
||||||
except MemoryError:
|
except MemoryError:
|
||||||
logging.warning("Ran out of memory while preparing pictures")
|
logging.warning("Ran out of memory while preparing pictures")
|
||||||
@ -106,9 +104,7 @@ def prepare_pictures(pictures, cache_path, with_dimensions, j=job.nulljob):
|
|||||||
|
|
||||||
|
|
||||||
def get_chunks(pictures):
|
def get_chunks(pictures):
|
||||||
min_chunk_count = (
|
min_chunk_count = multiprocessing.cpu_count() * 2 # have enough chunks to feed all subprocesses
|
||||||
multiprocessing.cpu_count() * 2
|
|
||||||
) # have enough chunks to feed all subprocesses
|
|
||||||
chunk_count = len(pictures) // DEFAULT_CHUNK_SIZE
|
chunk_count = len(pictures) // DEFAULT_CHUNK_SIZE
|
||||||
chunk_count = max(min_chunk_count, chunk_count)
|
chunk_count = max(min_chunk_count, chunk_count)
|
||||||
chunk_size = (len(pictures) // chunk_count) + 1
|
chunk_size = (len(pictures) // chunk_count) + 1
|
||||||
@ -185,9 +181,7 @@ def getmatches(pictures, cache_path, threshold, match_scaled=False, j=job.nulljo
|
|||||||
j.set_progress(comparison_count, progress_msg)
|
j.set_progress(comparison_count, progress_msg)
|
||||||
|
|
||||||
j = j.start_subjob([3, 7])
|
j = j.start_subjob([3, 7])
|
||||||
pictures = prepare_pictures(
|
pictures = prepare_pictures(pictures, cache_path, with_dimensions=not match_scaled, j=j)
|
||||||
pictures, cache_path, with_dimensions=not match_scaled, j=j
|
|
||||||
)
|
|
||||||
j = j.start_subjob([9, 1], tr("Preparing for matching"))
|
j = j.start_subjob([9, 1], tr("Preparing for matching"))
|
||||||
cache = get_cache(cache_path)
|
cache = get_cache(cache_path)
|
||||||
id2picture = {}
|
id2picture = {}
|
||||||
@ -231,12 +225,8 @@ def getmatches(pictures, cache_path, threshold, match_scaled=False, j=job.nulljo
|
|||||||
chunks,
|
chunks,
|
||||||
pictures,
|
pictures,
|
||||||
) # some wiggle room for the next statements
|
) # some wiggle room for the next statements
|
||||||
logging.warning(
|
logging.warning("Ran out of memory when scanning! We had %d matches.", len(matches))
|
||||||
"Ran out of memory when scanning! We had %d matches.", len(matches)
|
del matches[-len(matches) // 3 :] # some wiggle room to ensure we don't run out of memory again.
|
||||||
)
|
|
||||||
del matches[
|
|
||||||
-len(matches) // 3 :
|
|
||||||
] # some wiggle room to ensure we don't run out of memory again.
|
|
||||||
pool.close()
|
pool.close()
|
||||||
result = []
|
result = []
|
||||||
myiter = j.iter_with_progress(
|
myiter = j.iter_with_progress(
|
||||||
|
@ -87,11 +87,7 @@ class Scanner:
|
|||||||
if self.size_threshold:
|
if self.size_threshold:
|
||||||
files = [f for f in files if f.size >= self.size_threshold]
|
files = [f for f in files if f.size >= self.size_threshold]
|
||||||
if self.scan_type in {ScanType.Contents, ScanType.Folders}:
|
if self.scan_type in {ScanType.Contents, ScanType.Folders}:
|
||||||
return engine.getmatches_by_contents(
|
return engine.getmatches_by_contents(files, bigsize=self.big_file_size_threshold, j=j)
|
||||||
files,
|
|
||||||
bigsize=self.big_file_size_threshold,
|
|
||||||
j=j
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
j = j.start_subjob([2, 8])
|
j = j.start_subjob([2, 8])
|
||||||
kw = {}
|
kw = {}
|
||||||
@ -165,27 +161,13 @@ class Scanner:
|
|||||||
toremove.add(p)
|
toremove.add(p)
|
||||||
else:
|
else:
|
||||||
last_parent_path = p
|
last_parent_path = p
|
||||||
matches = [
|
matches = [m for m in matches if m.first.path not in toremove or m.second.path not in toremove]
|
||||||
m
|
|
||||||
for m in matches
|
|
||||||
if m.first.path not in toremove or m.second.path not in toremove
|
|
||||||
]
|
|
||||||
if not self.mix_file_kind:
|
if not self.mix_file_kind:
|
||||||
matches = [
|
matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)]
|
||||||
m
|
matches = [m for m in matches if m.first.path.exists() and m.second.path.exists()]
|
||||||
for m in matches
|
|
||||||
if get_file_ext(m.first.name) == get_file_ext(m.second.name)
|
|
||||||
]
|
|
||||||
matches = [
|
|
||||||
m for m in matches if m.first.path.exists() and m.second.path.exists()
|
|
||||||
]
|
|
||||||
matches = [m for m in matches if not (m.first.is_ref and m.second.is_ref)]
|
matches = [m for m in matches if not (m.first.is_ref and m.second.is_ref)]
|
||||||
if ignore_list:
|
if ignore_list:
|
||||||
matches = [
|
matches = [m for m in matches if not ignore_list.AreIgnored(str(m.first.path), str(m.second.path))]
|
||||||
m
|
|
||||||
for m in matches
|
|
||||||
if not ignore_list.AreIgnored(str(m.first.path), str(m.second.path))
|
|
||||||
]
|
|
||||||
logging.info("Grouping matches")
|
logging.info("Grouping matches")
|
||||||
groups = engine.get_groups(matches)
|
groups = engine.get_groups(matches)
|
||||||
if self.scan_type in {
|
if self.scan_type in {
|
||||||
@ -194,9 +176,7 @@ class Scanner:
|
|||||||
ScanType.FieldsNoOrder,
|
ScanType.FieldsNoOrder,
|
||||||
ScanType.Tag,
|
ScanType.Tag,
|
||||||
}:
|
}:
|
||||||
matched_files = dedupe(
|
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
|
||||||
[m.first for m in matches] + [m.second for m in matches]
|
|
||||||
)
|
|
||||||
self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups)
|
self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups)
|
||||||
else:
|
else:
|
||||||
# Ticket #195
|
# Ticket #195
|
||||||
|
@ -29,9 +29,7 @@ def add_fake_files_to_directories(directories, files):
|
|||||||
class TestCaseDupeGuru:
|
class TestCaseDupeGuru:
|
||||||
def test_apply_filter_calls_results_apply_filter(self, monkeypatch):
|
def test_apply_filter_calls_results_apply_filter(self, monkeypatch):
|
||||||
dgapp = TestApp().app
|
dgapp = TestApp().app
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(dgapp.results, "apply_filter", log_calls(dgapp.results.apply_filter))
|
||||||
dgapp.results, "apply_filter", log_calls(dgapp.results.apply_filter)
|
|
||||||
)
|
|
||||||
dgapp.apply_filter("foo")
|
dgapp.apply_filter("foo")
|
||||||
eq_(2, len(dgapp.results.apply_filter.calls))
|
eq_(2, len(dgapp.results.apply_filter.calls))
|
||||||
call = dgapp.results.apply_filter.calls[0]
|
call = dgapp.results.apply_filter.calls[0]
|
||||||
@ -41,15 +39,11 @@ class TestCaseDupeGuru:
|
|||||||
|
|
||||||
def test_apply_filter_escapes_regexp(self, monkeypatch):
|
def test_apply_filter_escapes_regexp(self, monkeypatch):
|
||||||
dgapp = TestApp().app
|
dgapp = TestApp().app
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(dgapp.results, "apply_filter", log_calls(dgapp.results.apply_filter))
|
||||||
dgapp.results, "apply_filter", log_calls(dgapp.results.apply_filter)
|
|
||||||
)
|
|
||||||
dgapp.apply_filter("()[]\\.|+?^abc")
|
dgapp.apply_filter("()[]\\.|+?^abc")
|
||||||
call = dgapp.results.apply_filter.calls[1]
|
call = dgapp.results.apply_filter.calls[1]
|
||||||
eq_("\\(\\)\\[\\]\\\\\\.\\|\\+\\?\\^abc", call["filter_str"])
|
eq_("\\(\\)\\[\\]\\\\\\.\\|\\+\\?\\^abc", call["filter_str"])
|
||||||
dgapp.apply_filter(
|
dgapp.apply_filter("(*)") # In "simple mode", we want the * to behave as a wilcard
|
||||||
"(*)"
|
|
||||||
) # In "simple mode", we want the * to behave as a wilcard
|
|
||||||
call = dgapp.results.apply_filter.calls[3]
|
call = dgapp.results.apply_filter.calls[3]
|
||||||
eq_(r"\(.*\)", call["filter_str"])
|
eq_(r"\(.*\)", call["filter_str"])
|
||||||
dgapp.options["escape_filter_regexp"] = False
|
dgapp.options["escape_filter_regexp"] = False
|
||||||
@ -70,9 +64,7 @@ class TestCaseDupeGuru:
|
|||||||
)
|
)
|
||||||
# XXX This monkeypatch is temporary. will be fixed in a better monkeypatcher.
|
# XXX This monkeypatch is temporary. will be fixed in a better monkeypatcher.
|
||||||
monkeypatch.setattr(app, "smart_copy", hscommon.conflict.smart_copy)
|
monkeypatch.setattr(app, "smart_copy", hscommon.conflict.smart_copy)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(os, "makedirs", lambda path: None) # We don't want the test to create that fake directory
|
||||||
os, "makedirs", lambda path: None
|
|
||||||
) # We don't want the test to create that fake directory
|
|
||||||
dgapp = TestApp().app
|
dgapp = TestApp().app
|
||||||
dgapp.directories.add_path(p)
|
dgapp.directories.add_path(p)
|
||||||
[f] = dgapp.directories.get_files()
|
[f] = dgapp.directories.get_files()
|
||||||
@ -320,9 +312,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
assert groups[0].ref is objects[1]
|
assert groups[0].ref is objects[1]
|
||||||
assert groups[1].ref is objects[4]
|
assert groups[1].ref is objects[4]
|
||||||
|
|
||||||
def test_makeSelectedReference_by_selecting_two_dupes_in_the_same_group(
|
def test_makeSelectedReference_by_selecting_two_dupes_in_the_same_group(self, do_setup):
|
||||||
self, do_setup
|
|
||||||
):
|
|
||||||
app = self.app
|
app = self.app
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
groups = self.groups
|
groups = self.groups
|
||||||
@ -404,9 +394,7 @@ class TestCaseDupeGuruWithResults:
|
|||||||
# results table.
|
# results table.
|
||||||
app = self.app
|
app = self.app
|
||||||
app.JOB = Job(1, lambda *args, **kw: False) # Cancels the task
|
app.JOB = Job(1, lambda *args, **kw: False) # Cancels the task
|
||||||
add_fake_files_to_directories(
|
add_fake_files_to_directories(app.directories, self.objects) # We want the scan to at least start
|
||||||
app.directories, self.objects
|
|
||||||
) # We want the scan to at least start
|
|
||||||
app.start_scanning() # will be cancelled immediately
|
app.start_scanning() # will be cancelled immediately
|
||||||
eq_(len(app.result_table), 0)
|
eq_(len(app.result_table), 0)
|
||||||
|
|
||||||
|
@ -140,9 +140,7 @@ def GetTestGroups():
|
|||||||
matches = engine.getmatches(objects) # we should have 5 matches
|
matches = engine.getmatches(objects) # we should have 5 matches
|
||||||
groups = engine.get_groups(matches) # We should have 2 groups
|
groups = engine.get_groups(matches) # We should have 2 groups
|
||||||
for g in groups:
|
for g in groups:
|
||||||
g.prioritize(
|
g.prioritize(lambda x: objects.index(x)) # We want the dupes to be in the same order as the list is
|
||||||
lambda x: objects.index(x)
|
|
||||||
) # We want the dupes to be in the same order as the list is
|
|
||||||
groups.sort(key=len, reverse=True) # We want the group with 3 members to be first.
|
groups.sort(key=len, reverse=True) # We want the group with 3 members to be first.
|
||||||
return (objects, matches, groups)
|
return (objects, matches, groups)
|
||||||
|
|
||||||
|
@ -14,9 +14,7 @@ except ImportError:
|
|||||||
skip("Can't import the block module, probably hasn't been compiled.")
|
skip("Can't import the block module, probably hasn't been compiled.")
|
||||||
|
|
||||||
|
|
||||||
def my_avgdiff(
|
def my_avgdiff(first, second, limit=768, min_iter=3): # this is so I don't have to re-write every call
|
||||||
first, second, limit=768, min_iter=3
|
|
||||||
): # this is so I don't have to re-write every call
|
|
||||||
return avgdiff(first, second, limit, min_iter)
|
return avgdiff(first, second, limit, min_iter)
|
||||||
|
|
||||||
|
|
||||||
|
@ -254,7 +254,12 @@ def test_invalid_path():
|
|||||||
def test_set_state_on_invalid_path():
|
def test_set_state_on_invalid_path():
|
||||||
d = Directories()
|
d = Directories()
|
||||||
try:
|
try:
|
||||||
d.set_state(Path("foobar",), DirectoryState.Normal)
|
d.set_state(
|
||||||
|
Path(
|
||||||
|
"foobar",
|
||||||
|
),
|
||||||
|
DirectoryState.Normal,
|
||||||
|
)
|
||||||
except LookupError:
|
except LookupError:
|
||||||
assert False
|
assert False
|
||||||
|
|
||||||
@ -345,15 +350,17 @@ def test_default_path_state_override(tmpdir):
|
|||||||
eq_(len(list(d.get_files())), 2)
|
eq_(len(list(d.get_files())), 2)
|
||||||
|
|
||||||
|
|
||||||
class TestExcludeList():
|
class TestExcludeList:
|
||||||
def setup_method(self, method):
|
def setup_method(self, method):
|
||||||
self.d = Directories(exclude_list=ExcludeList(union_regex=False))
|
self.d = Directories(exclude_list=ExcludeList(union_regex=False))
|
||||||
|
|
||||||
def get_files_and_expect_num_result(self, num_result):
|
def get_files_and_expect_num_result(self, num_result):
|
||||||
"""Calls get_files(), get the filenames only, print for debugging.
|
"""Calls get_files(), get the filenames only, print for debugging.
|
||||||
num_result is how many files are expected as a result."""
|
num_result is how many files are expected as a result."""
|
||||||
print(f"EXCLUDED REGEX: paths {self.d._exclude_list.compiled_paths} \
|
print(
|
||||||
files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled}")
|
f"EXCLUDED REGEX: paths {self.d._exclude_list.compiled_paths} \
|
||||||
|
files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled}"
|
||||||
|
)
|
||||||
files = list(self.d.get_files())
|
files = list(self.d.get_files())
|
||||||
files = [file.name for file in files]
|
files = [file.name for file in files]
|
||||||
print(f"FINAL FILES {files}")
|
print(f"FINAL FILES {files}")
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
# http://www.gnu.org/licenses/gpl-3.0.html
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
|
||||||
# import os.path as op
|
# import os.path as op
|
||||||
|
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
@ -104,7 +105,7 @@ class TestCaseListEmpty:
|
|||||||
regex1 = r"one"
|
regex1 = r"one"
|
||||||
regex2 = r"two"
|
regex2 = r"two"
|
||||||
self.exclude_list.add(regex1)
|
self.exclude_list.add(regex1)
|
||||||
assert(regex1 in self.exclude_list)
|
assert regex1 in self.exclude_list
|
||||||
self.exclude_list.add(regex2)
|
self.exclude_list.add(regex2)
|
||||||
self.exclude_list.mark(regex1)
|
self.exclude_list.mark(regex1)
|
||||||
self.exclude_list.mark(regex2)
|
self.exclude_list.mark(regex2)
|
||||||
@ -113,7 +114,7 @@ class TestCaseListEmpty:
|
|||||||
compiled_files = [x for x in self.exclude_list.compiled_files]
|
compiled_files = [x for x in self.exclude_list.compiled_files]
|
||||||
eq_(len(compiled_files), 2)
|
eq_(len(compiled_files), 2)
|
||||||
self.exclude_list.remove(regex2)
|
self.exclude_list.remove(regex2)
|
||||||
assert(regex2 not in self.exclude_list)
|
assert regex2 not in self.exclude_list
|
||||||
eq_(len(self.exclude_list), 1)
|
eq_(len(self.exclude_list), 1)
|
||||||
|
|
||||||
def test_add_duplicate(self):
|
def test_add_duplicate(self):
|
||||||
@ -237,6 +238,7 @@ class TestCaseListEmpty:
|
|||||||
|
|
||||||
class TestCaseListEmptyUnion(TestCaseListEmpty):
|
class TestCaseListEmptyUnion(TestCaseListEmpty):
|
||||||
"""Same but with union regex"""
|
"""Same but with union regex"""
|
||||||
|
|
||||||
def setup_method(self, method):
|
def setup_method(self, method):
|
||||||
self.app = DupeGuru()
|
self.app = DupeGuru()
|
||||||
self.app.exclude_list = ExcludeList(union_regex=True)
|
self.app.exclude_list = ExcludeList(union_regex=True)
|
||||||
@ -246,7 +248,7 @@ class TestCaseListEmptyUnion(TestCaseListEmpty):
|
|||||||
regex1 = r"one"
|
regex1 = r"one"
|
||||||
regex2 = r"two"
|
regex2 = r"two"
|
||||||
self.exclude_list.add(regex1)
|
self.exclude_list.add(regex1)
|
||||||
assert(regex1 in self.exclude_list)
|
assert regex1 in self.exclude_list
|
||||||
self.exclude_list.add(regex2)
|
self.exclude_list.add(regex2)
|
||||||
self.exclude_list.mark(regex1)
|
self.exclude_list.mark(regex1)
|
||||||
self.exclude_list.mark(regex2)
|
self.exclude_list.mark(regex2)
|
||||||
@ -256,7 +258,7 @@ class TestCaseListEmptyUnion(TestCaseListEmpty):
|
|||||||
eq_(len(compiled_files), 1) # Two patterns joined together into one
|
eq_(len(compiled_files), 1) # Two patterns joined together into one
|
||||||
assert "|" in compiled_files[0].pattern
|
assert "|" in compiled_files[0].pattern
|
||||||
self.exclude_list.remove(regex2)
|
self.exclude_list.remove(regex2)
|
||||||
assert(regex2 not in self.exclude_list)
|
assert regex2 not in self.exclude_list
|
||||||
eq_(len(self.exclude_list), 1)
|
eq_(len(self.exclude_list), 1)
|
||||||
|
|
||||||
def test_rename_regex_file_to_path(self):
|
def test_rename_regex_file_to_path(self):
|
||||||
@ -296,14 +298,15 @@ class TestCaseListEmptyUnion(TestCaseListEmpty):
|
|||||||
compiled = [x for x in self.exclude_list.compiled]
|
compiled = [x for x in self.exclude_list.compiled]
|
||||||
assert regex not in compiled
|
assert regex not in compiled
|
||||||
# Need to escape both to get the same strings after compilation
|
# Need to escape both to get the same strings after compilation
|
||||||
compiled_escaped = set([x.encode('unicode-escape').decode() for x in compiled[0].pattern.split("|")])
|
compiled_escaped = set([x.encode("unicode-escape").decode() for x in compiled[0].pattern.split("|")])
|
||||||
default_escaped = set([x.encode('unicode-escape').decode() for x in default_regexes])
|
default_escaped = set([x.encode("unicode-escape").decode() for x in default_regexes])
|
||||||
assert compiled_escaped == default_escaped
|
assert compiled_escaped == default_escaped
|
||||||
eq_(len(default_regexes), len(compiled[0].pattern.split("|")))
|
eq_(len(default_regexes), len(compiled[0].pattern.split("|")))
|
||||||
|
|
||||||
|
|
||||||
class TestCaseDictEmpty(TestCaseListEmpty):
|
class TestCaseDictEmpty(TestCaseListEmpty):
|
||||||
"""Same, but with dictionary implementation"""
|
"""Same, but with dictionary implementation"""
|
||||||
|
|
||||||
def setup_method(self, method):
|
def setup_method(self, method):
|
||||||
self.app = DupeGuru()
|
self.app = DupeGuru()
|
||||||
self.app.exclude_list = ExcludeDict(union_regex=False)
|
self.app.exclude_list = ExcludeDict(union_regex=False)
|
||||||
@ -312,6 +315,7 @@ class TestCaseDictEmpty(TestCaseListEmpty):
|
|||||||
|
|
||||||
class TestCaseDictEmptyUnion(TestCaseDictEmpty):
|
class TestCaseDictEmptyUnion(TestCaseDictEmpty):
|
||||||
"""Same, but with union regex"""
|
"""Same, but with union regex"""
|
||||||
|
|
||||||
def setup_method(self, method):
|
def setup_method(self, method):
|
||||||
self.app = DupeGuru()
|
self.app = DupeGuru()
|
||||||
self.app.exclude_list = ExcludeDict(union_regex=True)
|
self.app.exclude_list = ExcludeDict(union_regex=True)
|
||||||
@ -321,7 +325,7 @@ class TestCaseDictEmptyUnion(TestCaseDictEmpty):
|
|||||||
regex1 = r"one"
|
regex1 = r"one"
|
||||||
regex2 = r"two"
|
regex2 = r"two"
|
||||||
self.exclude_list.add(regex1)
|
self.exclude_list.add(regex1)
|
||||||
assert(regex1 in self.exclude_list)
|
assert regex1 in self.exclude_list
|
||||||
self.exclude_list.add(regex2)
|
self.exclude_list.add(regex2)
|
||||||
self.exclude_list.mark(regex1)
|
self.exclude_list.mark(regex1)
|
||||||
self.exclude_list.mark(regex2)
|
self.exclude_list.mark(regex2)
|
||||||
@ -331,7 +335,7 @@ class TestCaseDictEmptyUnion(TestCaseDictEmpty):
|
|||||||
# two patterns joined into one
|
# two patterns joined into one
|
||||||
eq_(len(compiled_files), 1)
|
eq_(len(compiled_files), 1)
|
||||||
self.exclude_list.remove(regex2)
|
self.exclude_list.remove(regex2)
|
||||||
assert(regex2 not in self.exclude_list)
|
assert regex2 not in self.exclude_list
|
||||||
eq_(len(self.exclude_list), 1)
|
eq_(len(self.exclude_list), 1)
|
||||||
|
|
||||||
def test_rename_regex_file_to_path(self):
|
def test_rename_regex_file_to_path(self):
|
||||||
@ -371,8 +375,8 @@ class TestCaseDictEmptyUnion(TestCaseDictEmpty):
|
|||||||
compiled = [x for x in self.exclude_list.compiled]
|
compiled = [x for x in self.exclude_list.compiled]
|
||||||
assert regex not in compiled
|
assert regex not in compiled
|
||||||
# Need to escape both to get the same strings after compilation
|
# Need to escape both to get the same strings after compilation
|
||||||
compiled_escaped = set([x.encode('unicode-escape').decode() for x in compiled[0].pattern.split("|")])
|
compiled_escaped = set([x.encode("unicode-escape").decode() for x in compiled[0].pattern.split("|")])
|
||||||
default_escaped = set([x.encode('unicode-escape').decode() for x in default_regexes])
|
default_escaped = set([x.encode("unicode-escape").decode() for x in default_regexes])
|
||||||
assert compiled_escaped == default_escaped
|
assert compiled_escaped == default_escaped
|
||||||
eq_(len(default_regexes), len(compiled[0].pattern.split("|")))
|
eq_(len(default_regexes), len(compiled[0].pattern.split("|")))
|
||||||
|
|
||||||
@ -382,8 +386,9 @@ def split_union(pattern_object):
|
|||||||
return [x for x in pattern_object.pattern.split("|")]
|
return [x for x in pattern_object.pattern.split("|")]
|
||||||
|
|
||||||
|
|
||||||
class TestCaseCompiledList():
|
class TestCaseCompiledList:
|
||||||
"""Test consistency between union or and separate versions."""
|
"""Test consistency between union or and separate versions."""
|
||||||
|
|
||||||
def setup_method(self, method):
|
def setup_method(self, method):
|
||||||
self.e_separate = ExcludeList(union_regex=False)
|
self.e_separate = ExcludeList(union_regex=False)
|
||||||
self.e_separate.restore_defaults()
|
self.e_separate.restore_defaults()
|
||||||
@ -431,6 +436,7 @@ class TestCaseCompiledList():
|
|||||||
|
|
||||||
class TestCaseCompiledDict(TestCaseCompiledList):
|
class TestCaseCompiledDict(TestCaseCompiledList):
|
||||||
"""Test the dictionary version"""
|
"""Test the dictionary version"""
|
||||||
|
|
||||||
def setup_method(self, method):
|
def setup_method(self, method):
|
||||||
self.e_separate = ExcludeDict(union_regex=False)
|
self.e_separate = ExcludeDict(union_regex=False)
|
||||||
self.e_separate.restore_defaults()
|
self.e_separate.restore_defaults()
|
||||||
|
@ -73,9 +73,7 @@ def test_save_to_xml():
|
|||||||
eq_(len(root), 2)
|
eq_(len(root), 2)
|
||||||
eq_(len([c for c in root if c.tag == "file"]), 2)
|
eq_(len([c for c in root if c.tag == "file"]), 2)
|
||||||
f1, f2 = root[:]
|
f1, f2 = root[:]
|
||||||
subchildren = [c for c in f1 if c.tag == "file"] + [
|
subchildren = [c for c in f1 if c.tag == "file"] + [c for c in f2 if c.tag == "file"]
|
||||||
c for c in f2 if c.tag == "file"
|
|
||||||
]
|
|
||||||
eq_(len(subchildren), 3)
|
eq_(len(subchildren), 3)
|
||||||
|
|
||||||
|
|
||||||
@ -96,9 +94,7 @@ def test_SaveThenLoad():
|
|||||||
|
|
||||||
def test_LoadXML_with_empty_file_tags():
|
def test_LoadXML_with_empty_file_tags():
|
||||||
f = io.BytesIO()
|
f = io.BytesIO()
|
||||||
f.write(
|
f.write(b'<?xml version="1.0" encoding="utf-8"?><ignore_list><file><file/></file></ignore_list>')
|
||||||
b'<?xml version="1.0" encoding="utf-8"?><ignore_list><file><file/></file></ignore_list>'
|
|
||||||
)
|
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
il = IgnoreList()
|
il = IgnoreList()
|
||||||
il.load_from_xml(f)
|
il.load_from_xml(f)
|
||||||
|
@ -117,9 +117,7 @@ class TestCaseResultsWithSomeGroups:
|
|||||||
assert d is g.ref
|
assert d is g.ref
|
||||||
|
|
||||||
def test_sort_groups(self):
|
def test_sort_groups(self):
|
||||||
self.results.make_ref(
|
self.results.make_ref(self.objects[1]) # We want to make the 1024 sized object to go ref.
|
||||||
self.objects[1]
|
|
||||||
) # We want to make the 1024 sized object to go ref.
|
|
||||||
g1, g2 = self.groups
|
g1, g2 = self.groups
|
||||||
self.results.sort_groups("size")
|
self.results.sort_groups("size")
|
||||||
assert self.results.groups[0] is g2
|
assert self.results.groups[0] is g2
|
||||||
@ -129,9 +127,7 @@ class TestCaseResultsWithSomeGroups:
|
|||||||
assert self.results.groups[1] is g2
|
assert self.results.groups[1] is g2
|
||||||
|
|
||||||
def test_set_groups_when_sorted(self):
|
def test_set_groups_when_sorted(self):
|
||||||
self.results.make_ref(
|
self.results.make_ref(self.objects[1]) # We want to make the 1024 sized object to go ref.
|
||||||
self.objects[1]
|
|
||||||
) # We want to make the 1024 sized object to go ref.
|
|
||||||
self.results.sort_groups("size")
|
self.results.sort_groups("size")
|
||||||
objects, matches, groups = GetTestGroups()
|
objects, matches, groups = GetTestGroups()
|
||||||
g1, g2 = groups
|
g1, g2 = groups
|
||||||
@ -601,9 +597,7 @@ class TestCaseResultsXML:
|
|||||||
matches = engine.getmatches(objects) # we should have 5 matches
|
matches = engine.getmatches(objects) # we should have 5 matches
|
||||||
groups = engine.get_groups(matches) # We should have 2 groups
|
groups = engine.get_groups(matches) # We should have 2 groups
|
||||||
for g in groups:
|
for g in groups:
|
||||||
g.prioritize(
|
g.prioritize(lambda x: objects.index(x)) # We want the dupes to be in the same order as the list is
|
||||||
lambda x: objects.index(x)
|
|
||||||
) # We want the dupes to be in the same order as the list is
|
|
||||||
app = DupeGuru()
|
app = DupeGuru()
|
||||||
results = Results(app)
|
results = Results(app)
|
||||||
results.groups = groups
|
results.groups = groups
|
||||||
@ -807,9 +801,7 @@ class TestCaseResultsFilter:
|
|||||||
# Now the stats should display *2* markable dupes (instead of 1)
|
# Now the stats should display *2* markable dupes (instead of 1)
|
||||||
expected = "0 / 2 (0.00 B / 2.00 B) duplicates marked. filter: foo"
|
expected = "0 / 2 (0.00 B / 2.00 B) duplicates marked. filter: foo"
|
||||||
eq_(expected, self.results.stat_line)
|
eq_(expected, self.results.stat_line)
|
||||||
self.results.apply_filter(
|
self.results.apply_filter(None) # Now let's make sure our unfiltered results aren't fucked up
|
||||||
None
|
|
||||||
) # Now let's make sure our unfiltered results aren't fucked up
|
|
||||||
expected = "0 / 3 (0.00 B / 3.00 B) duplicates marked."
|
expected = "0 / 3 (0.00 B / 3.00 B) duplicates marked."
|
||||||
eq_(expected, self.results.stat_line)
|
eq_(expected, self.results.stat_line)
|
||||||
|
|
||||||
|
@ -150,8 +150,7 @@ def test_big_file_partial_hashes(fake_fileexists):
|
|||||||
bigsize = 100 * 1024 * 1024 # 100MB
|
bigsize = 100 * 1024 * 1024 # 100MB
|
||||||
s.big_file_size_threshold = bigsize
|
s.big_file_size_threshold = bigsize
|
||||||
|
|
||||||
f = [no("bigfoo", bigsize), no("bigbar", bigsize),
|
f = [no("bigfoo", bigsize), no("bigbar", bigsize), no("smallfoo", smallsize), no("smallbar", smallsize)]
|
||||||
no("smallfoo", smallsize), no("smallbar", smallsize)]
|
|
||||||
f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
|
f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
|
||||||
f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
|
f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
|
||||||
f[2].md5 = f[2].md5partial = "bleh"
|
f[2].md5 = f[2].md5partial = "bleh"
|
||||||
@ -193,10 +192,8 @@ def test_content_scan_doesnt_put_md5_in_words_at_the_end(fake_fileexists):
|
|||||||
s = Scanner()
|
s = Scanner()
|
||||||
s.scan_type = ScanType.Contents
|
s.scan_type = ScanType.Contents
|
||||||
f = [no("foo"), no("bar")]
|
f = [no("foo"), no("bar")]
|
||||||
f[0].md5 = f[0].md5partial = f[0].md5samples =\
|
f[0].md5 = f[0].md5partial = f[0].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
|
||||||
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
|
f[1].md5 = f[1].md5partial = f[1].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
|
||||||
f[1].md5 = f[1].md5partial = f[1].md5samples =\
|
|
||||||
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
|
|
||||||
r = s.get_dupe_groups(f)
|
r = s.get_dupe_groups(f)
|
||||||
# FIXME looks like we are missing something here?
|
# FIXME looks like we are missing something here?
|
||||||
r[0]
|
r[0]
|
||||||
|
@ -11,9 +11,7 @@ from setuptools import setup, Extension
|
|||||||
|
|
||||||
def get_parser():
|
def get_parser():
|
||||||
parser = argparse.ArgumentParser(description="Build an arbitrary Python extension.")
|
parser = argparse.ArgumentParser(description="Build an arbitrary Python extension.")
|
||||||
parser.add_argument(
|
parser.add_argument("source_files", nargs="+", help="List of source files to compile")
|
||||||
"source_files", nargs="+", help="List of source files to compile"
|
|
||||||
)
|
|
||||||
parser.add_argument("name", nargs=1, help="Name of the resulting extension")
|
parser.add_argument("name", nargs=1, help="Name of the resulting extension")
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
@ -23,7 +21,8 @@ def main():
|
|||||||
print("Building {}...".format(args.name[0]))
|
print("Building {}...".format(args.name[0]))
|
||||||
ext = Extension(args.name[0], args.source_files)
|
ext = Extension(args.name[0], args.source_files)
|
||||||
setup(
|
setup(
|
||||||
script_args=["build_ext", "--inplace"], ext_modules=[ext],
|
script_args=["build_ext", "--inplace"],
|
||||||
|
ext_modules=[ext],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,15 +48,13 @@ def get_unconflicted_name(name):
|
|||||||
|
|
||||||
|
|
||||||
def is_conflicted(name):
|
def is_conflicted(name):
|
||||||
"""Returns whether ``name`` is prepended with a bracketed number.
|
"""Returns whether ``name`` is prepended with a bracketed number."""
|
||||||
"""
|
|
||||||
return re_conflict.match(name) is not None
|
return re_conflict.match(name) is not None
|
||||||
|
|
||||||
|
|
||||||
@pathify
|
@pathify
|
||||||
def _smart_move_or_copy(operation, source_path: Path, dest_path: Path):
|
def _smart_move_or_copy(operation, source_path: Path, dest_path: Path):
|
||||||
"""Use move() or copy() to move and copy file with the conflict management.
|
"""Use move() or copy() to move and copy file with the conflict management."""
|
||||||
"""
|
|
||||||
if dest_path.isdir() and not source_path.isdir():
|
if dest_path.isdir() and not source_path.isdir():
|
||||||
dest_path = dest_path[source_path.name]
|
dest_path = dest_path[source_path.name]
|
||||||
if dest_path.exists():
|
if dest_path.exists():
|
||||||
@ -68,14 +66,12 @@ def _smart_move_or_copy(operation, source_path: Path, dest_path: Path):
|
|||||||
|
|
||||||
|
|
||||||
def smart_move(source_path, dest_path):
|
def smart_move(source_path, dest_path):
|
||||||
"""Same as :func:`smart_copy`, but it moves files instead.
|
"""Same as :func:`smart_copy`, but it moves files instead."""
|
||||||
"""
|
|
||||||
_smart_move_or_copy(shutil.move, source_path, dest_path)
|
_smart_move_or_copy(shutil.move, source_path, dest_path)
|
||||||
|
|
||||||
|
|
||||||
def smart_copy(source_path, dest_path):
|
def smart_copy(source_path, dest_path):
|
||||||
"""Copies ``source_path`` to ``dest_path``, recursively and with conflict resolution.
|
"""Copies ``source_path`` to ``dest_path``, recursively and with conflict resolution."""
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
_smart_move_or_copy(shutil.copy, source_path, dest_path)
|
_smart_move_or_copy(shutil.copy, source_path, dest_path)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
|
@ -16,20 +16,17 @@ class SpecialFolder:
|
|||||||
|
|
||||||
|
|
||||||
def open_url(url):
|
def open_url(url):
|
||||||
"""Open ``url`` with the default browser.
|
"""Open ``url`` with the default browser."""
|
||||||
"""
|
|
||||||
_open_url(url)
|
_open_url(url)
|
||||||
|
|
||||||
|
|
||||||
def open_path(path):
|
def open_path(path):
|
||||||
"""Open ``path`` with its associated application.
|
"""Open ``path`` with its associated application."""
|
||||||
"""
|
|
||||||
_open_path(str(path))
|
_open_path(str(path))
|
||||||
|
|
||||||
|
|
||||||
def reveal_path(path):
|
def reveal_path(path):
|
||||||
"""Open the folder containing ``path`` with the default file browser.
|
"""Open the folder containing ``path`` with the default file browser."""
|
||||||
"""
|
|
||||||
_reveal_path(str(path))
|
_reveal_path(str(path))
|
||||||
|
|
||||||
|
|
||||||
|
@ -149,8 +149,7 @@ class Rect:
|
|||||||
return l1, l2, l3, l4
|
return l1, l2, l3, l4
|
||||||
|
|
||||||
def scaled_rect(self, dx, dy):
|
def scaled_rect(self, dx, dy):
|
||||||
"""Returns a rect that has the same borders at self, but grown/shrunk by dx/dy on each side.
|
"""Returns a rect that has the same borders at self, but grown/shrunk by dx/dy on each side."""
|
||||||
"""
|
|
||||||
x, y, w, h = self
|
x, y, w, h = self
|
||||||
x -= dx
|
x -= dx
|
||||||
y -= dy
|
y -= dy
|
||||||
@ -159,8 +158,7 @@ class Rect:
|
|||||||
return Rect(x, y, w, h)
|
return Rect(x, y, w, h)
|
||||||
|
|
||||||
def united(self, other):
|
def united(self, other):
|
||||||
"""Returns the bounding rectangle of this rectangle and `other`.
|
"""Returns the bounding rectangle of this rectangle and `other`."""
|
||||||
"""
|
|
||||||
# ul=upper left lr=lower right
|
# ul=upper left lr=lower right
|
||||||
ulcorner1, lrcorner1 = self.corners()
|
ulcorner1, lrcorner1 = self.corners()
|
||||||
ulcorner2, lrcorner2 = other.corners()
|
ulcorner2, lrcorner2 = other.corners()
|
||||||
|
@ -80,8 +80,7 @@ class PrefAccessInterface:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def set_default(self, key, value):
|
def set_default(self, key, value):
|
||||||
"""Set the value ``value`` for ``key`` in the currently running app's preference store.
|
"""Set the value ``value`` for ``key`` in the currently running app's preference store."""
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Columns(GUIObject):
|
class Columns(GUIObject):
|
||||||
@ -140,33 +139,27 @@ class Columns(GUIObject):
|
|||||||
|
|
||||||
# --- Public
|
# --- Public
|
||||||
def column_by_index(self, index):
|
def column_by_index(self, index):
|
||||||
"""Return the :class:`Column` having the :attr:`~Column.logical_index` ``index``.
|
"""Return the :class:`Column` having the :attr:`~Column.logical_index` ``index``."""
|
||||||
"""
|
|
||||||
return self.column_list[index]
|
return self.column_list[index]
|
||||||
|
|
||||||
def column_by_name(self, name):
|
def column_by_name(self, name):
|
||||||
"""Return the :class:`Column` having the :attr:`~Column.name` ``name``.
|
"""Return the :class:`Column` having the :attr:`~Column.name` ``name``."""
|
||||||
"""
|
|
||||||
return self.coldata[name]
|
return self.coldata[name]
|
||||||
|
|
||||||
def columns_count(self):
|
def columns_count(self):
|
||||||
"""Returns the number of columns in our set.
|
"""Returns the number of columns in our set."""
|
||||||
"""
|
|
||||||
return len(self.column_list)
|
return len(self.column_list)
|
||||||
|
|
||||||
def column_display(self, colname):
|
def column_display(self, colname):
|
||||||
"""Returns display name for column named ``colname``, or ``''`` if there's none.
|
"""Returns display name for column named ``colname``, or ``''`` if there's none."""
|
||||||
"""
|
|
||||||
return self._get_colname_attr(colname, "display", "")
|
return self._get_colname_attr(colname, "display", "")
|
||||||
|
|
||||||
def column_is_visible(self, colname):
|
def column_is_visible(self, colname):
|
||||||
"""Returns visibility for column named ``colname``, or ``True`` if there's none.
|
"""Returns visibility for column named ``colname``, or ``True`` if there's none."""
|
||||||
"""
|
|
||||||
return self._get_colname_attr(colname, "visible", True)
|
return self._get_colname_attr(colname, "visible", True)
|
||||||
|
|
||||||
def column_width(self, colname):
|
def column_width(self, colname):
|
||||||
"""Returns width for column named ``colname``, or ``0`` if there's none.
|
"""Returns width for column named ``colname``, or ``0`` if there's none."""
|
||||||
"""
|
|
||||||
return self._get_colname_attr(colname, "width", 0)
|
return self._get_colname_attr(colname, "width", 0)
|
||||||
|
|
||||||
def columns_to_right(self, colname):
|
def columns_to_right(self, colname):
|
||||||
@ -177,11 +170,7 @@ class Columns(GUIObject):
|
|||||||
"""
|
"""
|
||||||
column = self.coldata[colname]
|
column = self.coldata[colname]
|
||||||
index = column.ordered_index
|
index = column.ordered_index
|
||||||
return [
|
return [col.name for col in self.column_list if (col.visible and col.ordered_index > index)]
|
||||||
col.name
|
|
||||||
for col in self.column_list
|
|
||||||
if (col.visible and col.ordered_index > index)
|
|
||||||
]
|
|
||||||
|
|
||||||
def menu_items(self):
|
def menu_items(self):
|
||||||
"""Returns a list of items convenient for quick visibility menu generation.
|
"""Returns a list of items convenient for quick visibility menu generation.
|
||||||
@ -207,8 +196,7 @@ class Columns(GUIObject):
|
|||||||
self.set_column_order(colnames)
|
self.set_column_order(colnames)
|
||||||
|
|
||||||
def reset_to_defaults(self):
|
def reset_to_defaults(self):
|
||||||
"""Reset all columns' width and visibility to their default values.
|
"""Reset all columns' width and visibility to their default values."""
|
||||||
"""
|
|
||||||
self.set_column_order([col.name for col in self.column_list])
|
self.set_column_order([col.name for col in self.column_list])
|
||||||
for col in self._optional_columns():
|
for col in self._optional_columns():
|
||||||
col.visible = col.default_visible
|
col.visible = col.default_visible
|
||||||
@ -216,13 +204,11 @@ class Columns(GUIObject):
|
|||||||
self.view.restore_columns()
|
self.view.restore_columns()
|
||||||
|
|
||||||
def resize_column(self, colname, newwidth):
|
def resize_column(self, colname, newwidth):
|
||||||
"""Set column ``colname``'s width to ``newwidth``.
|
"""Set column ``colname``'s width to ``newwidth``."""
|
||||||
"""
|
|
||||||
self._set_colname_attr(colname, "width", newwidth)
|
self._set_colname_attr(colname, "width", newwidth)
|
||||||
|
|
||||||
def restore_columns(self):
|
def restore_columns(self):
|
||||||
"""Restore's column persistent attributes from the last :meth:`save_columns`.
|
"""Restore's column persistent attributes from the last :meth:`save_columns`."""
|
||||||
"""
|
|
||||||
if not (self.prefaccess and self.savename and self.coldata):
|
if not (self.prefaccess and self.savename and self.coldata):
|
||||||
if (not self.savename) and (self.coldata):
|
if (not self.savename) and (self.coldata):
|
||||||
# This is a table that will not have its coldata saved/restored. we should
|
# This is a table that will not have its coldata saved/restored. we should
|
||||||
@ -241,8 +227,7 @@ class Columns(GUIObject):
|
|||||||
self.view.restore_columns()
|
self.view.restore_columns()
|
||||||
|
|
||||||
def save_columns(self):
|
def save_columns(self):
|
||||||
"""Save column attributes in persistent storage for restoration in :meth:`restore_columns`.
|
"""Save column attributes in persistent storage for restoration in :meth:`restore_columns`."""
|
||||||
"""
|
|
||||||
if not (self.prefaccess and self.savename and self.coldata):
|
if not (self.prefaccess and self.savename and self.coldata):
|
||||||
return
|
return
|
||||||
for col in self.column_list:
|
for col in self.column_list:
|
||||||
@ -263,15 +248,13 @@ class Columns(GUIObject):
|
|||||||
col.ordered_index = i
|
col.ordered_index = i
|
||||||
|
|
||||||
def set_column_visible(self, colname, visible):
|
def set_column_visible(self, colname, visible):
|
||||||
"""Set the visibility of column ``colname``.
|
"""Set the visibility of column ``colname``."""
|
||||||
"""
|
|
||||||
self.table.save_edits() # the table on the GUI side will stop editing when the columns change
|
self.table.save_edits() # the table on the GUI side will stop editing when the columns change
|
||||||
self._set_colname_attr(colname, "visible", visible)
|
self._set_colname_attr(colname, "visible", visible)
|
||||||
self.view.set_column_visible(colname, visible)
|
self.view.set_column_visible(colname, visible)
|
||||||
|
|
||||||
def set_default_width(self, colname, width):
|
def set_default_width(self, colname, width):
|
||||||
"""Set the default width or column ``colname``.
|
"""Set the default width or column ``colname``."""
|
||||||
"""
|
|
||||||
self._set_colname_attr(colname, "default_width", width)
|
self._set_colname_attr(colname, "default_width", width)
|
||||||
|
|
||||||
def toggle_menu_item(self, index):
|
def toggle_menu_item(self, index):
|
||||||
@ -289,14 +272,10 @@ class Columns(GUIObject):
|
|||||||
# --- Properties
|
# --- Properties
|
||||||
@property
|
@property
|
||||||
def ordered_columns(self):
|
def ordered_columns(self):
|
||||||
"""List of :class:`Column` in visible order.
|
"""List of :class:`Column` in visible order."""
|
||||||
"""
|
return [col for col in sorted(self.column_list, key=lambda col: col.ordered_index)]
|
||||||
return [
|
|
||||||
col for col in sorted(self.column_list, key=lambda col: col.ordered_index)
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def colnames(self):
|
def colnames(self):
|
||||||
"""List of column names in visible order.
|
"""List of column names in visible order."""
|
||||||
"""
|
|
||||||
return [col.name for col in self.ordered_columns]
|
return [col.name for col in self.ordered_columns]
|
||||||
|
@ -21,12 +21,10 @@ class ProgressWindowView:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def show(self):
|
def show(self):
|
||||||
"""Show the dialog.
|
"""Show the dialog."""
|
||||||
"""
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Close the dialog.
|
"""Close the dialog."""
|
||||||
"""
|
|
||||||
|
|
||||||
def set_progress(self, progress):
|
def set_progress(self, progress):
|
||||||
"""Set the progress of the progress bar to ``progress``.
|
"""Set the progress of the progress bar to ``progress``.
|
||||||
@ -76,8 +74,7 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
|||||||
self.jobid = None
|
self.jobid = None
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
"""Call for a user-initiated job cancellation.
|
"""Call for a user-initiated job cancellation."""
|
||||||
"""
|
|
||||||
# The UI is sometimes a bit buggy and calls cancel() on self.view.close(). We just want to
|
# The UI is sometimes a bit buggy and calls cancel() on self.view.close(). We just want to
|
||||||
# make sure that this doesn't lead us to think that the user acually cancelled the task, so
|
# make sure that this doesn't lead us to think that the user acually cancelled the task, so
|
||||||
# we verify that the job is still running.
|
# we verify that the job is still running.
|
||||||
|
@ -27,9 +27,7 @@ class Selectable(Sequence):
|
|||||||
self._selected_indexes = []
|
self._selected_indexes = []
|
||||||
if not self._selected_indexes:
|
if not self._selected_indexes:
|
||||||
return
|
return
|
||||||
self._selected_indexes = [
|
self._selected_indexes = [index for index in self._selected_indexes if index < len(self)]
|
||||||
index for index in self._selected_indexes if index < len(self)
|
|
||||||
]
|
|
||||||
if not self._selected_indexes:
|
if not self._selected_indexes:
|
||||||
self._selected_indexes = [len(self) - 1]
|
self._selected_indexes = [len(self) - 1]
|
||||||
|
|
||||||
|
@ -71,8 +71,7 @@ class TextField(GUIObject):
|
|||||||
|
|
||||||
# --- Public
|
# --- Public
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
"""Triggers a view :meth:`~TextFieldView.refresh`.
|
"""Triggers a view :meth:`~TextFieldView.refresh`."""
|
||||||
"""
|
|
||||||
self.view.refresh()
|
self.view.refresh()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -55,8 +55,7 @@ class Node(MutableSequence):
|
|||||||
|
|
||||||
# --- Public
|
# --- Public
|
||||||
def clear(self):
|
def clear(self):
|
||||||
"""Clears the node of all its children.
|
"""Clears the node of all its children."""
|
||||||
"""
|
|
||||||
del self[:]
|
del self[:]
|
||||||
|
|
||||||
def find(self, predicate, include_self=True):
|
def find(self, predicate, include_self=True):
|
||||||
@ -103,14 +102,12 @@ class Node(MutableSequence):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def children_count(self):
|
def children_count(self):
|
||||||
"""Same as ``len(self)``.
|
"""Same as ``len(self)``."""
|
||||||
"""
|
|
||||||
return len(self)
|
return len(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Name for the node, supplied on init.
|
"""Name for the node, supplied on init."""
|
||||||
"""
|
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -56,8 +56,7 @@ class Job:
|
|||||||
|
|
||||||
# ---Private
|
# ---Private
|
||||||
def _subjob_callback(self, progress, desc=""):
|
def _subjob_callback(self, progress, desc=""):
|
||||||
"""This is the callback passed to children jobs.
|
"""This is the callback passed to children jobs."""
|
||||||
"""
|
|
||||||
self.set_progress(progress, desc)
|
self.set_progress(progress, desc)
|
||||||
return True # if JobCancelled has to be raised, it will be at the highest level
|
return True # if JobCancelled has to be raised, it will be at the highest level
|
||||||
|
|
||||||
|
@ -154,9 +154,7 @@ def strings2pot(target, dest):
|
|||||||
def allstrings2pot(lprojpath, dest, excludes=None):
|
def allstrings2pot(lprojpath, dest, excludes=None):
|
||||||
allstrings = files_with_ext(lprojpath, ".strings")
|
allstrings = files_with_ext(lprojpath, ".strings")
|
||||||
if excludes:
|
if excludes:
|
||||||
allstrings = [
|
allstrings = [p for p in allstrings if op.splitext(op.basename(p))[0] not in excludes]
|
||||||
p for p in allstrings if op.splitext(op.basename(p))[0] not in excludes
|
|
||||||
]
|
|
||||||
for strings_path in allstrings:
|
for strings_path in allstrings:
|
||||||
strings2pot(strings_path, dest)
|
strings2pot(strings_path, dest)
|
||||||
|
|
||||||
@ -195,11 +193,7 @@ def generate_cocoa_strings_from_code(code_folder, dest_folder):
|
|||||||
# genstrings produces utf-16 files with comments. After having generated the files, we convert
|
# genstrings produces utf-16 files with comments. After having generated the files, we convert
|
||||||
# them to utf-8 and remove the comments.
|
# them to utf-8 and remove the comments.
|
||||||
ensure_empty_folder(dest_folder)
|
ensure_empty_folder(dest_folder)
|
||||||
print_and_do(
|
print_and_do('genstrings -o "{}" `find "{}" -name *.m | xargs`'.format(dest_folder, code_folder))
|
||||||
'genstrings -o "{}" `find "{}" -name *.m | xargs`'.format(
|
|
||||||
dest_folder, code_folder
|
|
||||||
)
|
|
||||||
)
|
|
||||||
for stringsfile in os.listdir(dest_folder):
|
for stringsfile in os.listdir(dest_folder):
|
||||||
stringspath = op.join(dest_folder, stringsfile)
|
stringspath = op.join(dest_folder, stringsfile)
|
||||||
with open(stringspath, "rt", encoding="utf-16") as fp:
|
with open(stringspath, "rt", encoding="utf-16") as fp:
|
||||||
@ -214,9 +208,7 @@ def generate_cocoa_strings_from_code(code_folder, dest_folder):
|
|||||||
|
|
||||||
|
|
||||||
def generate_cocoa_strings_from_xib(xib_folder):
|
def generate_cocoa_strings_from_xib(xib_folder):
|
||||||
xibs = [
|
xibs = [op.join(xib_folder, fn) for fn in os.listdir(xib_folder) if fn.endswith(".xib")]
|
||||||
op.join(xib_folder, fn) for fn in os.listdir(xib_folder) if fn.endswith(".xib")
|
|
||||||
]
|
|
||||||
for xib in xibs:
|
for xib in xibs:
|
||||||
dest = xib.replace(".xib", ".strings")
|
dest = xib.replace(".xib", ".strings")
|
||||||
print_and_do("ibtool {} --generate-strings-file {}".format(xib, dest))
|
print_and_do("ibtool {} --generate-strings-file {}".format(xib, dest))
|
||||||
@ -234,10 +226,6 @@ def localize_stringsfile(stringsfile, dest_root_folder):
|
|||||||
|
|
||||||
|
|
||||||
def localize_all_stringsfiles(src_folder, dest_root_folder):
|
def localize_all_stringsfiles(src_folder, dest_root_folder):
|
||||||
stringsfiles = [
|
stringsfiles = [op.join(src_folder, fn) for fn in os.listdir(src_folder) if fn.endswith(".strings")]
|
||||||
op.join(src_folder, fn)
|
|
||||||
for fn in os.listdir(src_folder)
|
|
||||||
if fn.endswith(".strings")
|
|
||||||
]
|
|
||||||
for path in stringsfiles:
|
for path in stringsfiles:
|
||||||
localize_stringsfile(path, dest_root_folder)
|
localize_stringsfile(path, dest_root_folder)
|
||||||
|
@ -16,8 +16,7 @@ from collections import defaultdict
|
|||||||
|
|
||||||
|
|
||||||
class Broadcaster:
|
class Broadcaster:
|
||||||
"""Broadcasts messages that are received by all listeners.
|
"""Broadcasts messages that are received by all listeners."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.listeners = set()
|
self.listeners = set()
|
||||||
@ -39,8 +38,7 @@ class Broadcaster:
|
|||||||
|
|
||||||
|
|
||||||
class Listener:
|
class Listener:
|
||||||
"""A listener is initialized with the broadcaster it's going to listen to. Initially, it is not connected.
|
"""A listener is initialized with the broadcaster it's going to listen to. Initially, it is not connected."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, broadcaster):
|
def __init__(self, broadcaster):
|
||||||
self.broadcaster = broadcaster
|
self.broadcaster = broadcaster
|
||||||
@ -57,13 +55,11 @@ class Listener:
|
|||||||
self._bound_notifications[message].append(func)
|
self._bound_notifications[message].append(func)
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
"""Connects the listener to its broadcaster.
|
"""Connects the listener to its broadcaster."""
|
||||||
"""
|
|
||||||
self.broadcaster.add_listener(self)
|
self.broadcaster.add_listener(self)
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
"""Disconnects the listener from its broadcaster.
|
"""Disconnects the listener from its broadcaster."""
|
||||||
"""
|
|
||||||
self.broadcaster.remove_listener(self)
|
self.broadcaster.remove_listener(self)
|
||||||
|
|
||||||
def dispatch(self, msg):
|
def dispatch(self, msg):
|
||||||
|
@ -85,9 +85,7 @@ class Path(tuple):
|
|||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
if isinstance(key, slice):
|
if isinstance(key, slice):
|
||||||
if isinstance(key.start, Path):
|
if isinstance(key.start, Path):
|
||||||
equal_elems = list(
|
equal_elems = list(takewhile(lambda pair: pair[0] == pair[1], zip(self, key.start)))
|
||||||
takewhile(lambda pair: pair[0] == pair[1], zip(self, key.start))
|
|
||||||
)
|
|
||||||
key = slice(len(equal_elems), key.stop, key.step)
|
key = slice(len(equal_elems), key.stop, key.step)
|
||||||
if isinstance(key.stop, Path):
|
if isinstance(key.stop, Path):
|
||||||
equal_elems = list(
|
equal_elems = list(
|
||||||
@ -226,9 +224,7 @@ def pathify(f):
|
|||||||
Calling ``foo('/bar', 0)`` will convert ``'/bar'`` to ``Path('/bar')``.
|
Calling ``foo('/bar', 0)`` will convert ``'/bar'`` to ``Path('/bar')``.
|
||||||
"""
|
"""
|
||||||
sig = signature(f)
|
sig = signature(f)
|
||||||
pindexes = {
|
pindexes = {i for i, p in enumerate(sig.parameters.values()) if p.annotation is Path}
|
||||||
i for i, p in enumerate(sig.parameters.values()) if p.annotation is Path
|
|
||||||
}
|
|
||||||
pkeys = {k: v for k, v in sig.parameters.items() if v.annotation is Path}
|
pkeys = {k: v for k, v in sig.parameters.items() if v.annotation is Path}
|
||||||
|
|
||||||
def path_or_none(p):
|
def path_or_none(p):
|
||||||
@ -236,9 +232,7 @@ def pathify(f):
|
|||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapped(*args, **kwargs):
|
def wrapped(*args, **kwargs):
|
||||||
args = tuple(
|
args = tuple((path_or_none(a) if i in pindexes else a) for i, a in enumerate(args))
|
||||||
(path_or_none(a) if i in pindexes else a) for i, a in enumerate(args)
|
|
||||||
)
|
|
||||||
kwargs = {k: (path_or_none(v) if k in pkeys else v) for k, v in kwargs.items()}
|
kwargs = {k: (path_or_none(v) if k in pkeys else v) for k, v in kwargs.items()}
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
@ -246,8 +240,7 @@ def pathify(f):
|
|||||||
|
|
||||||
|
|
||||||
def log_io_error(func):
|
def log_io_error(func):
|
||||||
""" Catches OSError, IOError and WindowsError and log them
|
"""Catches OSError, IOError and WindowsError and log them"""
|
||||||
"""
|
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(path, *args, **kwargs):
|
def wrapper(path, *args, **kwargs):
|
||||||
|
@ -110,22 +110,14 @@ def _visit_pyfiles(list, dirname, names):
|
|||||||
# get extension for python source files
|
# get extension for python source files
|
||||||
if "_py_ext" not in globals():
|
if "_py_ext" not in globals():
|
||||||
global _py_ext
|
global _py_ext
|
||||||
_py_ext = [
|
_py_ext = [triple[0] for triple in imp.get_suffixes() if triple[2] == imp.PY_SOURCE][0]
|
||||||
triple[0] for triple in imp.get_suffixes() if triple[2] == imp.PY_SOURCE
|
|
||||||
][0]
|
|
||||||
|
|
||||||
# don't recurse into CVS directories
|
# don't recurse into CVS directories
|
||||||
if "CVS" in names:
|
if "CVS" in names:
|
||||||
names.remove("CVS")
|
names.remove("CVS")
|
||||||
|
|
||||||
# add all *.py files to list
|
# add all *.py files to list
|
||||||
list.extend(
|
list.extend([os.path.join(dirname, file) for file in names if os.path.splitext(file)[1] == _py_ext])
|
||||||
[
|
|
||||||
os.path.join(dirname, file)
|
|
||||||
for file in names
|
|
||||||
if os.path.splitext(file)[1] == _py_ext
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_modpkg_path(dotted_name, pathlist=None):
|
def _get_modpkg_path(dotted_name, pathlist=None):
|
||||||
@ -406,8 +398,7 @@ def main(source_files, outpath, keywords=None):
|
|||||||
eater(*_token)
|
eater(*_token)
|
||||||
except tokenize.TokenError as e:
|
except tokenize.TokenError as e:
|
||||||
print(
|
print(
|
||||||
"%s: %s, line %d, column %d"
|
"%s: %s, line %d, column %d" % (e.args[0], filename, e.args[1][0], e.args[1][1]),
|
||||||
% (e.args[0], filename, e.args[1][0], e.args[1][1]),
|
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
@ -22,9 +22,7 @@ def tixgen(tixurl):
|
|||||||
"""This is a filter *generator*. tixurl is a url pattern for the tix with a {0} placeholder
|
"""This is a filter *generator*. tixurl is a url pattern for the tix with a {0} placeholder
|
||||||
for the tix #
|
for the tix #
|
||||||
"""
|
"""
|
||||||
urlpattern = tixurl.format(
|
urlpattern = tixurl.format("\\1") # will be replaced buy the content of the first group in re
|
||||||
"\\1"
|
|
||||||
) # will be replaced buy the content of the first group in re
|
|
||||||
R = re.compile(r"#(\d+)")
|
R = re.compile(r"#(\d+)")
|
||||||
repl = "`#\\1 <{}>`__".format(urlpattern)
|
repl = "`#\\1 <{}>`__".format(urlpattern)
|
||||||
return lambda text: R.sub(repl, text)
|
return lambda text: R.sub(repl, text)
|
||||||
@ -61,9 +59,7 @@ def gen(
|
|||||||
# The format of the changelog descriptions is in markdown, but since we only use bulled list
|
# The format of the changelog descriptions is in markdown, but since we only use bulled list
|
||||||
# and links, it's not worth depending on the markdown package. A simple regexp suffice.
|
# and links, it's not worth depending on the markdown package. A simple regexp suffice.
|
||||||
description = re.sub(r"\[(.*?)\]\((.*?)\)", "`\\1 <\\2>`__", description)
|
description = re.sub(r"\[(.*?)\]\((.*?)\)", "`\\1 <\\2>`__", description)
|
||||||
rendered = CHANGELOG_FORMAT.format(
|
rendered = CHANGELOG_FORMAT.format(version=log["version"], date=log["date_str"], description=description)
|
||||||
version=log["version"], date=log["date_str"], description=description
|
|
||||||
)
|
|
||||||
rendered_logs.append(rendered)
|
rendered_logs.append(rendered)
|
||||||
confrepl["version"] = changelog[0]["version"]
|
confrepl["version"] = changelog[0]["version"]
|
||||||
changelog_out = op.join(basepath, "changelog.rst")
|
changelog_out = op.join(basepath, "changelog.rst")
|
||||||
@ -75,6 +71,4 @@ def gen(
|
|||||||
try:
|
try:
|
||||||
sphinx_build([basepath, destpath])
|
sphinx_build([basepath, destpath])
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
print(
|
print("Sphinx called sys.exit(), but we're cancelling it because we don't actually want to exit")
|
||||||
"Sphinx called sys.exit(), but we're cancelling it because we don't actually want to exit"
|
|
||||||
)
|
|
||||||
|
@ -80,9 +80,7 @@ class TestCase_move_copy:
|
|||||||
assert self.path["baz"].exists()
|
assert self.path["baz"].exists()
|
||||||
assert not self.path["foo"].exists()
|
assert not self.path["foo"].exists()
|
||||||
|
|
||||||
def test_copy_no_conflict(
|
def test_copy_no_conflict(self, do_setup): # No need to duplicate the rest of the tests... Let's just test on move
|
||||||
self, do_setup
|
|
||||||
): # No need to duplicate the rest of the tests... Let's just test on move
|
|
||||||
smart_copy(self.path + "foo", self.path + "baz")
|
smart_copy(self.path + "foo", self.path + "baz")
|
||||||
assert self.path["baz"].exists()
|
assert self.path["baz"].exists()
|
||||||
assert self.path["foo"].exists()
|
assert self.path["foo"].exists()
|
||||||
|
@ -128,9 +128,7 @@ def test_repeater_with_repeated_notifications():
|
|||||||
r.connect()
|
r.connect()
|
||||||
listener.connect()
|
listener.connect()
|
||||||
b.notify("hello")
|
b.notify("hello")
|
||||||
b.notify(
|
b.notify("foo") # if the repeater repeated this notif, we'd get a crash on HelloListener
|
||||||
"foo"
|
|
||||||
) # if the repeater repeated this notif, we'd get a crash on HelloListener
|
|
||||||
eq_(r.hello_count, 1)
|
eq_(r.hello_count, 1)
|
||||||
eq_(listener.hello_count, 1)
|
eq_(listener.hello_count, 1)
|
||||||
eq_(r.foo_count, 1)
|
eq_(r.foo_count, 1)
|
||||||
|
@ -87,8 +87,7 @@ def test_filename(force_ossep):
|
|||||||
|
|
||||||
|
|
||||||
def test_deal_with_empty_components(force_ossep):
|
def test_deal_with_empty_components(force_ossep):
|
||||||
"""Keep ONLY a leading space, which means we want a leading slash.
|
"""Keep ONLY a leading space, which means we want a leading slash."""
|
||||||
"""
|
|
||||||
eq_("foo//bar", str(Path(("foo", "", "bar"))))
|
eq_("foo//bar", str(Path(("foo", "", "bar"))))
|
||||||
eq_("/foo/bar", str(Path(("", "foo", "bar"))))
|
eq_("/foo/bar", str(Path(("", "foo", "bar"))))
|
||||||
eq_("foo/bar", str(Path("foo/bar/")))
|
eq_("foo/bar", str(Path("foo/bar/")))
|
||||||
@ -154,8 +153,7 @@ def test_path_slice(force_ossep):
|
|||||||
|
|
||||||
|
|
||||||
def test_add_with_root_path(force_ossep):
|
def test_add_with_root_path(force_ossep):
|
||||||
"""if I perform /a/b/c + /d/e/f, I want /a/b/c/d/e/f, not /a/b/c//d/e/f
|
"""if I perform /a/b/c + /d/e/f, I want /a/b/c/d/e/f, not /a/b/c//d/e/f"""
|
||||||
"""
|
|
||||||
eq_("/foo/bar", str(Path("/foo") + Path("/bar")))
|
eq_("/foo/bar", str(Path("/foo") + Path("/bar")))
|
||||||
|
|
||||||
|
|
||||||
@ -166,8 +164,7 @@ def test_create_with_tuple_that_have_slash_inside(force_ossep, monkeypatch):
|
|||||||
|
|
||||||
|
|
||||||
def test_auto_decode_os_sep(force_ossep, monkeypatch):
|
def test_auto_decode_os_sep(force_ossep, monkeypatch):
|
||||||
"""Path should decode any either / or os.sep, but always encode in os.sep.
|
"""Path should decode any either / or os.sep, but always encode in os.sep."""
|
||||||
"""
|
|
||||||
eq_(("foo\\bar", "bleh"), Path("foo\\bar/bleh"))
|
eq_(("foo\\bar", "bleh"), Path("foo\\bar/bleh"))
|
||||||
monkeypatch.setattr(os, "sep", "\\")
|
monkeypatch.setattr(os, "sep", "\\")
|
||||||
eq_(("foo", "bar/bleh"), Path("foo\\bar/bleh"))
|
eq_(("foo", "bar/bleh"), Path("foo\\bar/bleh"))
|
||||||
|
@ -44,9 +44,7 @@ def test_guicalls():
|
|||||||
# A GUISelectableList appropriately calls its view.
|
# A GUISelectableList appropriately calls its view.
|
||||||
sl = GUISelectableList(["foo", "bar"])
|
sl = GUISelectableList(["foo", "bar"])
|
||||||
sl.view = CallLogger()
|
sl.view = CallLogger()
|
||||||
sl.view.check_gui_calls(
|
sl.view.check_gui_calls(["refresh"]) # Upon setting the view, we get a call to refresh()
|
||||||
["refresh"]
|
|
||||||
) # Upon setting the view, we get a call to refresh()
|
|
||||||
sl[1] = "baz"
|
sl[1] = "baz"
|
||||||
sl.view.check_gui_calls(["refresh"])
|
sl.view.check_gui_calls(["refresh"])
|
||||||
sl.append("foo")
|
sl.append("foo")
|
||||||
|
@ -105,9 +105,7 @@ def test_findall_dont_include_self():
|
|||||||
# When calling findall with include_self=False, the node itself is never evaluated.
|
# When calling findall with include_self=False, the node itself is never evaluated.
|
||||||
t = tree_with_some_nodes()
|
t = tree_with_some_nodes()
|
||||||
del t._name # so that if the predicate is called on `t`, we crash
|
del t._name # so that if the predicate is called on `t`, we crash
|
||||||
r = t.findall(
|
r = t.findall(lambda n: not n.name.startswith("sub"), include_self=False) # no crash
|
||||||
lambda n: not n.name.startswith("sub"), include_self=False
|
|
||||||
) # no crash
|
|
||||||
eq_(set(r), set([t[0], t[1], t[2]]))
|
eq_(set(r), set([t[0], t[1], t[2]]))
|
||||||
|
|
||||||
|
|
||||||
|
@ -105,9 +105,7 @@ def test_iterconsume():
|
|||||||
# We just want to make sure that we return *all* items and that we're not mistakenly skipping
|
# We just want to make sure that we return *all* items and that we're not mistakenly skipping
|
||||||
# one.
|
# one.
|
||||||
eq_(list(range(2500)), list(iterconsume(list(range(2500)))))
|
eq_(list(range(2500)), list(iterconsume(list(range(2500)))))
|
||||||
eq_(
|
eq_(list(reversed(range(2500))), list(iterconsume(list(range(2500)), reverse=False)))
|
||||||
list(reversed(range(2500))), list(iterconsume(list(range(2500)), reverse=False))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# --- String
|
# --- String
|
||||||
|
@ -86,9 +86,7 @@ class CallLogger:
|
|||||||
eq_(set(self.calls), set(expected))
|
eq_(set(self.calls), set(expected))
|
||||||
self.clear_calls()
|
self.clear_calls()
|
||||||
|
|
||||||
def check_gui_calls_partial(
|
def check_gui_calls_partial(self, expected=None, not_expected=None, verify_order=False):
|
||||||
self, expected=None, not_expected=None, verify_order=False
|
|
||||||
):
|
|
||||||
"""Checks that the expected calls have been made to 'self', then clears the log.
|
"""Checks that the expected calls have been made to 'self', then clears the log.
|
||||||
|
|
||||||
`expected` is an iterable of strings representing method names. Order doesn't matter.
|
`expected` is an iterable of strings representing method names. Order doesn't matter.
|
||||||
@ -99,25 +97,17 @@ class CallLogger:
|
|||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
if expected is not None:
|
if expected is not None:
|
||||||
not_called = set(expected) - set(self.calls)
|
not_called = set(expected) - set(self.calls)
|
||||||
assert not not_called, "These calls haven't been made: {0}".format(
|
assert not not_called, "These calls haven't been made: {0}".format(not_called)
|
||||||
not_called
|
|
||||||
)
|
|
||||||
if verify_order:
|
if verify_order:
|
||||||
max_index = 0
|
max_index = 0
|
||||||
for call in expected:
|
for call in expected:
|
||||||
index = self.calls.index(call)
|
index = self.calls.index(call)
|
||||||
if index < max_index:
|
if index < max_index:
|
||||||
raise AssertionError(
|
raise AssertionError("The call {0} hasn't been made in the correct order".format(call))
|
||||||
"The call {0} hasn't been made in the correct order".format(
|
|
||||||
call
|
|
||||||
)
|
|
||||||
)
|
|
||||||
max_index = index
|
max_index = index
|
||||||
if not_expected is not None:
|
if not_expected is not None:
|
||||||
called = set(not_expected) & set(self.calls)
|
called = set(not_expected) & set(self.calls)
|
||||||
assert not called, "These calls shouldn't have been made: {0}".format(
|
assert not called, "These calls shouldn't have been made: {0}".format(called)
|
||||||
called
|
|
||||||
)
|
|
||||||
self.clear_calls()
|
self.clear_calls()
|
||||||
|
|
||||||
|
|
||||||
@ -211,9 +201,7 @@ def _unify_args(func, args, kwargs, args_to_ignore=None):
|
|||||||
result = kwargs.copy()
|
result = kwargs.copy()
|
||||||
if hasattr(func, "__code__"): # built-in functions don't have func_code
|
if hasattr(func, "__code__"): # built-in functions don't have func_code
|
||||||
args = list(args)
|
args = list(args)
|
||||||
if (
|
if getattr(func, "__self__", None) is not None: # bound method, we have to add self to args list
|
||||||
getattr(func, "__self__", None) is not None
|
|
||||||
): # bound method, we have to add self to args list
|
|
||||||
args = [func.__self__] + args
|
args = [func.__self__] + args
|
||||||
defaults = list(func.__defaults__) if func.__defaults__ is not None else []
|
defaults = list(func.__defaults__) if func.__defaults__ is not None else []
|
||||||
arg_count = func.__code__.co_argcount
|
arg_count = func.__code__.co_argcount
|
||||||
|
@ -110,9 +110,7 @@ def install_gettext_trans(base_folder, lang):
|
|||||||
if not lang:
|
if not lang:
|
||||||
return lambda s: s
|
return lambda s: s
|
||||||
try:
|
try:
|
||||||
return gettext.translation(
|
return gettext.translation(domain, localedir=base_folder, languages=[lang]).gettext
|
||||||
domain, localedir=base_folder, languages=[lang]
|
|
||||||
).gettext
|
|
||||||
except IOError:
|
except IOError:
|
||||||
return lambda s: s
|
return lambda s: s
|
||||||
|
|
||||||
|
@ -19,8 +19,7 @@ from .path import Path, pathify, log_io_error
|
|||||||
|
|
||||||
|
|
||||||
def nonone(value, replace_value):
|
def nonone(value, replace_value):
|
||||||
"""Returns ``value`` if ``value`` is not ``None``. Returns ``replace_value`` otherwise.
|
"""Returns ``value`` if ``value`` is not ``None``. Returns ``replace_value`` otherwise."""
|
||||||
"""
|
|
||||||
if value is None:
|
if value is None:
|
||||||
return replace_value
|
return replace_value
|
||||||
else:
|
else:
|
||||||
@ -28,8 +27,7 @@ def nonone(value, replace_value):
|
|||||||
|
|
||||||
|
|
||||||
def tryint(value, default=0):
|
def tryint(value, default=0):
|
||||||
"""Tries to convert ``value`` to in ``int`` and returns ``default`` if it fails.
|
"""Tries to convert ``value`` to in ``int`` and returns ``default`` if it fails."""
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
return int(value)
|
return int(value)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
@ -37,8 +35,7 @@ def tryint(value, default=0):
|
|||||||
|
|
||||||
|
|
||||||
def minmax(value, min_value, max_value):
|
def minmax(value, min_value, max_value):
|
||||||
"""Returns `value` or one of the min/max bounds if `value` is not between them.
|
"""Returns `value` or one of the min/max bounds if `value` is not between them."""
|
||||||
"""
|
|
||||||
return min(max(value, min_value), max_value)
|
return min(max(value, min_value), max_value)
|
||||||
|
|
||||||
|
|
||||||
@ -75,8 +72,7 @@ def flatten(iterables, start_with=None):
|
|||||||
|
|
||||||
|
|
||||||
def first(iterable):
|
def first(iterable):
|
||||||
"""Returns the first item of ``iterable``.
|
"""Returns the first item of ``iterable``."""
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
return next(iter(iterable))
|
return next(iter(iterable))
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
@ -84,14 +80,12 @@ def first(iterable):
|
|||||||
|
|
||||||
|
|
||||||
def stripfalse(seq):
|
def stripfalse(seq):
|
||||||
"""Returns a sequence with all false elements stripped out of seq.
|
"""Returns a sequence with all false elements stripped out of seq."""
|
||||||
"""
|
|
||||||
return [x for x in seq if x]
|
return [x for x in seq if x]
|
||||||
|
|
||||||
|
|
||||||
def extract(predicate, iterable):
|
def extract(predicate, iterable):
|
||||||
"""Separates the wheat from the shaft (`predicate` defines what's the wheat), and returns both.
|
"""Separates the wheat from the shaft (`predicate` defines what's the wheat), and returns both."""
|
||||||
"""
|
|
||||||
wheat = []
|
wheat = []
|
||||||
shaft = []
|
shaft = []
|
||||||
for item in iterable:
|
for item in iterable:
|
||||||
@ -103,8 +97,7 @@ def extract(predicate, iterable):
|
|||||||
|
|
||||||
|
|
||||||
def allsame(iterable):
|
def allsame(iterable):
|
||||||
"""Returns whether all elements of 'iterable' are the same.
|
"""Returns whether all elements of 'iterable' are the same."""
|
||||||
"""
|
|
||||||
it = iter(iterable)
|
it = iter(iterable)
|
||||||
try:
|
try:
|
||||||
first_item = next(it)
|
first_item = next(it)
|
||||||
@ -152,14 +145,12 @@ def iterconsume(seq, reverse=True):
|
|||||||
|
|
||||||
|
|
||||||
def escape(s, to_escape, escape_with="\\"):
|
def escape(s, to_escape, escape_with="\\"):
|
||||||
"""Returns ``s`` with characters in ``to_escape`` all prepended with ``escape_with``.
|
"""Returns ``s`` with characters in ``to_escape`` all prepended with ``escape_with``."""
|
||||||
"""
|
|
||||||
return "".join((escape_with + c if c in to_escape else c) for c in s)
|
return "".join((escape_with + c if c in to_escape else c) for c in s)
|
||||||
|
|
||||||
|
|
||||||
def get_file_ext(filename):
|
def get_file_ext(filename):
|
||||||
"""Returns the lowercase extension part of filename, without the dot.
|
"""Returns the lowercase extension part of filename, without the dot."""
|
||||||
"""
|
|
||||||
pos = filename.rfind(".")
|
pos = filename.rfind(".")
|
||||||
if pos > -1:
|
if pos > -1:
|
||||||
return filename[pos + 1 :].lower()
|
return filename[pos + 1 :].lower()
|
||||||
@ -168,8 +159,7 @@ def get_file_ext(filename):
|
|||||||
|
|
||||||
|
|
||||||
def rem_file_ext(filename):
|
def rem_file_ext(filename):
|
||||||
"""Returns the filename without extension.
|
"""Returns the filename without extension."""
|
||||||
"""
|
|
||||||
pos = filename.rfind(".")
|
pos = filename.rfind(".")
|
||||||
if pos > -1:
|
if pos > -1:
|
||||||
return filename[:pos]
|
return filename[:pos]
|
||||||
@ -217,8 +207,7 @@ def format_time(seconds, with_hours=True):
|
|||||||
|
|
||||||
|
|
||||||
def format_time_decimal(seconds):
|
def format_time_decimal(seconds):
|
||||||
"""Transforms seconds in a strings like '3.4 minutes'.
|
"""Transforms seconds in a strings like '3.4 minutes'."""
|
||||||
"""
|
|
||||||
minus = seconds < 0
|
minus = seconds < 0
|
||||||
if minus:
|
if minus:
|
||||||
seconds *= -1
|
seconds *= -1
|
||||||
@ -320,8 +309,7 @@ ONE_DAY = timedelta(1)
|
|||||||
|
|
||||||
|
|
||||||
def iterdaterange(start, end):
|
def iterdaterange(start, end):
|
||||||
"""Yields every day between ``start`` and ``end``.
|
"""Yields every day between ``start`` and ``end``."""
|
||||||
"""
|
|
||||||
date = start
|
date = start
|
||||||
while date <= end:
|
while date <= end:
|
||||||
yield date
|
yield date
|
||||||
@ -365,8 +353,7 @@ def find_in_path(name, paths=None):
|
|||||||
@log_io_error
|
@log_io_error
|
||||||
@pathify
|
@pathify
|
||||||
def delete_if_empty(path: Path, files_to_delete=[]):
|
def delete_if_empty(path: Path, files_to_delete=[]):
|
||||||
"""Deletes the directory at 'path' if it is empty or if it only contains files_to_delete.
|
"""Deletes the directory at 'path' if it is empty or if it only contains files_to_delete."""
|
||||||
"""
|
|
||||||
if not path.exists() or not path.isdir():
|
if not path.exists() or not path.isdir():
|
||||||
return
|
return
|
||||||
contents = path.listdir()
|
contents = path.listdir()
|
||||||
@ -411,8 +398,7 @@ def ensure_file(path):
|
|||||||
|
|
||||||
|
|
||||||
def delete_files_with_pattern(folder_path, pattern, recursive=True):
|
def delete_files_with_pattern(folder_path, pattern, recursive=True):
|
||||||
"""Delete all files (or folders) in `folder_path` that match the glob `pattern`.
|
"""Delete all files (or folders) in `folder_path` that match the glob `pattern`."""
|
||||||
"""
|
|
||||||
to_delete = glob.glob(op.join(folder_path, pattern))
|
to_delete = glob.glob(op.join(folder_path, pattern))
|
||||||
for fn in to_delete:
|
for fn in to_delete:
|
||||||
if op.isdir(fn):
|
if op.isdir(fn):
|
||||||
|
22
package.py
22
package.py
@ -82,11 +82,7 @@ def package_debian_distribution(distribution):
|
|||||||
copy(op.join(debskel, fn), op.join(debdest, fn))
|
copy(op.join(debskel, fn), op.join(debdest, fn))
|
||||||
filereplace(op.join(debskel, "control"), op.join(debdest, "control"), **debopts)
|
filereplace(op.join(debskel, "control"), op.join(debdest, "control"), **debopts)
|
||||||
filereplace(op.join(debskel, "Makefile"), op.join(destpath, "Makefile"), **debopts)
|
filereplace(op.join(debskel, "Makefile"), op.join(destpath, "Makefile"), **debopts)
|
||||||
filereplace(
|
filereplace(op.join(debskel, "dupeguru.desktop"), op.join(debdest, "dupeguru.desktop"), **debopts)
|
||||||
op.join(debskel, "dupeguru.desktop"),
|
|
||||||
op.join(debdest, "dupeguru.desktop"),
|
|
||||||
**debopts
|
|
||||||
)
|
|
||||||
changelogpath = op.join("help", "changelog")
|
changelogpath = op.join("help", "changelog")
|
||||||
changelog_dest = op.join(debdest, "changelog")
|
changelog_dest = op.join(debdest, "changelog")
|
||||||
project_name = debopts["pkgname"]
|
project_name = debopts["pkgname"]
|
||||||
@ -128,11 +124,7 @@ def package_arch():
|
|||||||
copy_files_to_package(srcpath, packages, with_so=True)
|
copy_files_to_package(srcpath, packages, with_so=True)
|
||||||
shutil.copy(op.join("images", "dgse_logo_128.png"), srcpath)
|
shutil.copy(op.join("images", "dgse_logo_128.png"), srcpath)
|
||||||
debopts = json.load(open(op.join("pkg", "arch", "dupeguru.json")))
|
debopts = json.load(open(op.join("pkg", "arch", "dupeguru.json")))
|
||||||
filereplace(
|
filereplace(op.join("pkg", "arch", "dupeguru.desktop"), op.join(srcpath, "dupeguru.desktop"), **debopts)
|
||||||
op.join("pkg", "arch", "dupeguru.desktop"),
|
|
||||||
op.join(srcpath, "dupeguru.desktop"),
|
|
||||||
**debopts
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def package_source_txz():
|
def package_source_txz():
|
||||||
@ -173,11 +165,7 @@ def package_windows():
|
|||||||
version_info = version_template.read()
|
version_info = version_template.read()
|
||||||
version_template.close()
|
version_template.close()
|
||||||
version_info_file = open("win_version_info.txt", "w")
|
version_info_file = open("win_version_info.txt", "w")
|
||||||
version_info_file.write(
|
version_info_file.write(version_info.format(version_array[0], version_array[1], version_array[2], bits))
|
||||||
version_info.format(
|
|
||||||
version_array[0], version_array[1], version_array[2], bits
|
|
||||||
)
|
|
||||||
)
|
|
||||||
version_info_file.close()
|
version_info_file.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
print("Error creating version info file, exiting...")
|
print("Error creating version info file, exiting...")
|
||||||
@ -195,9 +183,7 @@ def package_windows():
|
|||||||
"--add-data=build/locale;locale",
|
"--add-data=build/locale;locale",
|
||||||
"--add-data=build/help;help",
|
"--add-data=build/help;help",
|
||||||
"--version-file=win_version_info.txt",
|
"--version-file=win_version_info.txt",
|
||||||
"--paths=C:\\Program Files (x86)\\Windows Kits\\10\\Redist\\ucrt\\DLLs\\{0}".format(
|
"--paths=C:\\Program Files (x86)\\Windows Kits\\10\\Redist\\ucrt\\DLLs\\{0}".format(arch),
|
||||||
arch
|
|
||||||
),
|
|
||||||
"run.py",
|
"run.py",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -6,19 +6,19 @@ import importlib
|
|||||||
|
|
||||||
from setuptools import setup, Extension
|
from setuptools import setup, Extension
|
||||||
|
|
||||||
sys.path.insert(1, op.abspath('src'))
|
sys.path.insert(1, op.abspath("src"))
|
||||||
|
|
||||||
from hscommon.build import move_all
|
from hscommon.build import move_all
|
||||||
|
|
||||||
exts = [
|
exts = [
|
||||||
Extension("_block", [op.join('modules', 'block.c'), op.join('modules', 'common.c')]),
|
Extension("_block", [op.join("modules", "block.c"), op.join("modules", "common.c")]),
|
||||||
Extension("_cache", [op.join('modules', 'cache.c'), op.join('modules', 'common.c')]),
|
Extension("_cache", [op.join("modules", "cache.c"), op.join("modules", "common.c")]),
|
||||||
Extension("_block_qt", [op.join('modules', 'block_qt.c')]),
|
Extension("_block_qt", [op.join("modules", "block_qt.c")]),
|
||||||
]
|
]
|
||||||
setup(
|
setup(
|
||||||
script_args = ['build_ext', '--inplace'],
|
script_args=["build_ext", "--inplace"],
|
||||||
ext_modules=exts,
|
ext_modules=exts,
|
||||||
)
|
)
|
||||||
move_all('_block_qt*', op.join('src', 'qt', 'pe'))
|
move_all("_block_qt*", op.join("src", "qt", "pe"))
|
||||||
move_all('_cache*', op.join('src', 'core/pe'))
|
move_all("_cache*", op.join("src", "core/pe"))
|
||||||
move_all('_block*', op.join('src', 'core/pe'))
|
move_all("_block*", op.join("src", "core/pe"))
|
||||||
|
74
qt/app.py
74
qt/app.py
@ -65,18 +65,10 @@ class DupeGuru(QObject):
|
|||||||
self.recentResults.mustOpenItem.connect(self.model.load_from)
|
self.recentResults.mustOpenItem.connect(self.model.load_from)
|
||||||
self.resultWindow = None
|
self.resultWindow = None
|
||||||
if self.use_tabs:
|
if self.use_tabs:
|
||||||
self.main_window = (
|
self.main_window = TabBarWindow(self) if not self.prefs.tabs_default_pos else TabWindow(self)
|
||||||
TabBarWindow(self)
|
|
||||||
if not self.prefs.tabs_default_pos
|
|
||||||
else TabWindow(self)
|
|
||||||
)
|
|
||||||
parent_window = self.main_window
|
parent_window = self.main_window
|
||||||
self.directories_dialog = self.main_window.createPage(
|
self.directories_dialog = self.main_window.createPage("DirectoriesDialog", app=self)
|
||||||
"DirectoriesDialog", app=self
|
self.main_window.addTab(self.directories_dialog, tr("Directories"), switch=False)
|
||||||
)
|
|
||||||
self.main_window.addTab(
|
|
||||||
self.directories_dialog, tr("Directories"), switch=False
|
|
||||||
)
|
|
||||||
self.actionDirectoriesWindow.setEnabled(False)
|
self.actionDirectoriesWindow.setEnabled(False)
|
||||||
else: # floating windows only
|
else: # floating windows only
|
||||||
self.main_window = None
|
self.main_window = None
|
||||||
@ -84,9 +76,7 @@ class DupeGuru(QObject):
|
|||||||
parent_window = self.directories_dialog
|
parent_window = self.directories_dialog
|
||||||
|
|
||||||
self.progress_window = ProgressWindow(parent_window, self.model.progress_window)
|
self.progress_window = ProgressWindow(parent_window, self.model.progress_window)
|
||||||
self.problemDialog = ProblemDialog(
|
self.problemDialog = ProblemDialog(parent=parent_window, model=self.model.problem_dialog)
|
||||||
parent=parent_window, model=self.model.problem_dialog
|
|
||||||
)
|
|
||||||
if self.use_tabs:
|
if self.use_tabs:
|
||||||
self.ignoreListDialog = self.main_window.createPage(
|
self.ignoreListDialog = self.main_window.createPage(
|
||||||
"IgnoreListDialog",
|
"IgnoreListDialog",
|
||||||
@ -101,16 +91,10 @@ class DupeGuru(QObject):
|
|||||||
model=self.model.exclude_list_dialog,
|
model=self.model.exclude_list_dialog,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.ignoreListDialog = IgnoreListDialog(
|
self.ignoreListDialog = IgnoreListDialog(parent=parent_window, model=self.model.ignore_list_dialog)
|
||||||
parent=parent_window, model=self.model.ignore_list_dialog
|
self.excludeDialog = ExcludeListDialog(app=self, parent=parent_window, model=self.model.exclude_list_dialog)
|
||||||
)
|
|
||||||
self.excludeDialog = ExcludeListDialog(
|
|
||||||
app=self, parent=parent_window, model=self.model.exclude_list_dialog
|
|
||||||
)
|
|
||||||
|
|
||||||
self.deletionOptions = DeletionOptions(
|
self.deletionOptions = DeletionOptions(parent=parent_window, model=self.model.deletion_options)
|
||||||
parent=parent_window, model=self.model.deletion_options
|
|
||||||
)
|
|
||||||
self.about_box = AboutBox(parent_window, self)
|
self.about_box = AboutBox(parent_window, self)
|
||||||
|
|
||||||
parent_window.show()
|
parent_window.show()
|
||||||
@ -174,25 +158,19 @@ class DupeGuru(QObject):
|
|||||||
self.model.options["mix_file_kind"] = self.prefs.mix_file_kind
|
self.model.options["mix_file_kind"] = self.prefs.mix_file_kind
|
||||||
self.model.options["escape_filter_regexp"] = not self.prefs.use_regexp
|
self.model.options["escape_filter_regexp"] = not self.prefs.use_regexp
|
||||||
self.model.options["clean_empty_dirs"] = self.prefs.remove_empty_folders
|
self.model.options["clean_empty_dirs"] = self.prefs.remove_empty_folders
|
||||||
self.model.options[
|
self.model.options["ignore_hardlink_matches"] = self.prefs.ignore_hardlink_matches
|
||||||
"ignore_hardlink_matches"
|
|
||||||
] = self.prefs.ignore_hardlink_matches
|
|
||||||
self.model.options["copymove_dest_type"] = self.prefs.destination_type
|
self.model.options["copymove_dest_type"] = self.prefs.destination_type
|
||||||
self.model.options["scan_type"] = self.prefs.get_scan_type(self.model.app_mode)
|
self.model.options["scan_type"] = self.prefs.get_scan_type(self.model.app_mode)
|
||||||
self.model.options["min_match_percentage"] = self.prefs.filter_hardness
|
self.model.options["min_match_percentage"] = self.prefs.filter_hardness
|
||||||
self.model.options["word_weighting"] = self.prefs.word_weighting
|
self.model.options["word_weighting"] = self.prefs.word_weighting
|
||||||
self.model.options["match_similar_words"] = self.prefs.match_similar
|
self.model.options["match_similar_words"] = self.prefs.match_similar
|
||||||
threshold = (
|
threshold = self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0
|
||||||
self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0
|
self.model.options["size_threshold"] = threshold * 1024 # threshold is in KB. The scanner wants bytes
|
||||||
)
|
big_file_size_threshold = self.prefs.big_file_size_threshold if self.prefs.big_file_partial_hashes else 0
|
||||||
self.model.options["size_threshold"] = (
|
|
||||||
threshold * 1024
|
|
||||||
) # threshold is in KB. The scanner wants bytes
|
|
||||||
big_file_size_threshold = (
|
|
||||||
self.prefs.big_file_size_threshold if self.prefs.big_file_partial_hashes else 0
|
|
||||||
)
|
|
||||||
self.model.options["big_file_size_threshold"] = (
|
self.model.options["big_file_size_threshold"] = (
|
||||||
big_file_size_threshold * 1024 * 1024
|
big_file_size_threshold
|
||||||
|
* 1024
|
||||||
|
* 1024
|
||||||
# threshold is in MiB. The scanner wants bytes
|
# threshold is in MiB. The scanner wants bytes
|
||||||
)
|
)
|
||||||
scanned_tags = set()
|
scanned_tags = set()
|
||||||
@ -259,9 +237,7 @@ class DupeGuru(QObject):
|
|||||||
if self.resultWindow is not None:
|
if self.resultWindow is not None:
|
||||||
if self.use_tabs:
|
if self.use_tabs:
|
||||||
if self.main_window.indexOfWidget(self.resultWindow) < 0:
|
if self.main_window.indexOfWidget(self.resultWindow) < 0:
|
||||||
self.main_window.addTab(
|
self.main_window.addTab(self.resultWindow, tr("Results"), switch=True)
|
||||||
self.resultWindow, tr("Results"), switch=True
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
self.main_window.showTab(self.resultWindow)
|
self.main_window.showTab(self.resultWindow)
|
||||||
else:
|
else:
|
||||||
@ -318,9 +294,7 @@ class DupeGuru(QObject):
|
|||||||
|
|
||||||
def excludeListTriggered(self):
|
def excludeListTriggered(self):
|
||||||
if self.use_tabs:
|
if self.use_tabs:
|
||||||
self.showTriggeredTabbedDialog(
|
self.showTriggeredTabbedDialog(self.excludeListDialog, tr("Exclusion Filters"))
|
||||||
self.excludeListDialog, tr("Exclusion Filters")
|
|
||||||
)
|
|
||||||
else: # floating windows
|
else: # floating windows
|
||||||
self.model.exclude_list_dialog.show()
|
self.model.exclude_list_dialog.show()
|
||||||
|
|
||||||
@ -328,9 +302,7 @@ class DupeGuru(QObject):
|
|||||||
"""Add tab for dialog, name the tab with desc_string, then show it."""
|
"""Add tab for dialog, name the tab with desc_string, then show it."""
|
||||||
index = self.main_window.indexOfWidget(dialog)
|
index = self.main_window.indexOfWidget(dialog)
|
||||||
# Create the tab if it doesn't exist already
|
# Create the tab if it doesn't exist already
|
||||||
if (
|
if index < 0: # or (not dialog.isVisible() and not self.main_window.isTabVisible(index)):
|
||||||
index < 0
|
|
||||||
): # or (not dialog.isVisible() and not self.main_window.isTabVisible(index)):
|
|
||||||
index = self.main_window.addTab(dialog, desc_string, switch=True)
|
index = self.main_window.addTab(dialog, desc_string, switch=True)
|
||||||
# Show the tab for that widget
|
# Show the tab for that widget
|
||||||
self.main_window.setCurrentIndex(index)
|
self.main_window.setCurrentIndex(index)
|
||||||
@ -402,13 +374,9 @@ class DupeGuru(QObject):
|
|||||||
if self.resultWindow is not None:
|
if self.resultWindow is not None:
|
||||||
self.resultWindow.close()
|
self.resultWindow.close()
|
||||||
# This is better for tabs, as it takes care of duplicate items in menu bar
|
# This is better for tabs, as it takes care of duplicate items in menu bar
|
||||||
self.resultWindow.deleteLater() if self.use_tabs else self.resultWindow.setParent(
|
self.resultWindow.deleteLater() if self.use_tabs else self.resultWindow.setParent(None)
|
||||||
None
|
|
||||||
)
|
|
||||||
if self.use_tabs:
|
if self.use_tabs:
|
||||||
self.resultWindow = self.main_window.createPage(
|
self.resultWindow = self.main_window.createPage("ResultWindow", parent=self.main_window, app=self)
|
||||||
"ResultWindow", parent=self.main_window, app=self
|
|
||||||
)
|
|
||||||
else: # We don't use a tab widget, regular floating QMainWindow
|
else: # We don't use a tab widget, regular floating QMainWindow
|
||||||
self.resultWindow = ResultWindow(self.directories_dialog, self)
|
self.resultWindow = ResultWindow(self.directories_dialog, self)
|
||||||
self.directories_dialog._updateActionsState()
|
self.directories_dialog._updateActionsState()
|
||||||
@ -426,9 +394,7 @@ class DupeGuru(QObject):
|
|||||||
|
|
||||||
def select_dest_file(self, prompt, extension):
|
def select_dest_file(self, prompt, extension):
|
||||||
files = tr("{} file (*.{})").format(extension.upper(), extension)
|
files = tr("{} file (*.{})").format(extension.upper(), extension)
|
||||||
destination, chosen_filter = QFileDialog.getSaveFileName(
|
destination, chosen_filter = QFileDialog.getSaveFileName(self.resultWindow, prompt, "", files)
|
||||||
self.resultWindow, prompt, "", files
|
|
||||||
)
|
|
||||||
if not destination.endswith(".{}".format(extension)):
|
if not destination.endswith(".{}".format(extension)):
|
||||||
destination = "{}.{}".format(destination, extension)
|
destination = "{}.{}".format(destination, extension)
|
||||||
return destination
|
return destination
|
||||||
|
@ -42,9 +42,7 @@ class DeletionOptions(QDialog):
|
|||||||
self.linkMessageLabel = QLabel(text)
|
self.linkMessageLabel = QLabel(text)
|
||||||
self.linkMessageLabel.setWordWrap(True)
|
self.linkMessageLabel.setWordWrap(True)
|
||||||
self.verticalLayout.addWidget(self.linkMessageLabel)
|
self.verticalLayout.addWidget(self.linkMessageLabel)
|
||||||
self.linkTypeRadio = RadioBox(
|
self.linkTypeRadio = RadioBox(items=[tr("Symlink"), tr("Hardlink")], spread=False)
|
||||||
items=[tr("Symlink"), tr("Hardlink")], spread=False
|
|
||||||
)
|
|
||||||
self.verticalLayout.addWidget(self.linkTypeRadio)
|
self.verticalLayout.addWidget(self.linkTypeRadio)
|
||||||
if not self.model.supports_links():
|
if not self.model.supports_links():
|
||||||
self.linkCheckbox.setEnabled(False)
|
self.linkCheckbox.setEnabled(False)
|
||||||
|
@ -31,8 +31,7 @@ class DetailsDialog(QDockWidget):
|
|||||||
self.model.view = self
|
self.model.view = self
|
||||||
self.app.willSavePrefs.connect(self.appWillSavePrefs)
|
self.app.willSavePrefs.connect(self.appWillSavePrefs)
|
||||||
# self.setAttribute(Qt.WA_DeleteOnClose)
|
# self.setAttribute(Qt.WA_DeleteOnClose)
|
||||||
parent.addDockWidget(
|
parent.addDockWidget(area if self._wasDocked else Qt.BottomDockWidgetArea, self)
|
||||||
area if self._wasDocked else Qt.BottomDockWidgetArea, self)
|
|
||||||
|
|
||||||
def _setupUi(self): # Virtual
|
def _setupUi(self): # Virtual
|
||||||
pass
|
pass
|
||||||
|
@ -34,9 +34,11 @@ class DetailsModel(QAbstractTableModel):
|
|||||||
row = index.row()
|
row = index.row()
|
||||||
|
|
||||||
ignored_fields = ["Dupe Count"]
|
ignored_fields = ["Dupe Count"]
|
||||||
if (self.model.row(row)[0] in ignored_fields
|
if (
|
||||||
|
self.model.row(row)[0] in ignored_fields
|
||||||
or self.model.row(row)[1] == "---"
|
or self.model.row(row)[1] == "---"
|
||||||
or self.model.row(row)[2] == "---"):
|
or self.model.row(row)[2] == "---"
|
||||||
|
):
|
||||||
if role != Qt.DisplayRole:
|
if role != Qt.DisplayRole:
|
||||||
return None
|
return None
|
||||||
return self.model.row(row)[column]
|
return self.model.row(row)[column]
|
||||||
@ -52,17 +54,9 @@ class DetailsModel(QAbstractTableModel):
|
|||||||
return None # QVariant()
|
return None # QVariant()
|
||||||
|
|
||||||
def headerData(self, section, orientation, role):
|
def headerData(self, section, orientation, role):
|
||||||
if (
|
if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(HEADER):
|
||||||
orientation == Qt.Horizontal
|
|
||||||
and role == Qt.DisplayRole
|
|
||||||
and section < len(HEADER)
|
|
||||||
):
|
|
||||||
return HEADER[section]
|
return HEADER[section]
|
||||||
elif (
|
elif orientation == Qt.Vertical and role == Qt.DisplayRole and section < self.model.row_count():
|
||||||
orientation == Qt.Vertical
|
|
||||||
and role == Qt.DisplayRole
|
|
||||||
and section < self.model.row_count()
|
|
||||||
):
|
|
||||||
# Read "Attribute" cell for horizontal header
|
# Read "Attribute" cell for horizontal header
|
||||||
return self.model.row(section)[0]
|
return self.model.row(section)[0]
|
||||||
return None
|
return None
|
||||||
|
@ -45,9 +45,7 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
self.recentFolders = Recent(self.app, "recentFolders")
|
self.recentFolders = Recent(self.app, "recentFolders")
|
||||||
self._setupUi()
|
self._setupUi()
|
||||||
self._updateScanTypeList()
|
self._updateScanTypeList()
|
||||||
self.directoriesModel = DirectoriesModel(
|
self.directoriesModel = DirectoriesModel(self.app.model.directory_tree, view=self.treeView)
|
||||||
self.app.model.directory_tree, view=self.treeView
|
|
||||||
)
|
|
||||||
self.directoriesDelegate = DirectoriesDelegate()
|
self.directoriesDelegate = DirectoriesDelegate()
|
||||||
self.treeView.setItemDelegate(self.directoriesDelegate)
|
self.treeView.setItemDelegate(self.directoriesDelegate)
|
||||||
self._setupColumns()
|
self._setupColumns()
|
||||||
@ -170,9 +168,7 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
label = QLabel(tr("Application Mode:"), self)
|
label = QLabel(tr("Application Mode:"), self)
|
||||||
label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||||
hl.addWidget(label)
|
hl.addWidget(label)
|
||||||
self.appModeRadioBox = RadioBox(
|
self.appModeRadioBox = RadioBox(self, items=[tr("Standard"), tr("Music"), tr("Picture")], spread=False)
|
||||||
self, items=[tr("Standard"), tr("Music"), tr("Picture")], spread=False
|
|
||||||
)
|
|
||||||
hl.addWidget(self.appModeRadioBox)
|
hl.addWidget(self.appModeRadioBox)
|
||||||
self.verticalLayout.addLayout(hl)
|
self.verticalLayout.addLayout(hl)
|
||||||
hl = QHBoxLayout()
|
hl = QHBoxLayout()
|
||||||
@ -181,27 +177,21 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||||
hl.addWidget(label)
|
hl.addWidget(label)
|
||||||
self.scanTypeComboBox = QComboBox(self)
|
self.scanTypeComboBox = QComboBox(self)
|
||||||
self.scanTypeComboBox.setSizePolicy(
|
self.scanTypeComboBox.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed))
|
||||||
QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
||||||
)
|
|
||||||
self.scanTypeComboBox.setMaximumWidth(400)
|
self.scanTypeComboBox.setMaximumWidth(400)
|
||||||
hl.addWidget(self.scanTypeComboBox)
|
hl.addWidget(self.scanTypeComboBox)
|
||||||
self.showPreferencesButton = QPushButton(tr("More Options"), self.centralwidget)
|
self.showPreferencesButton = QPushButton(tr("More Options"), self.centralwidget)
|
||||||
self.showPreferencesButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
self.showPreferencesButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||||
hl.addWidget(self.showPreferencesButton)
|
hl.addWidget(self.showPreferencesButton)
|
||||||
self.verticalLayout.addLayout(hl)
|
self.verticalLayout.addLayout(hl)
|
||||||
self.promptLabel = QLabel(
|
self.promptLabel = QLabel(tr('Select folders to scan and press "Scan".'), self.centralwidget)
|
||||||
tr('Select folders to scan and press "Scan".'), self.centralwidget
|
|
||||||
)
|
|
||||||
self.verticalLayout.addWidget(self.promptLabel)
|
self.verticalLayout.addWidget(self.promptLabel)
|
||||||
self.treeView = QTreeView(self.centralwidget)
|
self.treeView = QTreeView(self.centralwidget)
|
||||||
self.treeView.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
self.treeView.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||||
self.treeView.setSelectionBehavior(QAbstractItemView.SelectRows)
|
self.treeView.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
self.treeView.setAcceptDrops(True)
|
self.treeView.setAcceptDrops(True)
|
||||||
triggers = (
|
triggers = (
|
||||||
QAbstractItemView.DoubleClicked
|
QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed | QAbstractItemView.SelectedClicked
|
||||||
| QAbstractItemView.EditKeyPressed
|
|
||||||
| QAbstractItemView.SelectedClicked
|
|
||||||
)
|
)
|
||||||
self.treeView.setEditTriggers(triggers)
|
self.treeView.setEditTriggers(triggers)
|
||||||
self.treeView.setDragDropOverwriteMode(True)
|
self.treeView.setDragDropOverwriteMode(True)
|
||||||
@ -267,9 +257,7 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
|
|
||||||
def _updateScanTypeList(self):
|
def _updateScanTypeList(self):
|
||||||
try:
|
try:
|
||||||
self.scanTypeComboBox.currentIndexChanged[int].disconnect(
|
self.scanTypeComboBox.currentIndexChanged[int].disconnect(self.scanTypeChanged)
|
||||||
self.scanTypeChanged
|
|
||||||
)
|
|
||||||
except TypeError:
|
except TypeError:
|
||||||
# Not connected, ignore
|
# Not connected, ignore
|
||||||
pass
|
pass
|
||||||
@ -299,9 +287,7 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
def addFolderTriggered(self):
|
def addFolderTriggered(self):
|
||||||
title = tr("Select a folder to add to the scanning list")
|
title = tr("Select a folder to add to the scanning list")
|
||||||
flags = QFileDialog.ShowDirsOnly
|
flags = QFileDialog.ShowDirsOnly
|
||||||
dirpath = str(
|
dirpath = str(QFileDialog.getExistingDirectory(self, title, self.lastAddedFolder, flags))
|
||||||
QFileDialog.getExistingDirectory(self, title, self.lastAddedFolder, flags)
|
|
||||||
)
|
|
||||||
if not dirpath:
|
if not dirpath:
|
||||||
return
|
return
|
||||||
self.lastAddedFolder = dirpath
|
self.lastAddedFolder = dirpath
|
||||||
@ -362,9 +348,7 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
|
|
||||||
def scanTypeChanged(self, index):
|
def scanTypeChanged(self, index):
|
||||||
scan_options = self.app.model.SCANNER_CLASS.get_scan_options()
|
scan_options = self.app.model.SCANNER_CLASS.get_scan_options()
|
||||||
self.app.prefs.set_scan_type(
|
self.app.prefs.set_scan_type(self.app.model.app_mode, scan_options[index].scan_type)
|
||||||
self.app.model.app_mode, scan_options[index].scan_type
|
|
||||||
)
|
|
||||||
self.app._update_options()
|
self.app._update_options()
|
||||||
|
|
||||||
def selectionChanged(self, selected, deselected):
|
def selectionChanged(self, selected, deselected):
|
||||||
|
@ -44,9 +44,7 @@ class DirectoriesDelegate(QStyledItemDelegate):
|
|||||||
# On OS X (with Qt4.6.0), adding State_Enabled to the flags causes the whole drawing to
|
# On OS X (with Qt4.6.0), adding State_Enabled to the flags causes the whole drawing to
|
||||||
# fail (draw nothing), but it's an OS X only glitch. On Windows, it works alright.
|
# fail (draw nothing), but it's an OS X only glitch. On Windows, it works alright.
|
||||||
cboption.state |= QStyle.State_Enabled
|
cboption.state |= QStyle.State_Enabled
|
||||||
QApplication.style().drawComplexControl(
|
QApplication.style().drawComplexControl(QStyle.CC_ComboBox, cboption, painter)
|
||||||
QStyle.CC_ComboBox, cboption, painter
|
|
||||||
)
|
|
||||||
painter.setBrush(option.palette.text())
|
painter.setBrush(option.palette.text())
|
||||||
rect = QRect(option.rect)
|
rect = QRect(option.rect)
|
||||||
rect.setLeft(rect.left() + 4)
|
rect.setLeft(rect.left() + 4)
|
||||||
@ -75,9 +73,7 @@ class DirectoriesModel(TreeModel):
|
|||||||
self.view = view
|
self.view = view
|
||||||
self.view.setModel(self)
|
self.view.setModel(self)
|
||||||
|
|
||||||
self.view.selectionModel().selectionChanged[
|
self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged)
|
||||||
(QItemSelection, QItemSelection)
|
|
||||||
].connect(self.selectionChanged)
|
|
||||||
|
|
||||||
def _createNode(self, ref, row):
|
def _createNode(self, ref, row):
|
||||||
return RefNode(self, None, ref, row)
|
return RefNode(self, None, ref, row)
|
||||||
@ -155,10 +151,7 @@ class DirectoriesModel(TreeModel):
|
|||||||
|
|
||||||
# --- Events
|
# --- Events
|
||||||
def selectionChanged(self, selected, deselected):
|
def selectionChanged(self, selected, deselected):
|
||||||
newNodes = [
|
newNodes = [modelIndex.internalPointer().ref for modelIndex in self.view.selectionModel().selectedRows()]
|
||||||
modelIndex.internalPointer().ref
|
|
||||||
for modelIndex in self.view.selectionModel().selectedRows()
|
|
||||||
]
|
|
||||||
self.model.selected_nodes = newNodes
|
self.model.selected_nodes = newNodes
|
||||||
|
|
||||||
# --- Signals
|
# --- Signals
|
||||||
|
@ -5,13 +5,22 @@
|
|||||||
import re
|
import re
|
||||||
from PyQt5.QtCore import Qt, pyqtSlot
|
from PyQt5.QtCore import Qt, pyqtSlot
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QPushButton, QLineEdit, QVBoxLayout, QGridLayout, QDialog,
|
QPushButton,
|
||||||
QTableView, QAbstractItemView, QSpacerItem, QSizePolicy, QHeaderView
|
QLineEdit,
|
||||||
|
QVBoxLayout,
|
||||||
|
QGridLayout,
|
||||||
|
QDialog,
|
||||||
|
QTableView,
|
||||||
|
QAbstractItemView,
|
||||||
|
QSpacerItem,
|
||||||
|
QSizePolicy,
|
||||||
|
QHeaderView,
|
||||||
)
|
)
|
||||||
from .exclude_list_table import ExcludeListTable
|
from .exclude_list_table import ExcludeListTable
|
||||||
|
|
||||||
from core.exclude import AlreadyThereException
|
from core.exclude import AlreadyThereException
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
|
|
||||||
tr = trget("ui")
|
tr = trget("ui")
|
||||||
|
|
||||||
|
|
||||||
@ -51,9 +60,7 @@ class ExcludeListDialog(QDialog):
|
|||||||
self.testLine = QLineEdit()
|
self.testLine = QLineEdit()
|
||||||
self.tableView = QTableView()
|
self.tableView = QTableView()
|
||||||
triggers = (
|
triggers = (
|
||||||
QAbstractItemView.DoubleClicked
|
QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed | QAbstractItemView.SelectedClicked
|
||||||
| QAbstractItemView.EditKeyPressed
|
|
||||||
| QAbstractItemView.SelectedClicked
|
|
||||||
)
|
)
|
||||||
self.tableView.setEditTriggers(triggers)
|
self.tableView.setEditTriggers(triggers)
|
||||||
self.tableView.setSelectionMode(QTableView.ExtendedSelection)
|
self.tableView.setSelectionMode(QTableView.ExtendedSelection)
|
||||||
@ -150,7 +157,9 @@ class ExcludeListDialog(QDialog):
|
|||||||
self.table.refresh()
|
self.table.refresh()
|
||||||
|
|
||||||
def display_help_message(self):
|
def display_help_message(self):
|
||||||
self.app.show_message(tr("""\
|
self.app.show_message(
|
||||||
|
tr(
|
||||||
|
"""\
|
||||||
These (case sensitive) python regular expressions will filter out files during scans.<br>\
|
These (case sensitive) python regular expressions will filter out files during scans.<br>\
|
||||||
Directores will also have their <strong>default state</strong> set to Excluded \
|
Directores will also have their <strong>default state</strong> set to Excluded \
|
||||||
in the Directories tab if their name happens to match one of the selected regular expressions.<br>\
|
in the Directories tab if their name happens to match one of the selected regular expressions.<br>\
|
||||||
@ -163,4 +172,6 @@ You can test the regular expression with the "test string" button after pasting
|
|||||||
<code>C:\\\\User\\My Pictures\\test.png</code><br><br>
|
<code>C:\\\\User\\My Pictures\\test.png</code><br><br>
|
||||||
Matching regular expressions will be highlighted.<br>\
|
Matching regular expressions will be highlighted.<br>\
|
||||||
If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>\
|
If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>\
|
||||||
Directories and files starting with a period '.' are filtered out by default.<br><br>"""))
|
Directories and files starting with a period '.' are filtered out by default.<br><br>"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@ -8,15 +8,14 @@ from PyQt5.QtGui import QFont, QFontMetrics, QIcon, QColor
|
|||||||
from qtlib.column import Column
|
from qtlib.column import Column
|
||||||
from qtlib.table import Table
|
from qtlib.table import Table
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
|
|
||||||
tr = trget("ui")
|
tr = trget("ui")
|
||||||
|
|
||||||
|
|
||||||
class ExcludeListTable(Table):
|
class ExcludeListTable(Table):
|
||||||
"""Model for exclude list"""
|
"""Model for exclude list"""
|
||||||
COLUMNS = [
|
|
||||||
Column("marked", defaultWidth=15),
|
COLUMNS = [Column("marked", defaultWidth=15), Column("regex", defaultWidth=230)]
|
||||||
Column("regex", defaultWidth=230)
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, app, view, **kwargs):
|
def __init__(self, app, view, **kwargs):
|
||||||
model = app.model.exclude_list_dialog.exclude_list_table # pointer to GUITable
|
model = app.model.exclude_list_dialog.exclude_list_table # pointer to GUITable
|
||||||
|
@ -56,9 +56,7 @@ class IgnoreListDialog(QDialog):
|
|||||||
self.clearButton = QPushButton(tr("Clear"))
|
self.clearButton = QPushButton(tr("Clear"))
|
||||||
self.closeButton = QPushButton(tr("Close"))
|
self.closeButton = QPushButton(tr("Close"))
|
||||||
self.verticalLayout.addLayout(
|
self.verticalLayout.addLayout(
|
||||||
horizontalWrap(
|
horizontalWrap([self.removeSelectedButton, self.clearButton, None, self.closeButton])
|
||||||
[self.removeSelectedButton, self.clearButton, None, self.closeButton]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- model --> view
|
# --- model --> view
|
||||||
|
@ -59,13 +59,9 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self.widgetsVLayout.addWidget(self.matchSimilarBox)
|
self.widgetsVLayout.addWidget(self.matchSimilarBox)
|
||||||
self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind"))
|
self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind"))
|
||||||
self.widgetsVLayout.addWidget(self.mixFileKindBox)
|
self.widgetsVLayout.addWidget(self.mixFileKindBox)
|
||||||
self._setupAddCheckbox(
|
self._setupAddCheckbox("useRegexpBox", tr("Use regular expressions when filtering"))
|
||||||
"useRegexpBox", tr("Use regular expressions when filtering")
|
|
||||||
)
|
|
||||||
self.widgetsVLayout.addWidget(self.useRegexpBox)
|
self.widgetsVLayout.addWidget(self.useRegexpBox)
|
||||||
self._setupAddCheckbox(
|
self._setupAddCheckbox("removeEmptyFoldersBox", tr("Remove empty folders on delete or move"))
|
||||||
"removeEmptyFoldersBox", tr("Remove empty folders on delete or move")
|
|
||||||
)
|
|
||||||
self.widgetsVLayout.addWidget(self.removeEmptyFoldersBox)
|
self.widgetsVLayout.addWidget(self.removeEmptyFoldersBox)
|
||||||
self._setupAddCheckbox(
|
self._setupAddCheckbox(
|
||||||
"ignoreHardlinkMatches",
|
"ignoreHardlinkMatches",
|
||||||
|
@ -5,14 +5,13 @@
|
|||||||
# 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 PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import QAbstractItemView, QSizePolicy, QGridLayout, QSplitter, QFrame
|
||||||
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 ..details_dialog import DetailsDialog as DetailsDialogBase
|
from ..details_dialog import DetailsDialog as DetailsDialogBase
|
||||||
from ..details_table import DetailsTable
|
from ..details_table import DetailsTable
|
||||||
from .image_viewer import (
|
from .image_viewer import ViewerToolBar, ScrollAreaImageViewer, ScrollAreaController
|
||||||
ViewerToolBar, ScrollAreaImageViewer, ScrollAreaController)
|
|
||||||
tr = trget("ui")
|
tr = trget("ui")
|
||||||
|
|
||||||
|
|
||||||
@ -70,8 +69,7 @@ class DetailsDialog(DetailsDialogBase):
|
|||||||
self.splitter.addWidget(self.tableView)
|
self.splitter.addWidget(self.tableView)
|
||||||
self.splitter.setStretchFactor(1, 1)
|
self.splitter.setStretchFactor(1, 1)
|
||||||
# Late population needed here for connections to the toolbar
|
# Late population needed here for connections to the toolbar
|
||||||
self.vController.setupViewers(
|
self.vController.setupViewers(self.selectedImageViewer, self.referenceImageViewer)
|
||||||
self.selectedImageViewer, self.referenceImageViewer)
|
|
||||||
# self.setCentralWidget(self.splitter) # only as QMainWindow
|
# self.setCentralWidget(self.splitter) # only as QMainWindow
|
||||||
self.setWidget(self.splitter) # only as QDockWidget
|
self.setWidget(self.splitter) # only as QDockWidget
|
||||||
|
|
||||||
@ -103,11 +101,11 @@ class DetailsDialog(DetailsDialogBase):
|
|||||||
# 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(
|
||||||
self.tableView.rowHeight(1)
|
self.tableView.rowHeight(1) * self.tableModel.model.row_count()
|
||||||
* self.tableModel.model.row_count()
|
|
||||||
+ self.tableView.verticalHeader().sectionSize(0)
|
+ self.tableView.verticalHeader().sectionSize(0)
|
||||||
# looks like the handle is taken into account by the splitter
|
# looks like the handle is taken into account by the splitter
|
||||||
+ self.splitter.handle(1).size().height())
|
+ self.splitter.handle(1).size().height()
|
||||||
|
)
|
||||||
DetailsDialogBase.show(self)
|
DetailsDialogBase.show(self)
|
||||||
self.ensure_same_sizes()
|
self.ensure_same_sizes()
|
||||||
self._update()
|
self._update()
|
||||||
@ -138,6 +136,7 @@ class DetailsDialog(DetailsDialogBase):
|
|||||||
|
|
||||||
class EmittingFrame(QFrame):
|
class EmittingFrame(QFrame):
|
||||||
"""Emits a signal whenever is resized"""
|
"""Emits a signal whenever is resized"""
|
||||||
|
|
||||||
resized = pyqtSignal(QResizeEvent)
|
resized = pyqtSignal(QResizeEvent)
|
||||||
|
|
||||||
def resizeEvent(self, event):
|
def resizeEvent(self, event):
|
||||||
|
@ -2,15 +2,24 @@
|
|||||||
# 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 (
|
from PyQt5.QtCore import QObject, Qt, QSize, QRectF, QPointF, QPoint, pyqtSlot, pyqtSignal, QEvent
|
||||||
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 (
|
||||||
QGraphicsView, QGraphicsScene, QGraphicsPixmapItem,
|
QGraphicsView,
|
||||||
QToolBar, QToolButton, QAction, QWidget, QScrollArea,
|
QGraphicsScene,
|
||||||
QApplication, QAbstractScrollArea, QStyle)
|
QGraphicsPixmapItem,
|
||||||
|
QToolBar,
|
||||||
|
QToolButton,
|
||||||
|
QAction,
|
||||||
|
QWidget,
|
||||||
|
QScrollArea,
|
||||||
|
QApplication,
|
||||||
|
QAbstractScrollArea,
|
||||||
|
QStyle,
|
||||||
|
)
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
from hscommon.plat import ISLINUX
|
from hscommon.plat import ISLINUX
|
||||||
|
|
||||||
tr = trget("ui")
|
tr = trget("ui")
|
||||||
|
|
||||||
MAX_SCALE = 12.0
|
MAX_SCALE = 12.0
|
||||||
@ -50,8 +59,7 @@ class ViewerToolBar(QToolBar):
|
|||||||
"actionZoomIn",
|
"actionZoomIn",
|
||||||
QKeySequence.ZoomIn,
|
QKeySequence.ZoomIn,
|
||||||
QIcon.fromTheme("zoom-in")
|
QIcon.fromTheme("zoom-in")
|
||||||
if ISLINUX
|
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
|
||||||
and not self.parent.app.prefs.details_dialog_override_theme_icons
|
|
||||||
else QIcon(QPixmap(":/" + "zoom_in")),
|
else QIcon(QPixmap(":/" + "zoom_in")),
|
||||||
tr("Increase zoom"),
|
tr("Increase zoom"),
|
||||||
controller.zoomIn,
|
controller.zoomIn,
|
||||||
@ -60,8 +68,7 @@ class ViewerToolBar(QToolBar):
|
|||||||
"actionZoomOut",
|
"actionZoomOut",
|
||||||
QKeySequence.ZoomOut,
|
QKeySequence.ZoomOut,
|
||||||
QIcon.fromTheme("zoom-out")
|
QIcon.fromTheme("zoom-out")
|
||||||
if ISLINUX
|
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
|
||||||
and not self.parent.app.prefs.details_dialog_override_theme_icons
|
|
||||||
else QIcon(QPixmap(":/" + "zoom_out")),
|
else QIcon(QPixmap(":/" + "zoom_out")),
|
||||||
tr("Decrease zoom"),
|
tr("Decrease zoom"),
|
||||||
controller.zoomOut,
|
controller.zoomOut,
|
||||||
@ -70,8 +77,7 @@ class ViewerToolBar(QToolBar):
|
|||||||
"actionNormalSize",
|
"actionNormalSize",
|
||||||
tr("Ctrl+/"),
|
tr("Ctrl+/"),
|
||||||
QIcon.fromTheme("zoom-original")
|
QIcon.fromTheme("zoom-original")
|
||||||
if ISLINUX
|
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
|
||||||
and not self.parent.app.prefs.details_dialog_override_theme_icons
|
|
||||||
else QIcon(QPixmap(":/" + "zoom_original")),
|
else QIcon(QPixmap(":/" + "zoom_original")),
|
||||||
tr("Normal size"),
|
tr("Normal size"),
|
||||||
controller.zoomNormalSize,
|
controller.zoomNormalSize,
|
||||||
@ -80,12 +86,11 @@ class ViewerToolBar(QToolBar):
|
|||||||
"actionBestFit",
|
"actionBestFit",
|
||||||
tr("Ctrl+*"),
|
tr("Ctrl+*"),
|
||||||
QIcon.fromTheme("zoom-best-fit")
|
QIcon.fromTheme("zoom-best-fit")
|
||||||
if ISLINUX
|
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
|
||||||
and not self.parent.app.prefs.details_dialog_override_theme_icons
|
|
||||||
else QIcon(QPixmap(":/" + "zoom_best_fit")),
|
else QIcon(QPixmap(":/" + "zoom_best_fit")),
|
||||||
tr("Best fit"),
|
tr("Best fit"),
|
||||||
controller.zoomBestFit,
|
controller.zoomBestFit,
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
# TODO try with QWidgetAction() instead in order to have
|
# TODO try with QWidgetAction() instead in order to have
|
||||||
# the popup menu work in the toolbar (if resized below minimum height)
|
# the popup menu work in the toolbar (if resized below minimum height)
|
||||||
@ -95,13 +100,12 @@ class ViewerToolBar(QToolBar):
|
|||||||
self.buttonImgSwap = QToolButton(self)
|
self.buttonImgSwap = QToolButton(self)
|
||||||
self.buttonImgSwap.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
self.buttonImgSwap.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||||
self.buttonImgSwap.setIcon(
|
self.buttonImgSwap.setIcon(
|
||||||
QIcon.fromTheme('view-refresh',
|
QIcon.fromTheme("view-refresh", self.style().standardIcon(QStyle.SP_BrowserReload))
|
||||||
self.style().standardIcon(QStyle.SP_BrowserReload))
|
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
|
||||||
if ISLINUX
|
else QIcon(QPixmap(":/" + "exchange"))
|
||||||
and not self.parent.app.prefs.details_dialog_override_theme_icons
|
)
|
||||||
else QIcon(QPixmap(":/" + "exchange")))
|
self.buttonImgSwap.setText("Swap images")
|
||||||
self.buttonImgSwap.setText('Swap images')
|
self.buttonImgSwap.setToolTip("Swap images")
|
||||||
self.buttonImgSwap.setToolTip('Swap images')
|
|
||||||
self.buttonImgSwap.pressed.connect(self.controller.swapImages)
|
self.buttonImgSwap.pressed.connect(self.controller.swapImages)
|
||||||
self.buttonImgSwap.released.connect(self.controller.swapImages)
|
self.buttonImgSwap.released.connect(self.controller.swapImages)
|
||||||
|
|
||||||
@ -207,11 +211,11 @@ class BaseController(QObject):
|
|||||||
# than the ReferenceImageViewer by one pixel, which distorts the
|
# than the ReferenceImageViewer by one pixel, which distorts the
|
||||||
# scaled down pixmap for the reference, hence we'll reuse its size here.
|
# scaled down pixmap for the reference, hence we'll reuse its size here.
|
||||||
selected_size = self._updateImage(
|
selected_size = self._updateImage(
|
||||||
self.selectedPixmap, self.scaledSelectedPixmap,
|
self.selectedPixmap, self.scaledSelectedPixmap, self.selectedViewer, None, same_group
|
||||||
self.selectedViewer, None, same_group)
|
)
|
||||||
self._updateImage(
|
self._updateImage(
|
||||||
self.referencePixmap, self.scaledReferencePixmap,
|
self.referencePixmap, self.scaledReferencePixmap, self.referenceViewer, selected_size, same_group
|
||||||
self.referenceViewer, selected_size, same_group)
|
)
|
||||||
if ignore_update:
|
if ignore_update:
|
||||||
self.selectedViewer.ignore_signal = False
|
self.selectedViewer.ignore_signal = False
|
||||||
|
|
||||||
@ -229,12 +233,10 @@ 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(
|
scaledpixmap = pixmap.scaled(target_size, Qt.KeepAspectRatioByExpanding, Qt.FastTransformation)
|
||||||
target_size, Qt.KeepAspectRatioByExpanding, Qt.FastTransformation)
|
|
||||||
else:
|
else:
|
||||||
# best fit, keep ratio always
|
# best fit, keep ratio always
|
||||||
scaledpixmap = pixmap.scaled(
|
scaledpixmap = pixmap.scaled(target_size, Qt.KeepAspectRatio, Qt.FastTransformation)
|
||||||
target_size, Qt.KeepAspectRatio, Qt.FastTransformation)
|
|
||||||
viewer.setImage(scaledpixmap)
|
viewer.setImage(scaledpixmap)
|
||||||
return target_size
|
return target_size
|
||||||
|
|
||||||
@ -347,12 +349,8 @@ class BaseController(QObject):
|
|||||||
self.selectedViewer.resetCenter()
|
self.selectedViewer.resetCenter()
|
||||||
self.referenceViewer.resetCenter()
|
self.referenceViewer.resetCenter()
|
||||||
|
|
||||||
target_size = self._updateImage(
|
target_size = self._updateImage(self.selectedPixmap, self.scaledSelectedPixmap, self.selectedViewer, None, True)
|
||||||
self.selectedPixmap, self.scaledSelectedPixmap,
|
self._updateImage(self.referencePixmap, self.scaledReferencePixmap, self.referenceViewer, target_size, True)
|
||||||
self.selectedViewer, None, True)
|
|
||||||
self._updateImage(
|
|
||||||
self.referencePixmap, self.scaledReferencePixmap,
|
|
||||||
self.referenceViewer, target_size, True)
|
|
||||||
self.centerViews()
|
self.centerViews()
|
||||||
|
|
||||||
self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)
|
self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)
|
||||||
@ -402,6 +400,7 @@ 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):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
@ -430,6 +429,7 @@ 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):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
@ -442,10 +442,8 @@ class ScrollAreaController(BaseController):
|
|||||||
super().updateBothImages(same_group)
|
super().updateBothImages(same_group)
|
||||||
if not self.referenceViewer.isEnabled():
|
if not self.referenceViewer.isEnabled():
|
||||||
return
|
return
|
||||||
self.referenceViewer._horizontalScrollBar.setValue(
|
self.referenceViewer._horizontalScrollBar.setValue(self.selectedViewer._horizontalScrollBar.value())
|
||||||
self.selectedViewer._horizontalScrollBar.value())
|
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):
|
||||||
@ -518,6 +516,7 @@ class ScrollAreaController(BaseController):
|
|||||||
|
|
||||||
class GraphicsViewController(BaseController):
|
class GraphicsViewController(BaseController):
|
||||||
"""Specialized version fro QGraphicsView-based viewers."""
|
"""Specialized version fro QGraphicsView-based viewers."""
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
@ -625,10 +624,8 @@ class GraphicsViewController(BaseController):
|
|||||||
if ignore_update:
|
if ignore_update:
|
||||||
self.selectedViewer.ignore_signal = True
|
self.selectedViewer.ignore_signal = True
|
||||||
|
|
||||||
self._updateFitImage(
|
self._updateFitImage(self.selectedPixmap, self.selectedViewer)
|
||||||
self.selectedPixmap, self.selectedViewer)
|
self._updateFitImage(self.referencePixmap, self.referenceViewer)
|
||||||
self._updateFitImage(
|
|
||||||
self.referencePixmap, self.referenceViewer)
|
|
||||||
|
|
||||||
if ignore_update:
|
if ignore_update:
|
||||||
self.selectedViewer.ignore_signal = False
|
self.selectedViewer.ignore_signal = False
|
||||||
@ -699,6 +696,7 @@ class GraphicsViewController(BaseController):
|
|||||||
|
|
||||||
class QWidgetImageViewer(QWidget):
|
class QWidgetImageViewer(QWidget):
|
||||||
"""Use a QPixmap, but no scrollbars and no keyboard key sequence for navigation."""
|
"""Use a QPixmap, but no scrollbars and no keyboard key sequence for navigation."""
|
||||||
|
|
||||||
# FIXME: panning while zoomed-in is broken (due to delta not interpolated right?
|
# FIXME: panning while zoomed-in is broken (due to delta not interpolated right?
|
||||||
mouseDragged = pyqtSignal(QPointF)
|
mouseDragged = pyqtSignal(QPointF)
|
||||||
mouseWheeled = pyqtSignal(float)
|
mouseWheeled = pyqtSignal(float)
|
||||||
@ -720,15 +718,13 @@ class QWidgetImageViewer(QWidget):
|
|||||||
self.setMouseTracking(False)
|
self.setMouseTracking(False)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'{self._instance_name}'
|
return f"{self._instance_name}"
|
||||||
|
|
||||||
def connectMouseSignals(self):
|
def connectMouseSignals(self):
|
||||||
if not self._dragConnection:
|
if not self._dragConnection:
|
||||||
self._dragConnection = self.mouseDragged.connect(
|
self._dragConnection = self.mouseDragged.connect(self.controller.onDraggedMouse)
|
||||||
self.controller.onDraggedMouse)
|
|
||||||
if not self._wheelConnection:
|
if not self._wheelConnection:
|
||||||
self._wheelConnection = self.mouseWheeled.connect(
|
self._wheelConnection = self.mouseWheeled.connect(self.controller.scaleImagesBy)
|
||||||
self.controller.scaleImagesBy)
|
|
||||||
|
|
||||||
def disconnectMouseSignals(self):
|
def disconnectMouseSignals(self):
|
||||||
if self._dragConnection:
|
if self._dragConnection:
|
||||||
@ -783,8 +779,7 @@ class QWidgetImageViewer(QWidget):
|
|||||||
event.ignore()
|
event.ignore()
|
||||||
return
|
return
|
||||||
|
|
||||||
self._mousePanningDelta += (
|
self._mousePanningDelta += (event.pos() - self._lastMouseClickPoint) * 1.0 / self.current_scale
|
||||||
event.pos() - self._lastMouseClickPoint) * 1.0 / self.current_scale
|
|
||||||
self._lastMouseClickPoint = event.pos()
|
self._lastMouseClickPoint = event.pos()
|
||||||
if self._drag:
|
if self._drag:
|
||||||
self.mouseDragged.emit(self._mousePanningDelta)
|
self.mouseDragged.emit(self._mousePanningDelta)
|
||||||
@ -860,6 +855,7 @@ class QWidgetImageViewer(QWidget):
|
|||||||
|
|
||||||
class ScalablePixmap(QWidget):
|
class ScalablePixmap(QWidget):
|
||||||
"""Container for a pixmap that scales up very fast, used in ScrollAreaImageViewer."""
|
"""Container for a pixmap that scales up very fast, used in ScrollAreaImageViewer."""
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._pixmap = QPixmap()
|
self._pixmap = QPixmap()
|
||||||
@ -881,6 +877,7 @@ class ScalablePixmap(QWidget):
|
|||||||
|
|
||||||
class ScrollAreaImageViewer(QScrollArea):
|
class ScrollAreaImageViewer(QScrollArea):
|
||||||
"""Implementation using a pixmap container in a simple scroll area."""
|
"""Implementation using a pixmap container in a simple scroll area."""
|
||||||
|
|
||||||
mouseDragged = pyqtSignal(QPoint)
|
mouseDragged = pyqtSignal(QPoint)
|
||||||
mouseWheeled = pyqtSignal(float, QPointF)
|
mouseWheeled = pyqtSignal(float, QPointF)
|
||||||
|
|
||||||
@ -921,7 +918,7 @@ class ScrollAreaImageViewer(QScrollArea):
|
|||||||
self.setVisible(True)
|
self.setVisible(True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'{self._instance_name}'
|
return f"{self._instance_name}"
|
||||||
|
|
||||||
def toggleScrollBars(self, forceOn=False):
|
def toggleScrollBars(self, forceOn=False):
|
||||||
if not self.prefs.details_dialog_viewers_show_scrollbars:
|
if not self.prefs.details_dialog_viewers_show_scrollbars:
|
||||||
@ -938,11 +935,9 @@ class ScrollAreaImageViewer(QScrollArea):
|
|||||||
|
|
||||||
def connectMouseSignals(self):
|
def connectMouseSignals(self):
|
||||||
if not self._dragConnection:
|
if not self._dragConnection:
|
||||||
self._dragConnection = self.mouseDragged.connect(
|
self._dragConnection = self.mouseDragged.connect(self.controller.onDraggedMouse)
|
||||||
self.controller.onDraggedMouse)
|
|
||||||
if not self._wheelConnection:
|
if not self._wheelConnection:
|
||||||
self._wheelConnection = self.mouseWheeled.connect(
|
self._wheelConnection = self.mouseWheeled.connect(self.controller.onMouseWheel)
|
||||||
self.controller.onMouseWheel)
|
|
||||||
|
|
||||||
def disconnectMouseSignals(self):
|
def disconnectMouseSignals(self):
|
||||||
if self._dragConnection:
|
if self._dragConnection:
|
||||||
@ -955,10 +950,8 @@ class ScrollAreaImageViewer(QScrollArea):
|
|||||||
def connectScrollBars(self):
|
def connectScrollBars(self):
|
||||||
"""Only call once controller is connected."""
|
"""Only call once controller is connected."""
|
||||||
# Cyclic connections are handled by Qt
|
# Cyclic connections are handled by Qt
|
||||||
self._verticalScrollBar.valueChanged.connect(
|
self._verticalScrollBar.valueChanged.connect(self.controller.onVScrollBarChanged, Qt.UniqueConnection)
|
||||||
self.controller.onVScrollBarChanged, Qt.UniqueConnection)
|
self._horizontalScrollBar.valueChanged.connect(self.controller.onHScrollBarChanged, Qt.UniqueConnection)
|
||||||
self._horizontalScrollBar.valueChanged.connect(
|
|
||||||
self.controller.onHScrollBarChanged, Qt.UniqueConnection)
|
|
||||||
|
|
||||||
def contextMenuEvent(self, event):
|
def contextMenuEvent(self, event):
|
||||||
"""Block parent's (main window) context menu on right click."""
|
"""Block parent's (main window) context menu on right click."""
|
||||||
@ -987,7 +980,7 @@ class ScrollAreaImageViewer(QScrollArea):
|
|||||||
event.ignore()
|
event.ignore()
|
||||||
return
|
return
|
||||||
if self._drag:
|
if self._drag:
|
||||||
delta = (event.pos() - self._lastMouseClickPoint)
|
delta = event.pos() - self._lastMouseClickPoint
|
||||||
self._lastMouseClickPoint = event.pos()
|
self._lastMouseClickPoint = event.pos()
|
||||||
self.mouseDragged.emit(delta)
|
self.mouseDragged.emit(delta)
|
||||||
super().mouseMoveEvent(event)
|
super().mouseMoveEvent(event)
|
||||||
@ -1064,32 +1057,26 @@ class ScrollAreaImageViewer(QScrollArea):
|
|||||||
"""After scaling, no mouse position, default to center."""
|
"""After scaling, no mouse position, default to center."""
|
||||||
# scrollBar.setMaximum(scrollBar.maximum() - scrollBar.minimum() + scrollBar.pageStep())
|
# scrollBar.setMaximum(scrollBar.maximum() - scrollBar.minimum() + scrollBar.pageStep())
|
||||||
self._horizontalScrollBar.setValue(
|
self._horizontalScrollBar.setValue(
|
||||||
int(factor * self._horizontalScrollBar.value()
|
int(factor * self._horizontalScrollBar.value() + ((factor - 1) * self._horizontalScrollBar.pageStep() / 2))
|
||||||
+ ((factor - 1) * self._horizontalScrollBar.pageStep() / 2)))
|
)
|
||||||
self._verticalScrollBar.setValue(
|
self._verticalScrollBar.setValue(
|
||||||
int(factor * self._verticalScrollBar.value()
|
int(factor * self._verticalScrollBar.value() + ((factor - 1) * self._verticalScrollBar.pageStep() / 2))
|
||||||
+ ((factor - 1) * self._verticalScrollBar.pageStep() / 2)))
|
)
|
||||||
|
|
||||||
def adjustScrollBarsScaled(self, delta):
|
def adjustScrollBarsScaled(self, delta):
|
||||||
"""After scaling with the mouse, update relative to mouse position."""
|
"""After scaling with the mouse, update relative to mouse position."""
|
||||||
self._horizontalScrollBar.setValue(
|
self._horizontalScrollBar.setValue(self._horizontalScrollBar.value() + delta.x())
|
||||||
self._horizontalScrollBar.value() + delta.x())
|
self._verticalScrollBar.setValue(self._verticalScrollBar.value() + delta.y())
|
||||||
self._verticalScrollBar.setValue(
|
|
||||||
self._verticalScrollBar.value() + delta.y())
|
|
||||||
|
|
||||||
def adjustScrollBarsAuto(self):
|
def adjustScrollBarsAuto(self):
|
||||||
"""After panning, update accordingly."""
|
"""After panning, update accordingly."""
|
||||||
self.horizontalScrollBar().setValue(
|
self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - self._mousePanningDelta.x())
|
||||||
self.horizontalScrollBar().value() - self._mousePanningDelta.x())
|
self.verticalScrollBar().setValue(self.verticalScrollBar().value() - self._mousePanningDelta.y())
|
||||||
self.verticalScrollBar().setValue(
|
|
||||||
self.verticalScrollBar().value() - self._mousePanningDelta.y())
|
|
||||||
|
|
||||||
def adjustScrollBarCentered(self):
|
def adjustScrollBarCentered(self):
|
||||||
"""Just center in the middle."""
|
"""Just center in the middle."""
|
||||||
self._horizontalScrollBar.setValue(
|
self._horizontalScrollBar.setValue(int(self._horizontalScrollBar.maximum() / 2))
|
||||||
int(self._horizontalScrollBar.maximum() / 2))
|
self._verticalScrollBar.setValue(int(self._verticalScrollBar.maximum() / 2))
|
||||||
self._verticalScrollBar.setValue(
|
|
||||||
int(self._verticalScrollBar.maximum() / 2))
|
|
||||||
|
|
||||||
def resetCenter(self):
|
def resetCenter(self):
|
||||||
"""Resets origin"""
|
"""Resets origin"""
|
||||||
@ -1127,6 +1114,7 @@ class ScrollAreaImageViewer(QScrollArea):
|
|||||||
|
|
||||||
class GraphicsViewViewer(QGraphicsView):
|
class GraphicsViewViewer(QGraphicsView):
|
||||||
"""Re-Implementation a full-fledged GraphicsView but is a bit buggy."""
|
"""Re-Implementation a full-fledged GraphicsView but is a bit buggy."""
|
||||||
|
|
||||||
mouseDragged = pyqtSignal()
|
mouseDragged = pyqtSignal()
|
||||||
mouseWheeled = pyqtSignal(float, QPointF)
|
mouseWheeled = pyqtSignal(float, QPointF)
|
||||||
|
|
||||||
@ -1178,11 +1166,9 @@ class GraphicsViewViewer(QGraphicsView):
|
|||||||
|
|
||||||
def connectMouseSignals(self):
|
def connectMouseSignals(self):
|
||||||
if not self._dragConnection:
|
if not self._dragConnection:
|
||||||
self._dragConnection = self.mouseDragged.connect(
|
self._dragConnection = self.mouseDragged.connect(self.controller.syncCenters)
|
||||||
self.controller.syncCenters)
|
|
||||||
if not self._wheelConnection:
|
if not self._wheelConnection:
|
||||||
self._wheelConnection = self.mouseWheeled.connect(
|
self._wheelConnection = self.mouseWheeled.connect(self.controller.onMouseWheel)
|
||||||
self.controller.onMouseWheel)
|
|
||||||
|
|
||||||
def disconnectMouseSignals(self):
|
def disconnectMouseSignals(self):
|
||||||
if self._dragConnection:
|
if self._dragConnection:
|
||||||
@ -1195,10 +1181,8 @@ class GraphicsViewViewer(QGraphicsView):
|
|||||||
def connectScrollBars(self):
|
def connectScrollBars(self):
|
||||||
"""Only call once controller is connected."""
|
"""Only call once controller is connected."""
|
||||||
# Cyclic connections are handled by Qt
|
# Cyclic connections are handled by Qt
|
||||||
self._verticalScrollBar.valueChanged.connect(
|
self._verticalScrollBar.valueChanged.connect(self.controller.onVScrollBarChanged, Qt.UniqueConnection)
|
||||||
self.controller.onVScrollBarChanged, Qt.UniqueConnection)
|
self._horizontalScrollBar.valueChanged.connect(self.controller.onHScrollBarChanged, Qt.UniqueConnection)
|
||||||
self._horizontalScrollBar.valueChanged.connect(
|
|
||||||
self.controller.onHScrollBarChanged, Qt.UniqueConnection)
|
|
||||||
|
|
||||||
def toggleScrollBars(self, forceOn=False):
|
def toggleScrollBars(self, forceOn=False):
|
||||||
if not self.prefs.details_dialog_viewers_show_scrollbars:
|
if not self.prefs.details_dialog_viewers_show_scrollbars:
|
||||||
@ -1345,10 +1329,8 @@ class GraphicsViewViewer(QGraphicsView):
|
|||||||
|
|
||||||
def adjustScrollBarsScaled(self, delta):
|
def adjustScrollBarsScaled(self, delta):
|
||||||
"""After scaling with the mouse, update relative to mouse position."""
|
"""After scaling with the mouse, update relative to mouse position."""
|
||||||
self._horizontalScrollBar.setValue(
|
self._horizontalScrollBar.setValue(self._horizontalScrollBar.value() + delta.x())
|
||||||
self._horizontalScrollBar.value() + delta.x())
|
self._verticalScrollBar.setValue(self._verticalScrollBar.value() + delta.y())
|
||||||
self._verticalScrollBar.setValue(
|
|
||||||
self._verticalScrollBar.value() + delta.y())
|
|
||||||
|
|
||||||
def sizeHint(self):
|
def sizeHint(self):
|
||||||
return self.viewport().rect().size()
|
return self.viewport().rect().size()
|
||||||
@ -1356,15 +1338,13 @@ class GraphicsViewViewer(QGraphicsView):
|
|||||||
def adjustScrollBarsFactor(self, factor):
|
def adjustScrollBarsFactor(self, factor):
|
||||||
"""After scaling, no mouse position, default to center."""
|
"""After scaling, no mouse position, default to center."""
|
||||||
self._horizontalScrollBar.setValue(
|
self._horizontalScrollBar.setValue(
|
||||||
int(factor * self._horizontalScrollBar.value()
|
int(factor * self._horizontalScrollBar.value() + ((factor - 1) * self._horizontalScrollBar.pageStep() / 2))
|
||||||
+ ((factor - 1) * self._horizontalScrollBar.pageStep() / 2)))
|
)
|
||||||
self._verticalScrollBar.setValue(
|
self._verticalScrollBar.setValue(
|
||||||
int(factor * self._verticalScrollBar.value()
|
int(factor * self._verticalScrollBar.value() + ((factor - 1) * self._verticalScrollBar.pageStep() / 2))
|
||||||
+ ((factor - 1) * self._verticalScrollBar.pageStep() / 2)))
|
)
|
||||||
|
|
||||||
def adjustScrollBarsAuto(self):
|
def adjustScrollBarsAuto(self):
|
||||||
"""After panning, update accordingly."""
|
"""After panning, update accordingly."""
|
||||||
self.horizontalScrollBar().setValue(
|
self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - self._mousePanningDelta.x())
|
||||||
self.horizontalScrollBar().value() - self._mousePanningDelta.x())
|
self.verticalScrollBar().setValue(self.verticalScrollBar().value() - self._mousePanningDelta.y())
|
||||||
self.verticalScrollBar().setValue(
|
|
||||||
self.verticalScrollBar().value() - self._mousePanningDelta.y())
|
|
||||||
|
@ -30,13 +30,15 @@ class File(PhotoBase):
|
|||||||
image = QImage(str(self.path))
|
image = QImage(str(self.path))
|
||||||
image = image.convertToFormat(QImage.Format_RGB888)
|
image = image.convertToFormat(QImage.Format_RGB888)
|
||||||
if type(orientation) == str:
|
if type(orientation) == str:
|
||||||
logging.warning("Orientation for file '%s' was a str '%s', not an int.",
|
logging.warning("Orientation for file '%s' was a str '%s', not an int.", str(self.path), orientation)
|
||||||
str(self.path), orientation)
|
|
||||||
try:
|
try:
|
||||||
orientation = int(orientation)
|
orientation = int(orientation)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception("Skipping transformation because could not \
|
logging.exception(
|
||||||
convert str to int. %s", e)
|
"Skipping transformation because could not \
|
||||||
|
convert str to int. %s",
|
||||||
|
e,
|
||||||
|
)
|
||||||
return getblocks(image, block_count_per_side)
|
return getblocks(image, block_count_per_side)
|
||||||
# MYSTERY TO SOLVE: For reasons I cannot explain, orientations 5 and 7 don't work for
|
# 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
|
# duplicate scanning. The transforms seems to work fine (if I try to save the image after
|
||||||
|
@ -21,19 +21,13 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
def _setupPreferenceWidgets(self):
|
def _setupPreferenceWidgets(self):
|
||||||
self._setupFilterHardnessBox()
|
self._setupFilterHardnessBox()
|
||||||
self.widgetsVLayout.addLayout(self.filterHardnessHLayout)
|
self.widgetsVLayout.addLayout(self.filterHardnessHLayout)
|
||||||
self._setupAddCheckbox(
|
self._setupAddCheckbox("matchScaledBox", tr("Match pictures of different dimensions"))
|
||||||
"matchScaledBox", tr("Match pictures of different dimensions")
|
|
||||||
)
|
|
||||||
self.widgetsVLayout.addWidget(self.matchScaledBox)
|
self.widgetsVLayout.addWidget(self.matchScaledBox)
|
||||||
self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind"))
|
self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind"))
|
||||||
self.widgetsVLayout.addWidget(self.mixFileKindBox)
|
self.widgetsVLayout.addWidget(self.mixFileKindBox)
|
||||||
self._setupAddCheckbox(
|
self._setupAddCheckbox("useRegexpBox", tr("Use regular expressions when filtering"))
|
||||||
"useRegexpBox", tr("Use regular expressions when filtering")
|
|
||||||
)
|
|
||||||
self.widgetsVLayout.addWidget(self.useRegexpBox)
|
self.widgetsVLayout.addWidget(self.useRegexpBox)
|
||||||
self._setupAddCheckbox(
|
self._setupAddCheckbox("removeEmptyFoldersBox", tr("Remove empty folders on delete or move"))
|
||||||
"removeEmptyFoldersBox", tr("Remove empty folders on delete or move")
|
|
||||||
)
|
|
||||||
self.widgetsVLayout.addWidget(self.removeEmptyFoldersBox)
|
self.widgetsVLayout.addWidget(self.removeEmptyFoldersBox)
|
||||||
self._setupAddCheckbox(
|
self._setupAddCheckbox(
|
||||||
"ignoreHardlinkMatches",
|
"ignoreHardlinkMatches",
|
||||||
@ -52,45 +46,37 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
|
|
||||||
def _setupDisplayPage(self):
|
def _setupDisplayPage(self):
|
||||||
super()._setupDisplayPage()
|
super()._setupDisplayPage()
|
||||||
self._setupAddCheckbox("details_dialog_override_theme_icons",
|
self._setupAddCheckbox("details_dialog_override_theme_icons", tr("Override theme icons in viewer toolbar"))
|
||||||
tr("Override theme icons in viewer toolbar"))
|
|
||||||
self.details_dialog_override_theme_icons.setToolTip(
|
self.details_dialog_override_theme_icons.setToolTip(
|
||||||
tr("Use our own internal icons instead of those provided by the theme engine"))
|
tr("Use our own internal icons instead of those provided by the theme engine")
|
||||||
|
)
|
||||||
# Prevent changing this on platforms where themes are unpredictable
|
# Prevent changing this on platforms where themes are unpredictable
|
||||||
self.details_dialog_override_theme_icons.setEnabled(False if not ISLINUX else True)
|
self.details_dialog_override_theme_icons.setEnabled(False if not ISLINUX else True)
|
||||||
# Insert this right after the vertical title bar option
|
# Insert this right after the vertical title bar option
|
||||||
index = self.details_groupbox_layout.indexOf(self.details_dialog_vertical_titlebar)
|
index = self.details_groupbox_layout.indexOf(self.details_dialog_vertical_titlebar)
|
||||||
self.details_groupbox_layout.insertWidget(
|
self.details_groupbox_layout.insertWidget(index + 1, self.details_dialog_override_theme_icons)
|
||||||
index + 1, self.details_dialog_override_theme_icons)
|
self._setupAddCheckbox("details_dialog_viewers_show_scrollbars", tr("Show scrollbars in image viewers"))
|
||||||
self._setupAddCheckbox("details_dialog_viewers_show_scrollbars",
|
|
||||||
tr("Show scrollbars in image viewers"))
|
|
||||||
self.details_dialog_viewers_show_scrollbars.setToolTip(
|
self.details_dialog_viewers_show_scrollbars.setToolTip(
|
||||||
tr("When the image displayed doesn't fit the viewport, \
|
tr(
|
||||||
show scrollbars to span the view around"))
|
"When the image displayed doesn't fit the viewport, \
|
||||||
self.details_groupbox_layout.insertWidget(
|
show scrollbars to span the view around"
|
||||||
index + 2, self.details_dialog_viewers_show_scrollbars)
|
)
|
||||||
|
)
|
||||||
|
self.details_groupbox_layout.insertWidget(index + 2, self.details_dialog_viewers_show_scrollbars)
|
||||||
|
|
||||||
def _load(self, prefs, setchecked, section):
|
def _load(self, prefs, setchecked, section):
|
||||||
setchecked(self.matchScaledBox, prefs.match_scaled)
|
setchecked(self.matchScaledBox, prefs.match_scaled)
|
||||||
self.cacheTypeRadio.selected_index = (
|
self.cacheTypeRadio.selected_index = 1 if prefs.picture_cache_type == "shelve" else 0
|
||||||
1 if prefs.picture_cache_type == "shelve" else 0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update UI state based on selected scan type
|
# Update UI state based on selected scan type
|
||||||
scan_type = prefs.get_scan_type(AppMode.Picture)
|
scan_type = prefs.get_scan_type(AppMode.Picture)
|
||||||
fuzzy_scan = scan_type == ScanType.FuzzyBlock
|
fuzzy_scan = scan_type == ScanType.FuzzyBlock
|
||||||
self.filterHardnessSlider.setEnabled(fuzzy_scan)
|
self.filterHardnessSlider.setEnabled(fuzzy_scan)
|
||||||
setchecked(self.details_dialog_override_theme_icons,
|
setchecked(self.details_dialog_override_theme_icons, prefs.details_dialog_override_theme_icons)
|
||||||
prefs.details_dialog_override_theme_icons)
|
setchecked(self.details_dialog_viewers_show_scrollbars, prefs.details_dialog_viewers_show_scrollbars)
|
||||||
setchecked(self.details_dialog_viewers_show_scrollbars,
|
|
||||||
prefs.details_dialog_viewers_show_scrollbars)
|
|
||||||
|
|
||||||
def _save(self, prefs, ischecked):
|
def _save(self, prefs, ischecked):
|
||||||
prefs.match_scaled = ischecked(self.matchScaledBox)
|
prefs.match_scaled = ischecked(self.matchScaledBox)
|
||||||
prefs.picture_cache_type = (
|
prefs.picture_cache_type = "shelve" if self.cacheTypeRadio.selected_index == 1 else "sqlite"
|
||||||
"shelve" if self.cacheTypeRadio.selected_index == 1 else "sqlite"
|
prefs.details_dialog_override_theme_icons = ischecked(self.details_dialog_override_theme_icons)
|
||||||
)
|
prefs.details_dialog_viewers_show_scrollbars = ischecked(self.details_dialog_viewers_show_scrollbars)
|
||||||
prefs.details_dialog_override_theme_icons =\
|
|
||||||
ischecked(self.details_dialog_override_theme_icons)
|
|
||||||
prefs.details_dialog_viewers_show_scrollbars =\
|
|
||||||
ischecked(self.details_dialog_viewers_show_scrollbars)
|
|
||||||
|
@ -20,9 +20,7 @@ class Preferences(PreferencesBase):
|
|||||||
get = self.get_value
|
get = self.get_value
|
||||||
self.filter_hardness = get("FilterHardness", self.filter_hardness)
|
self.filter_hardness = get("FilterHardness", self.filter_hardness)
|
||||||
self.mix_file_kind = get("MixFileKind", self.mix_file_kind)
|
self.mix_file_kind = get("MixFileKind", self.mix_file_kind)
|
||||||
self.ignore_hardlink_matches = get(
|
self.ignore_hardlink_matches = get("IgnoreHardlinkMatches", self.ignore_hardlink_matches)
|
||||||
"IgnoreHardlinkMatches", self.ignore_hardlink_matches
|
|
||||||
)
|
|
||||||
self.use_regexp = get("UseRegexp", self.use_regexp)
|
self.use_regexp = get("UseRegexp", self.use_regexp)
|
||||||
self.remove_empty_folders = get("RemoveEmptyFolders", self.remove_empty_folders)
|
self.remove_empty_folders = get("RemoveEmptyFolders", self.remove_empty_folders)
|
||||||
self.debug_mode = get("DebugMode", self.debug_mode)
|
self.debug_mode = get("DebugMode", self.debug_mode)
|
||||||
@ -34,37 +32,36 @@ class Preferences(PreferencesBase):
|
|||||||
|
|
||||||
self.tableFontSize = get("TableFontSize", self.tableFontSize)
|
self.tableFontSize = get("TableFontSize", self.tableFontSize)
|
||||||
self.reference_bold_font = get("ReferenceBoldFont", self.reference_bold_font)
|
self.reference_bold_font = get("ReferenceBoldFont", self.reference_bold_font)
|
||||||
self.details_dialog_titlebar_enabled = get("DetailsDialogTitleBarEnabled",
|
self.details_dialog_titlebar_enabled = get("DetailsDialogTitleBarEnabled", self.details_dialog_titlebar_enabled)
|
||||||
self.details_dialog_titlebar_enabled)
|
self.details_dialog_vertical_titlebar = get(
|
||||||
self.details_dialog_vertical_titlebar = get("DetailsDialogVerticalTitleBar",
|
"DetailsDialogVerticalTitleBar", self.details_dialog_vertical_titlebar
|
||||||
self.details_dialog_vertical_titlebar)
|
)
|
||||||
# On Windows and MacOS, use internal icons by default
|
# On Windows and MacOS, use internal icons by default
|
||||||
self.details_dialog_override_theme_icons =\
|
self.details_dialog_override_theme_icons = (
|
||||||
get("DetailsDialogOverrideThemeIcons",
|
get("DetailsDialogOverrideThemeIcons", self.details_dialog_override_theme_icons) if ISLINUX else True
|
||||||
self.details_dialog_override_theme_icons) if ISLINUX else True
|
|
||||||
self.details_table_delta_foreground_color =\
|
|
||||||
get("DetailsTableDeltaForegroundColor", self.details_table_delta_foreground_color)
|
|
||||||
self.details_dialog_viewers_show_scrollbars =\
|
|
||||||
get("DetailsDialogViewersShowScrollbars", self.details_dialog_viewers_show_scrollbars)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
self.resultWindowIsMaximized = get(
|
|
||||||
"ResultWindowIsMaximized", self.resultWindowIsMaximized
|
|
||||||
)
|
)
|
||||||
|
self.details_table_delta_foreground_color = get(
|
||||||
|
"DetailsTableDeltaForegroundColor", self.details_table_delta_foreground_color
|
||||||
|
)
|
||||||
|
self.details_dialog_viewers_show_scrollbars = get(
|
||||||
|
"DetailsDialogViewersShowScrollbars", self.details_dialog_viewers_show_scrollbars
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
self.resultWindowIsMaximized = get("ResultWindowIsMaximized", self.resultWindowIsMaximized)
|
||||||
self.resultWindowRect = self.get_rect("ResultWindowRect", self.resultWindowRect)
|
self.resultWindowRect = self.get_rect("ResultWindowRect", self.resultWindowRect)
|
||||||
self.mainWindowIsMaximized = get(
|
self.mainWindowIsMaximized = get("MainWindowIsMaximized", self.mainWindowIsMaximized)
|
||||||
"MainWindowIsMaximized", self.mainWindowIsMaximized
|
|
||||||
)
|
|
||||||
self.mainWindowRect = self.get_rect("MainWindowRect", self.mainWindowRect)
|
self.mainWindowRect = self.get_rect("MainWindowRect", self.mainWindowRect)
|
||||||
self.directoriesWindowRect = self.get_rect(
|
self.directoriesWindowRect = self.get_rect("DirectoriesWindowRect", self.directoriesWindowRect)
|
||||||
"DirectoriesWindowRect", self.directoriesWindowRect
|
|
||||||
)
|
|
||||||
|
|
||||||
self.recentResults = get("RecentResults", self.recentResults)
|
self.recentResults = get("RecentResults", self.recentResults)
|
||||||
self.recentFolders = get("RecentFolders", self.recentFolders)
|
self.recentFolders = get("RecentFolders", self.recentFolders)
|
||||||
|
@ -79,12 +79,8 @@ class PrioritizeDialog(QDialog):
|
|||||||
super().__init__(parent, flags, **kwargs)
|
super().__init__(parent, flags, **kwargs)
|
||||||
self._setupUi()
|
self._setupUi()
|
||||||
self.model = PrioritizeDialogModel(app=app.model)
|
self.model = PrioritizeDialogModel(app=app.model)
|
||||||
self.categoryList = ComboboxModel(
|
self.categoryList = ComboboxModel(model=self.model.category_list, view=self.categoryCombobox)
|
||||||
model=self.model.category_list, view=self.categoryCombobox
|
self.criteriaList = ListviewModel(model=self.model.criteria_list, view=self.criteriaListView)
|
||||||
)
|
|
||||||
self.criteriaList = ListviewModel(
|
|
||||||
model=self.model.criteria_list, view=self.criteriaListView
|
|
||||||
)
|
|
||||||
self.prioritizationList = PrioritizationList(
|
self.prioritizationList = PrioritizationList(
|
||||||
model=self.model.prioritization_list, view=self.prioritizationListView
|
model=self.model.prioritization_list, view=self.prioritizationListView
|
||||||
)
|
)
|
||||||
@ -112,12 +108,8 @@ class PrioritizeDialog(QDialog):
|
|||||||
self.categoryCombobox = QComboBox()
|
self.categoryCombobox = QComboBox()
|
||||||
self.criteriaListView = QListView()
|
self.criteriaListView = QListView()
|
||||||
self.criteriaListView.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
self.criteriaListView.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||||
self.addCriteriaButton = QPushButton(
|
self.addCriteriaButton = QPushButton(self.style().standardIcon(QStyle.SP_ArrowRight), "")
|
||||||
self.style().standardIcon(QStyle.SP_ArrowRight), ""
|
self.removeCriteriaButton = QPushButton(self.style().standardIcon(QStyle.SP_ArrowLeft), "")
|
||||||
)
|
|
||||||
self.removeCriteriaButton = QPushButton(
|
|
||||||
self.style().standardIcon(QStyle.SP_ArrowLeft), ""
|
|
||||||
)
|
|
||||||
self.prioritizationListView = QListView()
|
self.prioritizationListView = QListView()
|
||||||
self.prioritizationListView.setAcceptDrops(True)
|
self.prioritizationListView.setAcceptDrops(True)
|
||||||
self.prioritizationListView.setDragEnabled(True)
|
self.prioritizationListView.setDragEnabled(True)
|
||||||
|
@ -295,9 +295,7 @@ class ResultWindow(QMainWindow):
|
|||||||
if menu.actions():
|
if menu.actions():
|
||||||
menu.clear()
|
menu.clear()
|
||||||
self._column_actions = []
|
self._column_actions = []
|
||||||
for index, (display, visible) in enumerate(
|
for index, (display, visible) in enumerate(self.app.model.result_table.columns.menu_items()):
|
||||||
self.app.model.result_table.columns.menu_items()
|
|
||||||
):
|
|
||||||
action = menu.addAction(display)
|
action = menu.addAction(display)
|
||||||
action.setCheckable(True)
|
action.setCheckable(True)
|
||||||
action.setChecked(visible)
|
action.setChecked(visible)
|
||||||
|
@ -34,15 +34,11 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self.verticalLayout_4 = QVBoxLayout(self.widget)
|
self.verticalLayout_4 = QVBoxLayout(self.widget)
|
||||||
self._setupAddCheckbox("wordWeightingBox", tr("Word weighting"), self.widget)
|
self._setupAddCheckbox("wordWeightingBox", tr("Word weighting"), self.widget)
|
||||||
self.verticalLayout_4.addWidget(self.wordWeightingBox)
|
self.verticalLayout_4.addWidget(self.wordWeightingBox)
|
||||||
self._setupAddCheckbox(
|
self._setupAddCheckbox("matchSimilarBox", tr("Match similar words"), self.widget)
|
||||||
"matchSimilarBox", tr("Match similar words"), self.widget
|
|
||||||
)
|
|
||||||
self.verticalLayout_4.addWidget(self.matchSimilarBox)
|
self.verticalLayout_4.addWidget(self.matchSimilarBox)
|
||||||
self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind"), self.widget)
|
self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind"), self.widget)
|
||||||
self.verticalLayout_4.addWidget(self.mixFileKindBox)
|
self.verticalLayout_4.addWidget(self.mixFileKindBox)
|
||||||
self._setupAddCheckbox(
|
self._setupAddCheckbox("useRegexpBox", tr("Use regular expressions when filtering"), self.widget)
|
||||||
"useRegexpBox", tr("Use regular expressions when filtering"), self.widget
|
|
||||||
)
|
|
||||||
self.verticalLayout_4.addWidget(self.useRegexpBox)
|
self.verticalLayout_4.addWidget(self.useRegexpBox)
|
||||||
self._setupAddCheckbox(
|
self._setupAddCheckbox(
|
||||||
"removeEmptyFoldersBox",
|
"removeEmptyFoldersBox",
|
||||||
@ -51,17 +47,13 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
)
|
)
|
||||||
self.verticalLayout_4.addWidget(self.removeEmptyFoldersBox)
|
self.verticalLayout_4.addWidget(self.removeEmptyFoldersBox)
|
||||||
self.horizontalLayout_2 = QHBoxLayout()
|
self.horizontalLayout_2 = QHBoxLayout()
|
||||||
self._setupAddCheckbox(
|
self._setupAddCheckbox("ignoreSmallFilesBox", tr("Ignore files smaller than"), self.widget)
|
||||||
"ignoreSmallFilesBox", tr("Ignore files smaller than"), self.widget
|
|
||||||
)
|
|
||||||
self.horizontalLayout_2.addWidget(self.ignoreSmallFilesBox)
|
self.horizontalLayout_2.addWidget(self.ignoreSmallFilesBox)
|
||||||
self.sizeThresholdSpinBox = QSpinBox(self.widget)
|
self.sizeThresholdSpinBox = QSpinBox(self.widget)
|
||||||
sizePolicy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
|
sizePolicy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
|
||||||
sizePolicy.setHorizontalStretch(0)
|
sizePolicy.setHorizontalStretch(0)
|
||||||
sizePolicy.setVerticalStretch(0)
|
sizePolicy.setVerticalStretch(0)
|
||||||
sizePolicy.setHeightForWidth(
|
sizePolicy.setHeightForWidth(self.sizeThresholdSpinBox.sizePolicy().hasHeightForWidth())
|
||||||
self.sizeThresholdSpinBox.sizePolicy().hasHeightForWidth()
|
|
||||||
)
|
|
||||||
self.sizeThresholdSpinBox.setSizePolicy(sizePolicy)
|
self.sizeThresholdSpinBox.setSizePolicy(sizePolicy)
|
||||||
self.sizeThresholdSpinBox.setMaximumSize(QSize(100, 16777215))
|
self.sizeThresholdSpinBox.setMaximumSize(QSize(100, 16777215))
|
||||||
self.sizeThresholdSpinBox.setRange(0, 1000000)
|
self.sizeThresholdSpinBox.setRange(0, 1000000)
|
||||||
@ -96,9 +88,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self.widget,
|
self.widget,
|
||||||
)
|
)
|
||||||
self.verticalLayout_4.addWidget(self.ignoreHardlinkMatches)
|
self.verticalLayout_4.addWidget(self.ignoreHardlinkMatches)
|
||||||
self._setupAddCheckbox(
|
self._setupAddCheckbox("debugModeBox", tr("Debug mode (restart required)"), self.widget)
|
||||||
"debugModeBox", tr("Debug mode (restart required)"), self.widget
|
|
||||||
)
|
|
||||||
self.verticalLayout_4.addWidget(self.debugModeBox)
|
self.verticalLayout_4.addWidget(self.debugModeBox)
|
||||||
self.widgetsVLayout.addWidget(self.widget)
|
self.widgetsVLayout.addWidget(self.widget)
|
||||||
self._setupBottomPart()
|
self._setupBottomPart()
|
||||||
|
@ -19,6 +19,7 @@ from .directories_dialog import DirectoriesDialog
|
|||||||
from .result_window import ResultWindow
|
from .result_window import ResultWindow
|
||||||
from .ignore_list_dialog import IgnoreListDialog
|
from .ignore_list_dialog import IgnoreListDialog
|
||||||
from .exclude_list_dialog import ExcludeListDialog
|
from .exclude_list_dialog import ExcludeListDialog
|
||||||
|
|
||||||
tr = trget("ui")
|
tr = trget("ui")
|
||||||
|
|
||||||
|
|
||||||
@ -135,16 +136,15 @@ class TabWindow(QMainWindow):
|
|||||||
action.setEnabled(True)
|
action.setEnabled(True)
|
||||||
|
|
||||||
self.app.directories_dialog.actionShowResultsWindow.setEnabled(
|
self.app.directories_dialog.actionShowResultsWindow.setEnabled(
|
||||||
False if page_type == "ResultWindow"
|
False if page_type == "ResultWindow" else self.app.resultWindow is not None
|
||||||
else self.app.resultWindow is not None)
|
)
|
||||||
self.app.actionIgnoreList.setEnabled(
|
self.app.actionIgnoreList.setEnabled(
|
||||||
True if self.app.ignoreListDialog is not None
|
True if self.app.ignoreListDialog is not None and not page_type == "IgnoreListDialog" else False
|
||||||
and not page_type == "IgnoreListDialog" else False)
|
)
|
||||||
self.app.actionDirectoriesWindow.setEnabled(
|
self.app.actionDirectoriesWindow.setEnabled(False if page_type == "DirectoriesDialog" else True)
|
||||||
False if page_type == "DirectoriesDialog" else True)
|
|
||||||
self.app.actionExcludeList.setEnabled(
|
self.app.actionExcludeList.setEnabled(
|
||||||
True if self.app.excludeListDialog is not None
|
True if self.app.excludeListDialog is not None and not page_type == "ExcludeListDialog" else False
|
||||||
and not page_type == "ExcludeListDialog" else False)
|
)
|
||||||
|
|
||||||
self.previous_widget_actions = active_widget.specific_actions
|
self.previous_widget_actions = active_widget.specific_actions
|
||||||
self.last_index = current_index
|
self.last_index = current_index
|
||||||
@ -176,8 +176,7 @@ class TabWindow(QMainWindow):
|
|||||||
index = self.tabWidget.addTab(page, title)
|
index = self.tabWidget.addTab(page, title)
|
||||||
# index = self.tabWidget.insertTab(-1, page, title)
|
# index = self.tabWidget.insertTab(-1, page, title)
|
||||||
if isinstance(page, DirectoriesDialog):
|
if isinstance(page, DirectoriesDialog):
|
||||||
self.tabWidget.tabBar().setTabButton(
|
self.tabWidget.tabBar().setTabButton(index, QTabBar.RightSide, None)
|
||||||
index, QTabBar.RightSide, None)
|
|
||||||
if switch:
|
if switch:
|
||||||
self.setCurrentIndex(index)
|
self.setCurrentIndex(index)
|
||||||
return index
|
return index
|
||||||
@ -250,6 +249,7 @@ class TabWindow(QMainWindow):
|
|||||||
class TabBarWindow(TabWindow):
|
class TabBarWindow(TabWindow):
|
||||||
"""Implementation which uses a separate QTabBar and QStackedWidget.
|
"""Implementation which uses a separate QTabBar and QStackedWidget.
|
||||||
The Tab bar is placed next to the menu bar to save real estate."""
|
The Tab bar is placed next to the menu bar to save real estate."""
|
||||||
|
|
||||||
def __init__(self, app, **kwargs):
|
def __init__(self, app, **kwargs):
|
||||||
super().__init__(app, **kwargs)
|
super().__init__(app, **kwargs)
|
||||||
|
|
||||||
@ -286,8 +286,7 @@ class TabBarWindow(TabWindow):
|
|||||||
self.tabBar.insertTab(stack_index, title)
|
self.tabBar.insertTab(stack_index, title)
|
||||||
|
|
||||||
if isinstance(page, DirectoriesDialog):
|
if isinstance(page, DirectoriesDialog):
|
||||||
self.tabBar.setTabButton(
|
self.tabBar.setTabButton(stack_index, QTabBar.RightSide, None)
|
||||||
stack_index, QTabBar.RightSide, None)
|
|
||||||
if switch: # switch to the added tab immediately upon creation
|
if switch: # switch to the added tab immediately upon creation
|
||||||
self.setTabIndex(stack_index)
|
self.setTabIndex(stack_index)
|
||||||
return stack_index
|
return stack_index
|
||||||
|
@ -25,12 +25,7 @@ tr = trget("qtlib")
|
|||||||
|
|
||||||
class AboutBox(QDialog):
|
class AboutBox(QDialog):
|
||||||
def __init__(self, parent, app, **kwargs):
|
def __init__(self, parent, app, **kwargs):
|
||||||
flags = (
|
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.MSWindowsFixedSizeDialogHint
|
||||||
Qt.CustomizeWindowHint
|
|
||||||
| Qt.WindowTitleHint
|
|
||||||
| Qt.WindowSystemMenuHint
|
|
||||||
| Qt.MSWindowsFixedSizeDialogHint
|
|
||||||
)
|
|
||||||
super().__init__(parent, flags, **kwargs)
|
super().__init__(parent, flags, **kwargs)
|
||||||
self.app = app
|
self.app = app
|
||||||
self._setupUi()
|
self._setupUi()
|
||||||
@ -39,9 +34,7 @@ class AboutBox(QDialog):
|
|||||||
self.buttonBox.rejected.connect(self.reject)
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
self.setWindowTitle(
|
self.setWindowTitle(tr("About {}").format(QCoreApplication.instance().applicationName()))
|
||||||
tr("About {}").format(QCoreApplication.instance().applicationName())
|
|
||||||
)
|
|
||||||
self.resize(400, 290)
|
self.resize(400, 290)
|
||||||
sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||||
sizePolicy.setHorizontalStretch(0)
|
sizePolicy.setHorizontalStretch(0)
|
||||||
@ -61,9 +54,7 @@ class AboutBox(QDialog):
|
|||||||
self.nameLabel.setText(QCoreApplication.instance().applicationName())
|
self.nameLabel.setText(QCoreApplication.instance().applicationName())
|
||||||
self.verticalLayout.addWidget(self.nameLabel)
|
self.verticalLayout.addWidget(self.nameLabel)
|
||||||
self.versionLabel = QLabel(self)
|
self.versionLabel = QLabel(self)
|
||||||
self.versionLabel.setText(
|
self.versionLabel.setText(tr("Version {}").format(QCoreApplication.instance().applicationVersion()))
|
||||||
tr("Version {}").format(QCoreApplication.instance().applicationVersion())
|
|
||||||
)
|
|
||||||
self.verticalLayout.addWidget(self.versionLabel)
|
self.verticalLayout.addWidget(self.versionLabel)
|
||||||
self.label_3 = QLabel(self)
|
self.label_3 = QLabel(self)
|
||||||
self.verticalLayout.addWidget(self.label_3)
|
self.verticalLayout.addWidget(self.label_3)
|
||||||
|
@ -62,9 +62,7 @@ class Columns:
|
|||||||
# See moneyguru #14 and #15. This was added in order to allow automatic resizing of columns.
|
# See moneyguru #14 and #15. This was added in order to allow automatic resizing of columns.
|
||||||
for column in self.model.column_list:
|
for column in self.model.column_list:
|
||||||
if column.resizeToFit:
|
if column.resizeToFit:
|
||||||
self._headerView.setSectionResizeMode(
|
self._headerView.setSectionResizeMode(column.logical_index, QHeaderView.ResizeToContents)
|
||||||
column.logical_index, QHeaderView.ResizeToContents
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Public
|
# --- Public
|
||||||
def setColumnsWidth(self, widths):
|
def setColumnsWidth(self, widths):
|
||||||
|
@ -34,9 +34,7 @@ class ErrorReportDialog(QDialog):
|
|||||||
self._setupUi()
|
self._setupUi()
|
||||||
name = QCoreApplication.applicationName()
|
name = QCoreApplication.applicationName()
|
||||||
version = QCoreApplication.applicationVersion()
|
version = QCoreApplication.applicationVersion()
|
||||||
errorText = "Application Name: {}\nVersion: {}\n\n{}".format(
|
errorText = "Application Name: {}\nVersion: {}\n\n{}".format(name, version, error)
|
||||||
name, version, error
|
|
||||||
)
|
|
||||||
# Under windows, we end up with an error report without linesep if we don't mangle it
|
# Under windows, we end up with an error report without linesep if we don't mangle it
|
||||||
errorText = errorText.replace("\n", os.linesep)
|
errorText = errorText.replace("\n", os.linesep)
|
||||||
self.errorTextEdit.setPlainText(errorText)
|
self.errorTextEdit.setPlainText(errorText)
|
||||||
|
@ -102,20 +102,14 @@ class SearchEdit(ClearableEdit):
|
|||||||
if not bool(self.text()) and self.inactiveText and not self.hasFocus():
|
if not bool(self.text()) and self.inactiveText and not self.hasFocus():
|
||||||
panel = QStyleOptionFrame()
|
panel = QStyleOptionFrame()
|
||||||
self.initStyleOption(panel)
|
self.initStyleOption(panel)
|
||||||
textRect = self.style().subElementRect(
|
textRect = self.style().subElementRect(QStyle.SE_LineEditContents, panel, self)
|
||||||
QStyle.SE_LineEditContents, panel, self
|
|
||||||
)
|
|
||||||
leftMargin = 2
|
leftMargin = 2
|
||||||
rightMargin = self._clearButton.iconSize().width()
|
rightMargin = self._clearButton.iconSize().width()
|
||||||
textRect.adjust(leftMargin, 0, -rightMargin, 0)
|
textRect.adjust(leftMargin, 0, -rightMargin, 0)
|
||||||
painter = QPainter(self)
|
painter = QPainter(self)
|
||||||
disabledColor = (
|
disabledColor = self.palette().brush(QPalette.Disabled, QPalette.Text).color()
|
||||||
self.palette().brush(QPalette.Disabled, QPalette.Text).color()
|
|
||||||
)
|
|
||||||
painter.setPen(disabledColor)
|
painter.setPen(disabledColor)
|
||||||
painter.drawText(
|
painter.drawText(textRect, Qt.AlignLeft | Qt.AlignVCenter, self.inactiveText)
|
||||||
textRect, Qt.AlignLeft | Qt.AlignVCenter, self.inactiveText
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Event Handlers
|
# --- Event Handlers
|
||||||
def _returnPressed(self):
|
def _returnPressed(self):
|
||||||
@ -123,6 +117,4 @@ class SearchEdit(ClearableEdit):
|
|||||||
self.searchChanged.emit()
|
self.searchChanged.emit()
|
||||||
|
|
||||||
# --- Signals
|
# --- Signals
|
||||||
searchChanged = (
|
searchChanged = pyqtSignal() # Emitted when return is pressed or when the test is cleared
|
||||||
pyqtSignal()
|
|
||||||
) # Emitted when return is pressed or when the test is cleared
|
|
||||||
|
@ -76,15 +76,11 @@ class ComboboxModel(SelectableList):
|
|||||||
class ListviewModel(SelectableList):
|
class ListviewModel(SelectableList):
|
||||||
def __init__(self, model, view, **kwargs):
|
def __init__(self, model, view, **kwargs):
|
||||||
super().__init__(model, view, **kwargs)
|
super().__init__(model, view, **kwargs)
|
||||||
self.view.selectionModel().selectionChanged[
|
self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged)
|
||||||
(QItemSelection, QItemSelection)
|
|
||||||
].connect(self.selectionChanged)
|
|
||||||
|
|
||||||
# --- Override
|
# --- Override
|
||||||
def _updateSelection(self):
|
def _updateSelection(self):
|
||||||
newIndexes = [
|
newIndexes = [modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()]
|
||||||
modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()
|
|
||||||
]
|
|
||||||
if newIndexes != self.model.selected_indexes:
|
if newIndexes != self.model.selected_indexes:
|
||||||
self.model.select(newIndexes)
|
self.model.select(newIndexes)
|
||||||
|
|
||||||
@ -92,14 +88,10 @@ class ListviewModel(SelectableList):
|
|||||||
newSelection = QItemSelection()
|
newSelection = QItemSelection()
|
||||||
for index in self.model.selected_indexes:
|
for index in self.model.selected_indexes:
|
||||||
newSelection.select(self.createIndex(index, 0), self.createIndex(index, 0))
|
newSelection.select(self.createIndex(index, 0), self.createIndex(index, 0))
|
||||||
self.view.selectionModel().select(
|
self.view.selectionModel().select(newSelection, QItemSelectionModel.ClearAndSelect)
|
||||||
newSelection, QItemSelectionModel.ClearAndSelect
|
|
||||||
)
|
|
||||||
if len(newSelection.indexes()):
|
if len(newSelection.indexes()):
|
||||||
currentIndex = newSelection.indexes()[0]
|
currentIndex = newSelection.indexes()[0]
|
||||||
self.view.selectionModel().setCurrentIndex(
|
self.view.selectionModel().setCurrentIndex(currentIndex, QItemSelectionModel.Current)
|
||||||
currentIndex, QItemSelectionModel.Current
|
|
||||||
)
|
|
||||||
self.view.scrollTo(currentIndex)
|
self.view.scrollTo(currentIndex)
|
||||||
|
|
||||||
# --- Events
|
# --- Events
|
||||||
|
@ -29,22 +29,16 @@ class Table(QAbstractTableModel):
|
|||||||
self.view.setModel(self)
|
self.view.setModel(self)
|
||||||
self.model.view = self
|
self.model.view = self
|
||||||
if hasattr(self.model, "columns"):
|
if hasattr(self.model, "columns"):
|
||||||
self.columns = Columns(
|
self.columns = Columns(self.model.columns, self.COLUMNS, view.horizontalHeader())
|
||||||
self.model.columns, self.COLUMNS, view.horizontalHeader()
|
|
||||||
)
|
|
||||||
|
|
||||||
self.view.selectionModel().selectionChanged[
|
self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged)
|
||||||
(QItemSelection, QItemSelection)
|
|
||||||
].connect(self.selectionChanged)
|
|
||||||
|
|
||||||
def _updateModelSelection(self):
|
def _updateModelSelection(self):
|
||||||
# Takes the selection on the view's side and update the model with it.
|
# Takes the selection on the view's side and update the model with it.
|
||||||
# an _updateViewSelection() call will normally result in an _updateModelSelection() call.
|
# an _updateViewSelection() call will normally result in an _updateModelSelection() call.
|
||||||
# to avoid infinite loops, we check that the selection will actually change before calling
|
# to avoid infinite loops, we check that the selection will actually change before calling
|
||||||
# model.select()
|
# model.select()
|
||||||
newIndexes = [
|
newIndexes = [modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()]
|
||||||
modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()
|
|
||||||
]
|
|
||||||
if newIndexes != self.model.selected_indexes:
|
if newIndexes != self.model.selected_indexes:
|
||||||
self.model.select(newIndexes)
|
self.model.select(newIndexes)
|
||||||
|
|
||||||
@ -53,17 +47,11 @@ class Table(QAbstractTableModel):
|
|||||||
newSelection = QItemSelection()
|
newSelection = QItemSelection()
|
||||||
columnCount = self.columnCount(QModelIndex())
|
columnCount = self.columnCount(QModelIndex())
|
||||||
for index in self.model.selected_indexes:
|
for index in self.model.selected_indexes:
|
||||||
newSelection.select(
|
newSelection.select(self.createIndex(index, 0), self.createIndex(index, columnCount - 1))
|
||||||
self.createIndex(index, 0), self.createIndex(index, columnCount - 1)
|
self.view.selectionModel().select(newSelection, QItemSelectionModel.ClearAndSelect)
|
||||||
)
|
|
||||||
self.view.selectionModel().select(
|
|
||||||
newSelection, QItemSelectionModel.ClearAndSelect
|
|
||||||
)
|
|
||||||
if len(newSelection.indexes()):
|
if len(newSelection.indexes()):
|
||||||
currentIndex = newSelection.indexes()[0]
|
currentIndex = newSelection.indexes()[0]
|
||||||
self.view.selectionModel().setCurrentIndex(
|
self.view.selectionModel().setCurrentIndex(currentIndex, QItemSelectionModel.Current)
|
||||||
currentIndex, QItemSelectionModel.Current
|
|
||||||
)
|
|
||||||
self.view.scrollTo(currentIndex)
|
self.view.scrollTo(currentIndex)
|
||||||
|
|
||||||
# --- Data Model methods
|
# --- Data Model methods
|
||||||
|
@ -84,9 +84,7 @@ class DummyNode(TreeNode):
|
|||||||
class TreeModel(QAbstractItemModel, NodeContainer):
|
class TreeModel(QAbstractItemModel, NodeContainer):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._dummyNodes = (
|
self._dummyNodes = set() # dummy nodes' reference have to be kept to avoid segfault
|
||||||
set()
|
|
||||||
) # dummy nodes' reference have to be kept to avoid segfault
|
|
||||||
|
|
||||||
# --- Private
|
# --- Private
|
||||||
def _createDummyNode(self, parent, row):
|
def _createDummyNode(self, parent, row):
|
||||||
@ -98,8 +96,7 @@ class TreeModel(QAbstractItemModel, NodeContainer):
|
|||||||
return DummyNode(self, parent, row)
|
return DummyNode(self, parent, row)
|
||||||
|
|
||||||
def _lastIndex(self):
|
def _lastIndex(self):
|
||||||
"""Index of the very last item in the tree.
|
"""Index of the very last item in the tree."""
|
||||||
"""
|
|
||||||
currentIndex = QModelIndex()
|
currentIndex = QModelIndex()
|
||||||
rowCount = self.rowCount(currentIndex)
|
rowCount = self.rowCount(currentIndex)
|
||||||
while rowCount > 0:
|
while rowCount > 0:
|
||||||
|
Loading…
Reference in New Issue
Block a user