mirror of
https://github.com/arsenetar/dupeguru.git
synced 2025-05-07 09:19:50 +00:00
Added tox configuration
... and fixed pep8 warnings. There's a lot of them that are still ignored, but that's because it's too much of a step to take at once.
This commit is contained in:
parent
24643a9b5d
commit
2166a0996c
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,6 +6,7 @@
|
|||||||
*.waf*
|
*.waf*
|
||||||
.lock-waf*
|
.lock-waf*
|
||||||
.idea
|
.idea
|
||||||
|
.tox
|
||||||
|
|
||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
|
121
build.py
121
build.py
@ -18,10 +18,12 @@ import compileall
|
|||||||
from setuptools import setup, Extension
|
from setuptools import setup, Extension
|
||||||
|
|
||||||
from hscommon import sphinxgen
|
from hscommon import sphinxgen
|
||||||
from hscommon.build import (add_to_pythonpath, print_and_do, copy_packages, filereplace,
|
from hscommon.build import (
|
||||||
|
add_to_pythonpath, print_and_do, copy_packages, filereplace,
|
||||||
get_module_version, move_all, copy_all, OSXAppStructure,
|
get_module_version, move_all, copy_all, OSXAppStructure,
|
||||||
build_cocoalib_xibless, fix_qt_resource_file, build_cocoa_ext, copy_embeddable_python_dylib,
|
build_cocoalib_xibless, fix_qt_resource_file, build_cocoa_ext, copy_embeddable_python_dylib,
|
||||||
collect_stdlib_dependencies, copy)
|
collect_stdlib_dependencies, copy
|
||||||
|
)
|
||||||
from hscommon import loc
|
from hscommon import loc
|
||||||
from hscommon.plat import ISOSX, ISLINUX
|
from hscommon.plat import ISOSX, ISLINUX
|
||||||
from hscommon.util import ensure_folder, delete_files_with_pattern
|
from hscommon.util import ensure_folder, delete_files_with_pattern
|
||||||
@ -29,24 +31,42 @@ from hscommon.util import ensure_folder, delete_files_with_pattern
|
|||||||
def parse_args():
|
def parse_args():
|
||||||
usage = "usage: %prog [options]"
|
usage = "usage: %prog [options]"
|
||||||
parser = OptionParser(usage=usage)
|
parser = OptionParser(usage=usage)
|
||||||
parser.add_option('--clean', action='store_true', dest='clean',
|
parser.add_option(
|
||||||
help="Clean build folder before building")
|
'--clean', action='store_true', dest='clean',
|
||||||
parser.add_option('--doc', action='store_true', dest='doc',
|
help="Clean build folder before building"
|
||||||
help="Build only the help file")
|
)
|
||||||
parser.add_option('--loc', action='store_true', dest='loc',
|
parser.add_option(
|
||||||
help="Build only localization")
|
'--doc', action='store_true', dest='doc',
|
||||||
parser.add_option('--cocoa-ext', action='store_true', dest='cocoa_ext',
|
help="Build only the help file"
|
||||||
help="Build only Cocoa extensions")
|
)
|
||||||
parser.add_option('--cocoa-compile', action='store_true', dest='cocoa_compile',
|
parser.add_option(
|
||||||
help="Build only Cocoa executable")
|
'--loc', action='store_true', dest='loc',
|
||||||
parser.add_option('--xibless', action='store_true', dest='xibless',
|
help="Build only localization"
|
||||||
help="Build only xibless UIs")
|
)
|
||||||
parser.add_option('--updatepot', action='store_true', dest='updatepot',
|
parser.add_option(
|
||||||
help="Generate .pot files from source code.")
|
'--cocoa-ext', action='store_true', dest='cocoa_ext',
|
||||||
parser.add_option('--mergepot', action='store_true', dest='mergepot',
|
help="Build only Cocoa extensions"
|
||||||
help="Update all .po files based on .pot files.")
|
)
|
||||||
parser.add_option('--normpo', action='store_true', dest='normpo',
|
parser.add_option(
|
||||||
help="Normalize all PO files (do this before commit).")
|
'--cocoa-compile', action='store_true', dest='cocoa_compile',
|
||||||
|
help="Build only Cocoa executable"
|
||||||
|
)
|
||||||
|
parser.add_option(
|
||||||
|
'--xibless', action='store_true', dest='xibless',
|
||||||
|
help="Build only xibless UIs"
|
||||||
|
)
|
||||||
|
parser.add_option(
|
||||||
|
'--updatepot', action='store_true', dest='updatepot',
|
||||||
|
help="Generate .pot files from source code."
|
||||||
|
)
|
||||||
|
parser.add_option(
|
||||||
|
'--mergepot', action='store_true', dest='mergepot',
|
||||||
|
help="Update all .po files based on .pot files."
|
||||||
|
)
|
||||||
|
parser.add_option(
|
||||||
|
'--normpo', action='store_true', dest='normpo',
|
||||||
|
help="Normalize all PO files (do this before commit)."
|
||||||
|
)
|
||||||
(options, args) = parser.parse_args()
|
(options, args) = parser.parse_args()
|
||||||
return options
|
return options
|
||||||
|
|
||||||
@ -75,12 +95,20 @@ def build_xibless(edition, dest='cocoa/autogen'):
|
|||||||
('preferences_panel.py', 'PreferencesPanel_UI'),
|
('preferences_panel.py', 'PreferencesPanel_UI'),
|
||||||
]
|
]
|
||||||
for srcname, dstname in FNPAIRS:
|
for srcname, dstname in FNPAIRS:
|
||||||
xibless.generate(op.join('cocoa', 'base', 'ui', srcname), op.join(dest, dstname),
|
xibless.generate(
|
||||||
localizationTable='Localizable', args={'edition': edition})
|
op.join('cocoa', 'base', 'ui', srcname), op.join(dest, dstname),
|
||||||
|
localizationTable='Localizable', args={'edition': edition}
|
||||||
|
)
|
||||||
if edition == 'pe':
|
if edition == 'pe':
|
||||||
xibless.generate('cocoa/pe/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'), localizationTable='Localizable')
|
xibless.generate(
|
||||||
|
'cocoa/pe/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'),
|
||||||
|
localizationTable='Localizable'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
xibless.generate('cocoa/base/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'), localizationTable='Localizable')
|
xibless.generate(
|
||||||
|
'cocoa/base/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'),
|
||||||
|
localizationTable='Localizable'
|
||||||
|
)
|
||||||
|
|
||||||
def build_cocoa(edition, dev):
|
def build_cocoa(edition, dev):
|
||||||
print("Creating OS X app structure")
|
print("Creating OS X app structure")
|
||||||
@ -119,7 +147,7 @@ def build_cocoa(edition, dev):
|
|||||||
if edition == 'pe':
|
if edition == 'pe':
|
||||||
# ModuleFinder can't seem to correctly detect the multiprocessing dependency, so we have
|
# ModuleFinder can't seem to correctly detect the multiprocessing dependency, so we have
|
||||||
# to manually specify it.
|
# to manually specify it.
|
||||||
extra_deps=['multiprocessing']
|
extra_deps = ['multiprocessing']
|
||||||
collect_stdlib_dependencies('build/dg_cocoa.py', pydep_folder, extra_deps=extra_deps)
|
collect_stdlib_dependencies('build/dg_cocoa.py', pydep_folder, extra_deps=extra_deps)
|
||||||
del sys.path[0]
|
del sys.path[0]
|
||||||
# Views are not referenced by python code, so they're not found by the collector.
|
# Views are not referenced by python code, so they're not found by the collector.
|
||||||
@ -225,8 +253,10 @@ def build_updatepot():
|
|||||||
os.remove(cocoalib_pot)
|
os.remove(cocoalib_pot)
|
||||||
loc.strings2pot(op.join('cocoalib', 'en.lproj', 'cocoalib.strings'), cocoalib_pot)
|
loc.strings2pot(op.join('cocoalib', 'en.lproj', 'cocoalib.strings'), cocoalib_pot)
|
||||||
print("Enhancing ui.pot with Cocoa's strings files")
|
print("Enhancing ui.pot with Cocoa's strings files")
|
||||||
loc.strings2pot(op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'),
|
loc.strings2pot(
|
||||||
op.join('locale', 'ui.pot'))
|
op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'),
|
||||||
|
op.join('locale', 'ui.pot')
|
||||||
|
)
|
||||||
|
|
||||||
def build_mergepot():
|
def build_mergepot():
|
||||||
print("Updating .po files using .pot files")
|
print("Updating .po files using .pot files")
|
||||||
@ -243,11 +273,15 @@ def build_cocoa_proxy_module():
|
|||||||
print("Building Cocoa Proxy")
|
print("Building Cocoa Proxy")
|
||||||
import objp.p2o
|
import objp.p2o
|
||||||
objp.p2o.generate_python_proxy_code('cocoalib/cocoa/CocoaProxy.h', 'build/CocoaProxy.m')
|
objp.p2o.generate_python_proxy_code('cocoalib/cocoa/CocoaProxy.h', 'build/CocoaProxy.m')
|
||||||
build_cocoa_ext("CocoaProxy", 'cocoalib/cocoa',
|
build_cocoa_ext(
|
||||||
['cocoalib/cocoa/CocoaProxy.m', 'build/CocoaProxy.m', 'build/ObjP.m',
|
"CocoaProxy", 'cocoalib/cocoa',
|
||||||
'cocoalib/HSErrorReportWindow.m', 'cocoa/autogen/HSErrorReportWindow_UI.m'],
|
[
|
||||||
|
'cocoalib/cocoa/CocoaProxy.m', 'build/CocoaProxy.m', 'build/ObjP.m',
|
||||||
|
'cocoalib/HSErrorReportWindow.m', 'cocoa/autogen/HSErrorReportWindow_UI.m'
|
||||||
|
],
|
||||||
['AppKit', 'CoreServices'],
|
['AppKit', 'CoreServices'],
|
||||||
['cocoalib', 'cocoa/autogen'])
|
['cocoalib', 'cocoa/autogen']
|
||||||
|
)
|
||||||
|
|
||||||
def build_cocoa_bridging_interfaces(edition):
|
def build_cocoa_bridging_interfaces(edition):
|
||||||
print("Building Cocoa Bridging Interfaces")
|
print("Building Cocoa Bridging Interfaces")
|
||||||
@ -255,9 +289,11 @@ def build_cocoa_bridging_interfaces(edition):
|
|||||||
import objp.p2o
|
import objp.p2o
|
||||||
add_to_pythonpath('cocoa')
|
add_to_pythonpath('cocoa')
|
||||||
add_to_pythonpath('cocoalib')
|
add_to_pythonpath('cocoalib')
|
||||||
from cocoa.inter import (PyGUIObject, GUIObjectView, PyColumns, ColumnsView, PyOutline,
|
from cocoa.inter import (
|
||||||
|
PyGUIObject, GUIObjectView, PyColumns, ColumnsView, PyOutline,
|
||||||
OutlineView, PySelectableList, SelectableListView, PyTable, TableView, PyBaseApp,
|
OutlineView, PySelectableList, SelectableListView, PyTable, TableView, PyBaseApp,
|
||||||
PyTextField, ProgressWindowView, PyProgressWindow)
|
PyTextField, ProgressWindowView, PyProgressWindow
|
||||||
|
)
|
||||||
from inter.deletion_options import PyDeletionOptions, DeletionOptionsView
|
from inter.deletion_options import PyDeletionOptions, DeletionOptionsView
|
||||||
from inter.details_panel import PyDetailsPanel, DetailsPanelView
|
from inter.details_panel import PyDetailsPanel, DetailsPanelView
|
||||||
from inter.directory_outline import PyDirectoryOutline, DirectoryOutlineView
|
from inter.directory_outline import PyDirectoryOutline, DirectoryOutlineView
|
||||||
@ -269,16 +305,20 @@ def build_cocoa_bridging_interfaces(edition):
|
|||||||
from inter.stats_label import PyStatsLabel, StatsLabelView
|
from inter.stats_label import PyStatsLabel, StatsLabelView
|
||||||
from inter.app import PyDupeGuruBase, DupeGuruView
|
from inter.app import PyDupeGuruBase, DupeGuruView
|
||||||
appmod = importlib.import_module('inter.app_{}'.format(edition))
|
appmod = importlib.import_module('inter.app_{}'.format(edition))
|
||||||
allclasses = [PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp,
|
allclasses = [
|
||||||
|
PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp,
|
||||||
PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog,
|
PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog,
|
||||||
PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuruBase,
|
PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuruBase,
|
||||||
PyTextField, PyProgressWindow, appmod.PyDupeGuru]
|
PyTextField, PyProgressWindow, appmod.PyDupeGuru
|
||||||
|
]
|
||||||
for class_ in allclasses:
|
for class_ in allclasses:
|
||||||
objp.o2p.generate_objc_code(class_, 'cocoa/autogen', inherit=True)
|
objp.o2p.generate_objc_code(class_, 'cocoa/autogen', inherit=True)
|
||||||
allclasses = [GUIObjectView, ColumnsView, OutlineView, SelectableListView, TableView,
|
allclasses = [
|
||||||
|
GUIObjectView, ColumnsView, OutlineView, SelectableListView, TableView,
|
||||||
DetailsPanelView, DirectoryOutlineView, PrioritizeDialogView, PrioritizeListView,
|
DetailsPanelView, DirectoryOutlineView, PrioritizeDialogView, PrioritizeListView,
|
||||||
IgnoreListDialogView, DeletionOptionsView, ResultTableView, StatsLabelView,
|
IgnoreListDialogView, DeletionOptionsView, ResultTableView, StatsLabelView,
|
||||||
ProgressWindowView, DupeGuruView]
|
ProgressWindowView, DupeGuruView
|
||||||
|
]
|
||||||
clsspecs = [objp.o2p.spec_from_python_class(class_) for class_ in allclasses]
|
clsspecs = [objp.o2p.spec_from_python_class(class_) for class_ in allclasses]
|
||||||
objp.p2o.generate_python_proxy_code_from_clsspec(clsspecs, 'build/CocoaViews.m')
|
objp.p2o.generate_python_proxy_code_from_clsspec(clsspecs, 'build/CocoaViews.m')
|
||||||
build_cocoa_ext('CocoaViews', 'cocoa/inter', ['build/CocoaViews.m', 'build/ObjP.m'])
|
build_cocoa_ext('CocoaViews', 'cocoa/inter', ['build/CocoaViews.m', 'build/ObjP.m'])
|
||||||
@ -297,11 +337,12 @@ def build_pe_modules(ui):
|
|||||||
extra_link_args=[
|
extra_link_args=[
|
||||||
"-framework", "CoreFoundation",
|
"-framework", "CoreFoundation",
|
||||||
"-framework", "Foundation",
|
"-framework", "Foundation",
|
||||||
"-framework", "ApplicationServices",]
|
"-framework", "ApplicationServices",
|
||||||
|
]
|
||||||
))
|
))
|
||||||
setup(
|
setup(
|
||||||
script_args = ['build_ext', '--inplace'],
|
script_args=['build_ext', '--inplace'],
|
||||||
ext_modules = exts,
|
ext_modules=exts,
|
||||||
)
|
)
|
||||||
move_all('_block_qt*', op.join('qt', 'pe'))
|
move_all('_block_qt*', op.join('qt', 'pe'))
|
||||||
move_all('_block*', 'core_pe')
|
move_all('_block*', 'core_pe')
|
||||||
|
26
configure.py
26
configure.py
@ -1,12 +1,11 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-12-30
|
# Created On: 2009-12-30
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import sys
|
|
||||||
from optparse import OptionParser
|
from optparse import OptionParser
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@ -29,11 +28,18 @@ def main(options):
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
usage = "usage: %prog [options]"
|
usage = "usage: %prog [options]"
|
||||||
parser = OptionParser(usage=usage)
|
parser = OptionParser(usage=usage)
|
||||||
parser.add_option('--edition', dest='edition',
|
parser.add_option(
|
||||||
help="dupeGuru edition to build (se, me or pe). Default is se.")
|
'--edition', dest='edition',
|
||||||
parser.add_option('--ui', dest='ui',
|
help="dupeGuru edition to build (se, me or pe). Default is se."
|
||||||
help="Type of UI to build. 'qt' or 'cocoa'. Default is determined by your system.")
|
)
|
||||||
parser.add_option('--dev', action='store_true', dest='dev', default=False,
|
parser.add_option(
|
||||||
help="If this flag is set, will configure for dev builds.")
|
'--ui', dest='ui',
|
||||||
|
help="Type of UI to build. 'qt' or 'cocoa'. Default is determined by your system."
|
||||||
|
)
|
||||||
|
parser.add_option(
|
||||||
|
'--dev', action='store_true', dest='dev', default=False,
|
||||||
|
help="If this flag is set, will configure for dev builds."
|
||||||
|
)
|
||||||
(options, args) = parser.parse_args()
|
(options, args) = parser.parse_args()
|
||||||
main(options)
|
main(options)
|
||||||
|
|
||||||
|
39
core/app.py
39
core/app.py
@ -38,8 +38,10 @@ DEBUG_MODE_PREFERENCE = 'DebugMode'
|
|||||||
|
|
||||||
MSG_NO_MARKED_DUPES = tr("There are no marked duplicates. Nothing has been done.")
|
MSG_NO_MARKED_DUPES = tr("There are no marked duplicates. Nothing has been done.")
|
||||||
MSG_NO_SELECTED_DUPES = tr("There are no selected duplicates. Nothing has been done.")
|
MSG_NO_SELECTED_DUPES = tr("There are no selected duplicates. Nothing has been done.")
|
||||||
MSG_MANY_FILES_TO_OPEN = tr("You're about to open many files at once. Depending on what those "
|
MSG_MANY_FILES_TO_OPEN = tr(
|
||||||
"files are opened with, doing so can create quite a mess. Continue?")
|
"You're about to open many files at once. Depending on what those "
|
||||||
|
"files are opened with, doing so can create quite a mess. Continue?"
|
||||||
|
)
|
||||||
|
|
||||||
class DestType:
|
class DestType:
|
||||||
Direct = 0
|
Direct = 0
|
||||||
@ -265,8 +267,10 @@ class DupeGuru(Broadcaster):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_export_data(self):
|
def _get_export_data(self):
|
||||||
columns = [col for col in self.result_table.columns.ordered_columns
|
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):
|
||||||
@ -278,8 +282,10 @@ class DupeGuru(Broadcaster):
|
|||||||
return colnames, rows
|
return colnames, rows
|
||||||
|
|
||||||
def _results_changed(self):
|
def _results_changed(self):
|
||||||
self.selected_dupes = [d for d in self.selected_dupes
|
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=()):
|
||||||
@ -287,7 +293,10 @@ class DupeGuru(Broadcaster):
|
|||||||
try:
|
try:
|
||||||
self.progress_window.run(jobid, title, func, args=args)
|
self.progress_window.run(jobid, title, func, args=args)
|
||||||
except job.JobInProgressError:
|
except job.JobInProgressError:
|
||||||
msg = tr("A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again.")
|
msg = tr(
|
||||||
|
"A previous action is still hanging in there. You can't start a new one yet. Wait "
|
||||||
|
"a few seconds, then try again."
|
||||||
|
)
|
||||||
self.view.show_message(msg)
|
self.view.show_message(msg)
|
||||||
|
|
||||||
def _job_completed(self, jobid):
|
def _job_completed(self, jobid):
|
||||||
@ -439,8 +448,10 @@ class DupeGuru(Broadcaster):
|
|||||||
return
|
return
|
||||||
if not self.deletion_options.show(self.results.mark_count):
|
if not self.deletion_options.show(self.results.mark_count):
|
||||||
return
|
return
|
||||||
args = [self.deletion_options.link_deleted, self.deletion_options.use_hardlinks,
|
args = [
|
||||||
self.deletion_options.direct]
|
self.deletion_options.link_deleted, self.deletion_options.use_hardlinks,
|
||||||
|
self.deletion_options.direct
|
||||||
|
]
|
||||||
logging.debug("Starting deletion job with args %r", args)
|
logging.debug("Starting deletion job with args %r", args)
|
||||||
self._start_job(JobType.Delete, self._do_delete, args=args)
|
self._start_job(JobType.Delete, self._do_delete, args=args)
|
||||||
|
|
||||||
@ -550,8 +561,10 @@ class DupeGuru(Broadcaster):
|
|||||||
# If no group was changed, however, we don't touch the selection.
|
# If no group was changed, however, we don't touch the selection.
|
||||||
if not self.result_table.power_marker:
|
if not self.result_table.power_marker:
|
||||||
if changed_groups:
|
if changed_groups:
|
||||||
self.selected_dupes = [d for d in self.selected_dupes
|
self.selected_dupes = [
|
||||||
if self.results.get_group_of_duplicate(d).ref is d]
|
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:
|
||||||
# If we're in "Dupes Only" mode (previously called Power Marker), things are a bit
|
# If we're in "Dupes Only" mode (previously called Power Marker), things are a bit
|
||||||
@ -604,7 +617,7 @@ class DupeGuru(Broadcaster):
|
|||||||
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.scanner.ignore_list.Filter(lambda f,s:op.exists(f) and op.exists(s))
|
self.scanner.ignore_list.Filter(lambda f, s: op.exists(f) and op.exists(s))
|
||||||
self.ignore_list_dialog.refresh()
|
self.ignore_list_dialog.refresh()
|
||||||
|
|
||||||
def remove_directories(self, indexes):
|
def remove_directories(self, indexes):
|
||||||
@ -641,7 +654,7 @@ class DupeGuru(Broadcaster):
|
|||||||
msg = tr("You are about to remove %d files from results. Continue?")
|
msg = tr("You are about to remove %d files from results. Continue?")
|
||||||
if not self.view.ask_yes_no(msg % self.results.mark_count):
|
if not self.view.ask_yes_no(msg % self.results.mark_count):
|
||||||
return
|
return
|
||||||
self.results.perform_on_marked(lambda x:None, True)
|
self.results.perform_on_marked(lambda x: None, True)
|
||||||
self._results_changed()
|
self._results_changed()
|
||||||
|
|
||||||
def remove_selected(self):
|
def remove_selected(self):
|
||||||
|
@ -62,10 +62,10 @@ class Directories:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __delitem__(self,key):
|
def __delitem__(self, key):
|
||||||
self._dirs.__delitem__(key)
|
self._dirs.__delitem__(key)
|
||||||
|
|
||||||
def __getitem__(self,key):
|
def __getitem__(self, key):
|
||||||
return self._dirs.__getitem__(key)
|
return self._dirs.__getitem__(key)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
@ -95,7 +95,8 @@ class Directories:
|
|||||||
file.is_ref = state == DirectoryState.Reference
|
file.is_ref = state == DirectoryState.Reference
|
||||||
filepaths.add(file.path)
|
filepaths.add(file.path)
|
||||||
yield file
|
yield file
|
||||||
# it's possible that a folder (bundle) gets into the file list. in that case, we don't want to recurse into it
|
# it's possible that a folder (bundle) gets into the file list. in that case, we don't
|
||||||
|
# want to recurse into it
|
||||||
subfolders = [p for p in from_path.listdir() if not p.islink() and p.isdir() and p not in filepaths]
|
subfolders = [p for p in from_path.listdir() if not p.islink() and p.isdir() and p not in filepaths]
|
||||||
for subfolder in subfolders:
|
for subfolder in subfolders:
|
||||||
for file in self._get_files(subfolder, j):
|
for file in self._get_files(subfolder, j):
|
||||||
@ -144,7 +145,7 @@ class Directories:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
subpaths = [p for p in path.listdir() if p.isdir()]
|
subpaths = [p for p in path.listdir() if p.isdir()]
|
||||||
subpaths.sort(key=lambda x:x.name.lower())
|
subpaths.sort(key=lambda x: x.name.lower())
|
||||||
return subpaths
|
return subpaths
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
return []
|
return []
|
||||||
|
@ -17,9 +17,11 @@ from hscommon.util import flatten, multi_replace
|
|||||||
from hscommon.trans import tr
|
from hscommon.trans import tr
|
||||||
from hscommon.jobprogress import job
|
from hscommon.jobprogress import job
|
||||||
|
|
||||||
(WEIGHT_WORDS,
|
(
|
||||||
MATCH_SIMILAR_WORDS,
|
WEIGHT_WORDS,
|
||||||
NO_FIELD_ORDER) = range(3)
|
MATCH_SIMILAR_WORDS,
|
||||||
|
NO_FIELD_ORDER,
|
||||||
|
) = range(3)
|
||||||
|
|
||||||
JOB_REFRESH_RATE = 100
|
JOB_REFRESH_RATE = 100
|
||||||
|
|
||||||
@ -259,6 +261,7 @@ def getmatches_by_contents(files, sizeattr='size', partial=False, j=job.nulljob)
|
|||||||
filesize = getattr(file, sizeattr)
|
filesize = getattr(file, sizeattr)
|
||||||
if filesize:
|
if filesize:
|
||||||
size2files[filesize].add(file)
|
size2files[filesize].add(file)
|
||||||
|
del files
|
||||||
possible_matches = [files for files in size2files.values() if len(files) > 1]
|
possible_matches = [files for files in size2files.values() if len(files) > 1]
|
||||||
del size2files
|
del size2files
|
||||||
result = []
|
result = []
|
||||||
@ -495,7 +498,10 @@ def get_groups(matches, j=job.nulljob):
|
|||||||
matched_files = set(flatten(groups))
|
matched_files = set(flatten(groups))
|
||||||
orphan_matches = []
|
orphan_matches = []
|
||||||
for group in groups:
|
for group in groups:
|
||||||
orphan_matches += set(m for m in group.discard_matches() if not any(obj in matched_files for obj in [m.first, m.second]))
|
orphan_matches += {
|
||||||
|
m for m in group.discard_matches()
|
||||||
|
if not any(obj in matched_files for obj in [m.first, m.second])
|
||||||
|
}
|
||||||
if groups and orphan_matches:
|
if groups and orphan_matches:
|
||||||
groups += get_groups(orphan_matches) # no job, as it isn't supposed to take a long time
|
groups += get_groups(orphan_matches) # no job, as it isn't supposed to take a long time
|
||||||
return groups
|
return groups
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2006/09/16
|
# Created On: 2006/09/16
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import os.path as op
|
import os.path as op
|
||||||
@ -19,56 +19,56 @@ MAIN_TEMPLATE = """
|
|||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
<head>
|
<head>
|
||||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
|
||||||
<title>dupeGuru Results</title>
|
<title>dupeGuru Results</title>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
BODY
|
BODY
|
||||||
{
|
{
|
||||||
background-color:white;
|
background-color:white;
|
||||||
}
|
}
|
||||||
|
|
||||||
BODY,A,P,UL,TABLE,TR,TD
|
BODY,A,P,UL,TABLE,TR,TD
|
||||||
{
|
{
|
||||||
font-family:Tahoma,Arial,sans-serif;
|
font-family:Tahoma,Arial,sans-serif;
|
||||||
font-size:10pt;
|
font-size:10pt;
|
||||||
color: #4477AA;
|
color: #4477AA;
|
||||||
}
|
}
|
||||||
|
|
||||||
TABLE
|
TABLE
|
||||||
{
|
{
|
||||||
background-color: #225588;
|
background-color: #225588;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
TR
|
TR
|
||||||
{
|
{
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
TH
|
TH
|
||||||
{
|
{
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: black;
|
color: black;
|
||||||
background-color: #C8D6E5;
|
background-color: #C8D6E5;
|
||||||
}
|
}
|
||||||
|
|
||||||
TH TD
|
TH TD
|
||||||
{
|
{
|
||||||
color:black;
|
color:black;
|
||||||
}
|
}
|
||||||
|
|
||||||
TD
|
TD
|
||||||
{
|
{
|
||||||
padding-left: 2pt;
|
padding-left: 2pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
TD.rightelem
|
TD.rightelem
|
||||||
{
|
{
|
||||||
text-align:right;
|
text-align:right;
|
||||||
/*padding-left:0pt;*/
|
/*padding-left:0pt;*/
|
||||||
padding-right: 2pt;
|
padding-right: 2pt;
|
||||||
width: 17%;
|
width: 17%;
|
||||||
}
|
}
|
||||||
|
|
||||||
TD.indented
|
TD.indented
|
||||||
@ -78,19 +78,19 @@ TD.indented
|
|||||||
|
|
||||||
H1
|
H1
|
||||||
{
|
{
|
||||||
font-family:"Courier New",monospace;
|
font-family:"Courier New",monospace;
|
||||||
color:#6699CC;
|
color:#6699CC;
|
||||||
font-size:18pt;
|
font-size:18pt;
|
||||||
color:#6da500;
|
color:#6da500;
|
||||||
border-color: #70A0CF;
|
border-color: #70A0CF;
|
||||||
border-width: 1pt;
|
border-width: 1pt;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
margin-top: 16pt;
|
margin-top: 16pt;
|
||||||
margin-left: 5%;
|
margin-left: 5%;
|
||||||
margin-right: 5%;
|
margin-right: 5%;
|
||||||
padding-top: 2pt;
|
padding-top: 2pt;
|
||||||
padding-bottom:2pt;
|
padding-bottom:2pt;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
63
core/fs.py
63
core/fs.py
@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-10-22
|
# Created On: 2009-10-22
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
# This is a fork from hsfs. The reason for this fork is that hsfs has been designed for musicGuru
|
# This is a fork from hsfs. The reason for this fork is that hsfs has been designed for musicGuru
|
||||||
@ -32,6 +32,7 @@ NOT_SET = object()
|
|||||||
|
|
||||||
class FSError(Exception):
|
class FSError(Exception):
|
||||||
cls_message = "An error has occured on '{name}' in '{parent}'"
|
cls_message = "An error has occured on '{name}' in '{parent}'"
|
||||||
|
|
||||||
def __init__(self, fsobject, parent=None):
|
def __init__(self, fsobject, parent=None):
|
||||||
message = self.cls_message
|
message = self.cls_message
|
||||||
if isinstance(fsobject, str):
|
if isinstance(fsobject, str):
|
||||||
@ -42,7 +43,7 @@ class FSError(Exception):
|
|||||||
name = ''
|
name = ''
|
||||||
parentname = str(parent) if parent is not None else ''
|
parentname = str(parent) if parent is not None else ''
|
||||||
Exception.__init__(self, message.format(name=name, parent=parentname))
|
Exception.__init__(self, message.format(name=name, parent=parentname))
|
||||||
|
|
||||||
|
|
||||||
class AlreadyExistsError(FSError):
|
class AlreadyExistsError(FSError):
|
||||||
"The directory or file name we're trying to add already exists"
|
"The directory or file name we're trying to add already exists"
|
||||||
@ -57,7 +58,7 @@ class InvalidDestinationError(FSError):
|
|||||||
cls_message = "'{name}' is an invalid destination for this operation."
|
cls_message = "'{name}' is an invalid destination for this operation."
|
||||||
|
|
||||||
class OperationError(FSError):
|
class OperationError(FSError):
|
||||||
"""A copy/move/delete operation has been called, but the checkup after the
|
"""A copy/move/delete operation has been called, but the checkup after the
|
||||||
operation shows that it didn't work."""
|
operation shows that it didn't work."""
|
||||||
cls_message = "Operation on '{name}' failed."
|
cls_message = "Operation on '{name}' failed."
|
||||||
|
|
||||||
@ -74,15 +75,15 @@ class File:
|
|||||||
# 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.
|
||||||
__slots__ = ('path', 'is_ref', 'words') + tuple(INITIAL_INFO.keys())
|
__slots__ = ('path', 'is_ref', 'words') + tuple(INITIAL_INFO.keys())
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
self.path = path
|
self.path = path
|
||||||
for attrname in self.INITIAL_INFO:
|
for attrname in self.INITIAL_INFO:
|
||||||
setattr(self, attrname, NOT_SET)
|
setattr(self, attrname, NOT_SET)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<{} {}>".format(self.__class__.__name__, str(self.path))
|
return "<{} {}>".format(self.__class__.__name__, str(self.path))
|
||||||
|
|
||||||
def __getattribute__(self, attrname):
|
def __getattribute__(self, attrname):
|
||||||
result = object.__getattribute__(self, attrname)
|
result = object.__getattribute__(self, attrname)
|
||||||
if result is NOT_SET:
|
if result is NOT_SET:
|
||||||
@ -94,12 +95,12 @@ class File:
|
|||||||
if result is NOT_SET:
|
if result is NOT_SET:
|
||||||
result = self.INITIAL_INFO[attrname]
|
result = self.INITIAL_INFO[attrname]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
#This offset is where we should start reading the file to get a partial md5
|
#This offset is where we should start reading the file to get a partial md5
|
||||||
#For audio file, it should be where audio data starts
|
#For audio file, it should be where audio data starts
|
||||||
def _get_md5partial_offset_and_size(self):
|
def _get_md5partial_offset_and_size(self):
|
||||||
return (0x4000, 0x4000) #16Kb
|
return (0x4000, 0x4000) #16Kb
|
||||||
|
|
||||||
def _read_info(self, field):
|
def _read_info(self, field):
|
||||||
if field in ('size', 'mtime'):
|
if field in ('size', 'mtime'):
|
||||||
stats = self.path.stat()
|
stats = self.path.stat()
|
||||||
@ -129,24 +130,24 @@ class File:
|
|||||||
fp.close()
|
fp.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _read_all_info(self, attrnames=None):
|
def _read_all_info(self, attrnames=None):
|
||||||
"""Cache all possible info.
|
"""Cache all possible info.
|
||||||
|
|
||||||
If `attrnames` is not None, caches only attrnames.
|
If `attrnames` is not None, caches only attrnames.
|
||||||
"""
|
"""
|
||||||
if attrnames is None:
|
if attrnames is None:
|
||||||
attrnames = self.INITIAL_INFO.keys()
|
attrnames = self.INITIAL_INFO.keys()
|
||||||
for attrname in attrnames:
|
for attrname in attrnames:
|
||||||
getattr(self, attrname)
|
getattr(self, attrname)
|
||||||
|
|
||||||
#--- 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):
|
||||||
if newname == self.name:
|
if newname == self.name:
|
||||||
return
|
return
|
||||||
@ -160,42 +161,42 @@ class File:
|
|||||||
if not destpath.exists():
|
if not destpath.exists():
|
||||||
raise OperationError(self)
|
raise OperationError(self)
|
||||||
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
|
||||||
@property
|
@property
|
||||||
def extension(self):
|
def extension(self):
|
||||||
return get_file_ext(self.name)
|
return get_file_ext(self.name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return self.path.name
|
return self.path.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def folder_path(self):
|
def folder_path(self):
|
||||||
return self.path.parent()
|
return self.path.parent()
|
||||||
|
|
||||||
|
|
||||||
class Folder(File):
|
class Folder(File):
|
||||||
"""A wrapper around a folder path.
|
"""A wrapper around a folder path.
|
||||||
|
|
||||||
It has the size/md5 info of a File, but it's value are the sum of its subitems.
|
It has the size/md5 info of a File, but it's value are the sum of its subitems.
|
||||||
"""
|
"""
|
||||||
__slots__ = File.__slots__ + ('_subfolders', )
|
__slots__ = File.__slots__ + ('_subfolders', )
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
File.__init__(self, path)
|
File.__init__(self, path)
|
||||||
self._subfolders = None
|
self._subfolders = None
|
||||||
|
|
||||||
def _all_items(self):
|
def _all_items(self):
|
||||||
folders = self.subfolders
|
folders = self.subfolders
|
||||||
files = get_files(self.path)
|
files = get_files(self.path)
|
||||||
return folders + files
|
return folders + files
|
||||||
|
|
||||||
def _read_info(self, field):
|
def _read_info(self, field):
|
||||||
if field in {'size', 'mtime'}:
|
if field in {'size', 'mtime'}:
|
||||||
size = sum((f.size for f in self._all_items()), 0)
|
size = sum((f.size for f in self._all_items()), 0)
|
||||||
@ -208,31 +209,31 @@ class Folder(File):
|
|||||||
# different md5 if a file gets moved in a different subdirectory.
|
# different md5 if a file gets moved in a different subdirectory.
|
||||||
def get_dir_md5_concat():
|
def get_dir_md5_concat():
|
||||||
items = self._all_items()
|
items = self._all_items()
|
||||||
items.sort(key=lambda f:f.path)
|
items.sort(key=lambda f: f.path)
|
||||||
md5s = [getattr(f, field) for f in items]
|
md5s = [getattr(f, field) for f in items]
|
||||||
return b''.join(md5s)
|
return b''.join(md5s)
|
||||||
|
|
||||||
md5 = hashlib.md5(get_dir_md5_concat())
|
md5 = hashlib.md5(get_dir_md5_concat())
|
||||||
digest = md5.digest()
|
digest = md5.digest()
|
||||||
setattr(self, field, digest)
|
setattr(self, field, digest)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def subfolders(self):
|
def subfolders(self):
|
||||||
if self._subfolders is None:
|
if self._subfolders is None:
|
||||||
subfolders = [p for p in self.path.listdir() if not p.islink() and p.isdir()]
|
subfolders = [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
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def can_handle(cls, path):
|
def can_handle(cls, path):
|
||||||
return not path.islink() and path.isdir()
|
return not path.islink() and path.isdir()
|
||||||
|
|
||||||
|
|
||||||
def get_file(path, fileclasses=[File]):
|
def get_file(path, fileclasses=[File]):
|
||||||
"""Wraps ``path`` around its appropriate :class:`File` class.
|
"""Wraps ``path`` around its appropriate :class:`File` class.
|
||||||
|
|
||||||
Whether a class is "appropriate" is decided by :meth:`File.can_handle`
|
Whether a class is "appropriate" is decided by :meth:`File.can_handle`
|
||||||
|
|
||||||
:param Path path: path to wrap
|
:param Path path: path to wrap
|
||||||
:param fileclasses: List of candidate :class:`File` classes
|
:param fileclasses: List of candidate :class:`File` classes
|
||||||
"""
|
"""
|
||||||
@ -242,7 +243,7 @@ def get_file(path, fileclasses=[File]):
|
|||||||
|
|
||||||
def get_files(path, fileclasses=[File]):
|
def get_files(path, fileclasses=[File]):
|
||||||
"""Returns a list of :class:`File` for each file contained in ``path``.
|
"""Returns a list of :class:`File` for each file contained in ``path``.
|
||||||
|
|
||||||
:param Path path: path to scan
|
:param Path path: path to scan
|
||||||
:param fileclasses: List of candidate :class:`File` classes
|
:param fileclasses: List of candidate :class:`File` classes
|
||||||
"""
|
"""
|
||||||
|
@ -12,4 +12,5 @@ either Cocoa's ``NSTableView`` or Qt's ``QTableView``. It tells them which cell
|
|||||||
blue, which is supposed to be orange, does the sorting logic, holds selection, etc..
|
blue, which is supposed to be orange, does the sorting logic, holds selection, etc..
|
||||||
|
|
||||||
.. _cross-toolkit: http://www.hardcoded.net/articles/cross-toolkit-software
|
.. _cross-toolkit: http://www.hardcoded.net/articles/cross-toolkit-software
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -1,31 +1,30 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2010-02-06
|
# Created On: 2010-02-06
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from hscommon.notify import Listener
|
from hscommon.notify import Listener
|
||||||
from hscommon.gui.base import NoopGUI
|
|
||||||
|
|
||||||
class DupeGuruGUIObject(Listener):
|
class DupeGuruGUIObject(Listener):
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
Listener.__init__(self, app)
|
Listener.__init__(self, app)
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
def directories_changed(self):
|
def directories_changed(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def dupes_selected(self):
|
def dupes_selected(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def marking_changed(self):
|
def marking_changed(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def results_changed(self):
|
def results_changed(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def results_changed_but_keep_selection(self):
|
def results_changed_but_keep_selection(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2011-09-06
|
# Created On: 2011-09-06
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from hscommon.gui.base import GUIObject
|
from hscommon.gui.base import GUIObject
|
||||||
@ -13,7 +13,7 @@ class CriterionCategoryList(GUISelectableList):
|
|||||||
def __init__(self, dialog):
|
def __init__(self, dialog):
|
||||||
self.dialog = dialog
|
self.dialog = dialog
|
||||||
GUISelectableList.__init__(self, [c.NAME for c in dialog.categories])
|
GUISelectableList.__init__(self, [c.NAME for c in dialog.categories])
|
||||||
|
|
||||||
def _update_selection(self):
|
def _update_selection(self):
|
||||||
self.dialog.select_category(self.dialog.categories[self.selected_index])
|
self.dialog.select_category(self.dialog.categories[self.selected_index])
|
||||||
GUISelectableList._update_selection(self)
|
GUISelectableList._update_selection(self)
|
||||||
@ -22,10 +22,10 @@ class PrioritizationList(GUISelectableList):
|
|||||||
def __init__(self, dialog):
|
def __init__(self, dialog):
|
||||||
self.dialog = dialog
|
self.dialog = dialog
|
||||||
GUISelectableList.__init__(self)
|
GUISelectableList.__init__(self)
|
||||||
|
|
||||||
def _refresh_contents(self):
|
def _refresh_contents(self):
|
||||||
self[:] = [crit.display for crit in self.dialog.prioritizations]
|
self[:] = [crit.display for crit in self.dialog.prioritizations]
|
||||||
|
|
||||||
def move_indexes(self, indexes, dest_index):
|
def move_indexes(self, indexes, dest_index):
|
||||||
indexes.sort()
|
indexes.sort()
|
||||||
prilist = self.dialog.prioritizations
|
prilist = self.dialog.prioritizations
|
||||||
@ -34,7 +34,7 @@ class PrioritizationList(GUISelectableList):
|
|||||||
del prilist[i]
|
del prilist[i]
|
||||||
prilist[dest_index:dest_index] = selected
|
prilist[dest_index:dest_index] = selected
|
||||||
self._refresh_contents()
|
self._refresh_contents()
|
||||||
|
|
||||||
def remove_selected(self):
|
def remove_selected(self):
|
||||||
prilist = self.dialog.prioritizations
|
prilist = self.dialog.prioritizations
|
||||||
for i in sorted(self.selected_indexes, reverse=True):
|
for i in sorted(self.selected_indexes, reverse=True):
|
||||||
@ -51,15 +51,15 @@ class PrioritizeDialog(GUIObject):
|
|||||||
self.criteria_list = GUISelectableList()
|
self.criteria_list = GUISelectableList()
|
||||||
self.prioritizations = []
|
self.prioritizations = []
|
||||||
self.prioritization_list = PrioritizationList(self)
|
self.prioritization_list = PrioritizationList(self)
|
||||||
|
|
||||||
#--- Override
|
#--- Override
|
||||||
def _view_updated(self):
|
def _view_updated(self):
|
||||||
self.category_list.select(0)
|
self.category_list.select(0)
|
||||||
|
|
||||||
#--- Private
|
#--- Private
|
||||||
def _sort_key(self, dupe):
|
def _sort_key(self, dupe):
|
||||||
return tuple(crit.sort_key(dupe) for crit in self.prioritizations)
|
return tuple(crit.sort_key(dupe) for crit in self.prioritizations)
|
||||||
|
|
||||||
#--- Public
|
#--- Public
|
||||||
def select_category(self, category):
|
def select_category(self, category):
|
||||||
self.criteria = category.criteria_list()
|
self.criteria = category.criteria_list()
|
||||||
@ -71,10 +71,11 @@ class PrioritizeDialog(GUIObject):
|
|||||||
return
|
return
|
||||||
crit = self.criteria[self.criteria_list.selected_index]
|
crit = self.criteria[self.criteria_list.selected_index]
|
||||||
self.prioritizations.append(crit)
|
self.prioritizations.append(crit)
|
||||||
|
del crit
|
||||||
self.prioritization_list[:] = [crit.display for crit in self.prioritizations]
|
self.prioritization_list[:] = [crit.display for crit in self.prioritizations]
|
||||||
|
|
||||||
def remove_selected(self):
|
def remove_selected(self):
|
||||||
self.prioritization_list.remove_selected()
|
self.prioritization_list.remove_selected()
|
||||||
|
|
||||||
def perform_reprioritization(self):
|
def perform_reprioritization(self):
|
||||||
self.app.reprioritize_groups(self._sort_key)
|
self.app.reprioritize_groups(self._sort_key)
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2006/05/02
|
# Created On: 2006/05/02
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
@ -12,7 +12,7 @@ from hscommon.util import FileOrPath
|
|||||||
|
|
||||||
class IgnoreList:
|
class IgnoreList:
|
||||||
"""An ignore list implementation that is iterable, filterable and exportable to XML.
|
"""An ignore list implementation that is iterable, filterable and exportable to XML.
|
||||||
|
|
||||||
Call Ignore to add an ignore list entry, and AreIgnore to check if 2 items are in the list.
|
Call Ignore to add an ignore list entry, and AreIgnore to check if 2 items are in the list.
|
||||||
When iterated, 2 sized tuples will be returned, the tuples containing 2 items ignored together.
|
When iterated, 2 sized tuples will be returned, the tuples containing 2 items ignored together.
|
||||||
"""
|
"""
|
||||||
@ -20,43 +20,43 @@ class IgnoreList:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._ignored = {}
|
self._ignored = {}
|
||||||
self._count = 0
|
self._count = 0
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
for first,seconds in self._ignored.items():
|
for first, seconds in self._ignored.items():
|
||||||
for second in seconds:
|
for second in seconds:
|
||||||
yield (first,second)
|
yield (first, second)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return self._count
|
return self._count
|
||||||
|
|
||||||
#---Public
|
#---Public
|
||||||
def AreIgnored(self,first,second):
|
def AreIgnored(self, first, second):
|
||||||
def do_check(first,second):
|
def do_check(first, second):
|
||||||
try:
|
try:
|
||||||
matches = self._ignored[first]
|
matches = self._ignored[first]
|
||||||
return second in matches
|
return second in matches
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return do_check(first,second) or do_check(second,first)
|
return do_check(first, second) or do_check(second, first)
|
||||||
|
|
||||||
def Clear(self):
|
def Clear(self):
|
||||||
self._ignored = {}
|
self._ignored = {}
|
||||||
self._count = 0
|
self._count = 0
|
||||||
|
|
||||||
def Filter(self,func):
|
def Filter(self, func):
|
||||||
"""Applies a filter on all ignored items, and remove all matches where func(first,second)
|
"""Applies a filter on all ignored items, and remove all matches where func(first,second)
|
||||||
doesn't return True.
|
doesn't return True.
|
||||||
"""
|
"""
|
||||||
filtered = IgnoreList()
|
filtered = IgnoreList()
|
||||||
for first,second in self:
|
for first, second in self:
|
||||||
if func(first,second):
|
if func(first, second):
|
||||||
filtered.Ignore(first,second)
|
filtered.Ignore(first, second)
|
||||||
self._ignored = filtered._ignored
|
self._ignored = filtered._ignored
|
||||||
self._count = filtered._count
|
self._count = filtered._count
|
||||||
|
|
||||||
def Ignore(self,first,second):
|
def Ignore(self, first, second):
|
||||||
if self.AreIgnored(first,second):
|
if self.AreIgnored(first, second):
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
matches = self._ignored[first]
|
matches = self._ignored[first]
|
||||||
@ -70,7 +70,7 @@ class IgnoreList:
|
|||||||
matches.add(second)
|
matches.add(second)
|
||||||
self._ignored[first] = matches
|
self._ignored[first] = matches
|
||||||
self._count += 1
|
self._count += 1
|
||||||
|
|
||||||
def remove(self, first, second):
|
def remove(self, first, second):
|
||||||
def inner(first, second):
|
def inner(first, second):
|
||||||
try:
|
try:
|
||||||
@ -85,14 +85,14 @@ class IgnoreList:
|
|||||||
return False
|
return False
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not inner(first, second):
|
if not inner(first, second):
|
||||||
if not inner(second, first):
|
if not inner(second, first):
|
||||||
raise ValueError()
|
raise ValueError()
|
||||||
|
|
||||||
def load_from_xml(self, infile):
|
def load_from_xml(self, infile):
|
||||||
"""Loads the ignore list from a XML created with save_to_xml.
|
"""Loads the ignore list from a XML created with save_to_xml.
|
||||||
|
|
||||||
infile can be a file object or a filename.
|
infile can be a file object or a filename.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@ -109,10 +109,10 @@ class IgnoreList:
|
|||||||
subfile_path = sfn.get('path')
|
subfile_path = sfn.get('path')
|
||||||
if subfile_path:
|
if subfile_path:
|
||||||
self.Ignore(file_path, subfile_path)
|
self.Ignore(file_path, subfile_path)
|
||||||
|
|
||||||
def save_to_xml(self, outfile):
|
def save_to_xml(self, outfile):
|
||||||
"""Create a XML file that can be used by load_from_xml.
|
"""Create a XML file that can be used by load_from_xml.
|
||||||
|
|
||||||
outfile can be a file object or a filename.
|
outfile can be a file object or a filename.
|
||||||
"""
|
"""
|
||||||
root = ET.Element('ignore_list')
|
root = ET.Element('ignore_list')
|
||||||
@ -125,5 +125,5 @@ class IgnoreList:
|
|||||||
tree = ET.ElementTree(root)
|
tree = ET.ElementTree(root)
|
||||||
with FileOrPath(outfile, 'wb') as fp:
|
with FileOrPath(outfile, 'wb') as fp:
|
||||||
tree.write(fp, encoding='utf-8')
|
tree.write(fp, encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
@ -96,7 +96,10 @@ class Results(Markable):
|
|||||||
self.__dupes = flatten(group.dupes for group in self.groups)
|
self.__dupes = flatten(group.dupes for group in self.groups)
|
||||||
if None in self.__dupes:
|
if None in self.__dupes:
|
||||||
# This is debug logging to try to figure out #44
|
# This is debug logging to try to figure out #44
|
||||||
logging.warning("There is a None value in the Results' dupe list. dupes: %r groups: %r", self.__dupes, self.groups)
|
logging.warning(
|
||||||
|
"There is a None value in the Results' dupe list. dupes: %r groups: %r",
|
||||||
|
self.__dupes, self.groups
|
||||||
|
)
|
||||||
if self.__filtered_dupes:
|
if self.__filtered_dupes:
|
||||||
self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes]
|
self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes]
|
||||||
sd = self.__dupes_sort_descriptor
|
sd = self.__dupes_sort_descriptor
|
||||||
@ -249,7 +252,8 @@ class Results(Markable):
|
|||||||
second_file = dupes[int(attrs['second'])]
|
second_file = dupes[int(attrs['second'])]
|
||||||
percentage = int(attrs['percentage'])
|
percentage = int(attrs['percentage'])
|
||||||
group.add_match(engine.Match(first_file, second_file, percentage))
|
group.add_match(engine.Match(first_file, second_file, percentage))
|
||||||
except (IndexError, KeyError, ValueError): # Covers missing attr, non-int values and indexes out of bounds
|
except (IndexError, KeyError, ValueError):
|
||||||
|
# Covers missing attr, non-int values and indexes out of bounds
|
||||||
pass
|
pass
|
||||||
if (not group.matches) and (len(dupes) >= 2):
|
if (not group.matches) and (len(dupes) >= 2):
|
||||||
do_match(dupes[0], dupes[1:], group)
|
do_match(dupes[0], dupes[1:], group)
|
||||||
@ -393,7 +397,7 @@ class Results(Markable):
|
|||||||
self.__get_dupe_list()
|
self.__get_dupe_list()
|
||||||
keyfunc = lambda d: self.app._get_dupe_sort_key(d, lambda: self.get_group_of_duplicate(d), key, delta)
|
keyfunc = lambda d: self.app._get_dupe_sort_key(d, lambda: self.get_group_of_duplicate(d), key, delta)
|
||||||
self.__dupes.sort(key=keyfunc, reverse=not asc)
|
self.__dupes.sort(key=keyfunc, reverse=not asc)
|
||||||
self.__dupes_sort_descriptor = (key,asc,delta)
|
self.__dupes_sort_descriptor = (key, asc, delta)
|
||||||
|
|
||||||
def sort_groups(self, key, asc=True):
|
def sort_groups(self, key, asc=True):
|
||||||
"""Sort :attr:`groups` according to ``key``.
|
"""Sort :attr:`groups` according to ``key``.
|
||||||
@ -405,9 +409,10 @@ class Results(Markable):
|
|||||||
"""
|
"""
|
||||||
keyfunc = lambda g: self.app._get_group_sort_key(g, key)
|
keyfunc = lambda g: self.app._get_group_sort_key(g, key)
|
||||||
self.groups.sort(key=keyfunc, reverse=not asc)
|
self.groups.sort(key=keyfunc, reverse=not asc)
|
||||||
self.__groups_sort_descriptor = (key,asc)
|
self.__groups_sort_descriptor = (key, asc)
|
||||||
|
|
||||||
#---Properties
|
#---Properties
|
||||||
dupes = property(__get_dupe_list)
|
dupes = property(__get_dupe_list)
|
||||||
groups = property(__get_groups, __set_groups)
|
groups = property(__get_groups, __set_groups)
|
||||||
stat_line = property(__get_stat_line)
|
stat_line = property(__get_stat_line)
|
||||||
|
|
||||||
|
@ -81,7 +81,9 @@ class Scanner:
|
|||||||
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.ContentsAudio, ScanType.Folders}:
|
if self.scan_type in {ScanType.Contents, ScanType.ContentsAudio, ScanType.Folders}:
|
||||||
sizeattr = 'audiosize' if self.scan_type == ScanType.ContentsAudio else 'size'
|
sizeattr = 'audiosize' if self.scan_type == ScanType.ContentsAudio else 'size'
|
||||||
return engine.getmatches_by_contents(files, sizeattr, partial=self.scan_type==ScanType.ContentsAudio, j=j)
|
return engine.getmatches_by_contents(
|
||||||
|
files, sizeattr, partial=self.scan_type == ScanType.ContentsAudio, j=j
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
j = j.start_subjob([2, 8])
|
j = j.start_subjob([2, 8])
|
||||||
kw = {}
|
kw = {}
|
||||||
@ -94,7 +96,11 @@ class Scanner:
|
|||||||
func = {
|
func = {
|
||||||
ScanType.Filename: lambda f: engine.getwords(rem_file_ext(f.name)),
|
ScanType.Filename: lambda f: engine.getwords(rem_file_ext(f.name)),
|
||||||
ScanType.Fields: lambda f: engine.getfields(rem_file_ext(f.name)),
|
ScanType.Fields: lambda f: engine.getfields(rem_file_ext(f.name)),
|
||||||
ScanType.Tag: lambda f: [engine.getwords(str(getattr(f, attrname))) for attrname in SCANNABLE_TAGS if attrname in self.scanned_tags],
|
ScanType.Tag: lambda f: [
|
||||||
|
engine.getwords(str(getattr(f, attrname)))
|
||||||
|
for attrname in SCANNABLE_TAGS
|
||||||
|
if attrname in self.scanned_tags
|
||||||
|
],
|
||||||
}[self.scan_type]
|
}[self.scan_type]
|
||||||
for f in j.iter_with_progress(files, tr("Read metadata of %d/%d files")):
|
for f in j.iter_with_progress(files, tr("Read metadata of %d/%d files")):
|
||||||
logging.debug("Reading metadata of {}".format(str(f.path)))
|
logging.debug("Reading metadata of {}".format(str(f.path)))
|
||||||
@ -152,8 +158,10 @@ class Scanner:
|
|||||||
if self.ignore_list:
|
if self.ignore_list:
|
||||||
j = j.start_subjob(2)
|
j = j.start_subjob(2)
|
||||||
iter_matches = j.iter_with_progress(matches, tr("Processed %d/%d matches against the ignore list"))
|
iter_matches = j.iter_with_progress(matches, tr("Processed %d/%d matches against the ignore list"))
|
||||||
matches = [m for m in iter_matches
|
matches = [
|
||||||
if not self.ignore_list.AreIgnored(str(m.first.path), str(m.second.path))]
|
m for m in iter_matches
|
||||||
|
if not self.ignore_list.AreIgnored(str(m.first.path), str(m.second.path))
|
||||||
|
]
|
||||||
logging.info('Grouping matches')
|
logging.info('Grouping matches')
|
||||||
groups = engine.get_groups(matches, j)
|
groups = engine.get_groups(matches, j)
|
||||||
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
|
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
|
||||||
@ -178,10 +186,11 @@ class Scanner:
|
|||||||
g.prioritize(self._key_func, self._tie_breaker)
|
g.prioritize(self._key_func, self._tie_breaker)
|
||||||
return groups
|
return groups
|
||||||
|
|
||||||
match_similar_words = False
|
match_similar_words = False
|
||||||
min_match_percentage = 80
|
min_match_percentage = 80
|
||||||
mix_file_kind = True
|
mix_file_kind = True
|
||||||
scan_type = ScanType.Filename
|
scan_type = ScanType.Filename
|
||||||
scanned_tags = {'artist', 'title'}
|
scanned_tags = {'artist', 'title'}
|
||||||
size_threshold = 0
|
size_threshold = 0
|
||||||
word_weighting = False
|
word_weighting = False
|
||||||
|
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
__version__ = '6.8.0'
|
__version__ = '6.8.0'
|
||||||
__appname__ = 'dupeGuru Music Edition'
|
__appname__ = 'dupeGuru Music Edition'
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# Created On: 2011/09/20
|
# Created On: 2011/09/20
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from core.app import DupeGuru as DupeGuruBase
|
from core.app import DupeGuru as DupeGuruBase
|
||||||
@ -13,28 +13,30 @@ from .result_table import ResultTable
|
|||||||
|
|
||||||
class DupeGuru(DupeGuruBase):
|
class DupeGuru(DupeGuruBase):
|
||||||
NAME = __appname__
|
NAME = __appname__
|
||||||
METADATA_TO_READ = ['size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
|
METADATA_TO_READ = [
|
||||||
'album', 'genre', 'year', 'track', 'comment']
|
'size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
|
||||||
|
'album', 'genre', 'year', 'track', 'comment'
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, view):
|
def __init__(self, view):
|
||||||
DupeGuruBase.__init__(self, view)
|
DupeGuruBase.__init__(self, view)
|
||||||
self.scanner = scanner.ScannerME()
|
self.scanner = scanner.ScannerME()
|
||||||
self.directories.fileclasses = [fs.MusicFile]
|
self.directories.fileclasses = [fs.MusicFile]
|
||||||
|
|
||||||
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
|
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
|
||||||
if key == 'folder_path':
|
if key == 'folder_path':
|
||||||
dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path)
|
dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path)
|
||||||
return str(dupe_folder_path).lower()
|
return str(dupe_folder_path).lower()
|
||||||
return DupeGuruBase._get_dupe_sort_key(self, dupe, get_group, key, delta)
|
return DupeGuruBase._get_dupe_sort_key(self, dupe, get_group, key, delta)
|
||||||
|
|
||||||
def _get_group_sort_key(self, group, key):
|
def _get_group_sort_key(self, group, key):
|
||||||
if key == 'folder_path':
|
if key == 'folder_path':
|
||||||
dupe_folder_path = getattr(group.ref, 'display_folder_path', group.ref.folder_path)
|
dupe_folder_path = getattr(group.ref, 'display_folder_path', group.ref.folder_path)
|
||||||
return str(dupe_folder_path).lower()
|
return str(dupe_folder_path).lower()
|
||||||
return DupeGuruBase._get_group_sort_key(self, group, key)
|
return DupeGuruBase._get_group_sort_key(self, group, key)
|
||||||
|
|
||||||
def _prioritization_categories(self):
|
def _prioritization_categories(self):
|
||||||
return prioritize.all_categories()
|
return prioritize.all_categories()
|
||||||
|
|
||||||
def _create_result_table(self):
|
def _create_result_table(self):
|
||||||
return ResultTable(self)
|
return ResultTable(self)
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-10-23
|
# Created On: 2009-10-23
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from hsaudiotag import auto
|
from hsaudiotag import auto
|
||||||
@ -12,32 +12,34 @@ from hscommon.util import get_file_ext, format_size, format_time
|
|||||||
from core.app import format_timestamp, format_perc, format_words, format_dupe_count
|
from core.app import format_timestamp, format_perc, format_words, format_dupe_count
|
||||||
from core import fs
|
from core import fs
|
||||||
|
|
||||||
TAG_FIELDS = {'audiosize', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
|
TAG_FIELDS = {
|
||||||
'album', 'genre', 'year', 'track', 'comment'}
|
'audiosize', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
|
||||||
|
'album', 'genre', 'year', 'track', 'comment'
|
||||||
|
}
|
||||||
|
|
||||||
class MusicFile(fs.File):
|
class MusicFile(fs.File):
|
||||||
INITIAL_INFO = fs.File.INITIAL_INFO.copy()
|
INITIAL_INFO = fs.File.INITIAL_INFO.copy()
|
||||||
INITIAL_INFO.update({
|
INITIAL_INFO.update({
|
||||||
'audiosize': 0,
|
'audiosize': 0,
|
||||||
'bitrate' : 0,
|
'bitrate': 0,
|
||||||
'duration' : 0,
|
'duration': 0,
|
||||||
'samplerate':0,
|
'samplerate': 0,
|
||||||
'artist' : '',
|
'artist': '',
|
||||||
'album' : '',
|
'album': '',
|
||||||
'title' : '',
|
'title': '',
|
||||||
'genre' : '',
|
'genre': '',
|
||||||
'comment' : '',
|
'comment': '',
|
||||||
'year' : '',
|
'year': '',
|
||||||
'track' : 0,
|
'track': 0,
|
||||||
})
|
})
|
||||||
__slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys())
|
__slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def can_handle(cls, path):
|
def can_handle(cls, path):
|
||||||
if not fs.File.can_handle(path):
|
if not fs.File.can_handle(path):
|
||||||
return False
|
return False
|
||||||
return get_file_ext(path.name) in auto.EXT2CLASS
|
return get_file_ext(path.name) in auto.EXT2CLASS
|
||||||
|
|
||||||
def get_display_info(self, group, delta):
|
def get_display_info(self, group, delta):
|
||||||
size = self.size
|
size = self.size
|
||||||
duration = self.duration
|
duration = self.duration
|
||||||
@ -67,7 +69,7 @@ class MusicFile(fs.File):
|
|||||||
'bitrate': str(bitrate),
|
'bitrate': str(bitrate),
|
||||||
'samplerate': str(samplerate),
|
'samplerate': str(samplerate),
|
||||||
'extension': self.extension,
|
'extension': self.extension,
|
||||||
'mtime': format_timestamp(mtime,delta and m),
|
'mtime': format_timestamp(mtime, delta and m),
|
||||||
'title': self.title,
|
'title': self.title,
|
||||||
'artist': self.artist,
|
'artist': self.artist,
|
||||||
'album': self.album,
|
'album': self.album,
|
||||||
@ -79,11 +81,11 @@ class MusicFile(fs.File):
|
|||||||
'words': format_words(self.words) if hasattr(self, 'words') else '',
|
'words': format_words(self.words) if hasattr(self, 'words') else '',
|
||||||
'dupe_count': format_dupe_count(dupe_count),
|
'dupe_count': format_dupe_count(dupe_count),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_md5partial_offset_and_size(self):
|
def _get_md5partial_offset_and_size(self):
|
||||||
f = auto.File(str(self.path))
|
f = auto.File(str(self.path))
|
||||||
return (f.audio_offset, f.audio_size)
|
return (f.audio_offset, f.audio_size)
|
||||||
|
|
||||||
def _read_info(self, field):
|
def _read_info(self, field):
|
||||||
fs.File._read_info(self, field)
|
fs.File._read_info(self, field)
|
||||||
if field in TAG_FIELDS:
|
if field in TAG_FIELDS:
|
||||||
@ -99,4 +101,4 @@ class MusicFile(fs.File):
|
|||||||
self.comment = f.comment
|
self.comment = f.comment
|
||||||
self.year = f.year
|
self.year = f.year
|
||||||
self.track = f.track
|
self.track = f.track
|
||||||
|
|
||||||
|
@ -1,35 +1,40 @@
|
|||||||
# Created On: 2011/09/16
|
# Created On: 2011/09/16
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
|
|
||||||
from core.prioritize import (KindCategory, FolderCategory, FilenameCategory, NumericalCategory,
|
from core.prioritize import (
|
||||||
SizeCategory, MtimeCategory)
|
KindCategory, FolderCategory, FilenameCategory, NumericalCategory,
|
||||||
|
SizeCategory, MtimeCategory
|
||||||
|
)
|
||||||
|
|
||||||
coltr = trget('columns')
|
coltr = trget('columns')
|
||||||
|
|
||||||
class DurationCategory(NumericalCategory):
|
class DurationCategory(NumericalCategory):
|
||||||
NAME = coltr("Duration")
|
NAME = coltr("Duration")
|
||||||
|
|
||||||
def extract_value(self, dupe):
|
def extract_value(self, dupe):
|
||||||
return dupe.duration
|
return dupe.duration
|
||||||
|
|
||||||
class BitrateCategory(NumericalCategory):
|
class BitrateCategory(NumericalCategory):
|
||||||
NAME = coltr("Bitrate")
|
NAME = coltr("Bitrate")
|
||||||
|
|
||||||
def extract_value(self, dupe):
|
def extract_value(self, dupe):
|
||||||
return dupe.bitrate
|
return dupe.bitrate
|
||||||
|
|
||||||
class SamplerateCategory(NumericalCategory):
|
class SamplerateCategory(NumericalCategory):
|
||||||
NAME = coltr("Samplerate")
|
NAME = coltr("Samplerate")
|
||||||
|
|
||||||
def extract_value(self, dupe):
|
def extract_value(self, dupe):
|
||||||
return dupe.samplerate
|
return dupe.samplerate
|
||||||
|
|
||||||
def all_categories():
|
def all_categories():
|
||||||
return [KindCategory, FolderCategory, FilenameCategory, SizeCategory, DurationCategory,
|
return [
|
||||||
BitrateCategory, SamplerateCategory, MtimeCategory]
|
KindCategory, FolderCategory, FilenameCategory, SizeCategory, DurationCategory,
|
||||||
|
BitrateCategory, SamplerateCategory, MtimeCategory
|
||||||
|
]
|
||||||
|
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2006/09/01
|
# Created On: 2006/09/01
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2
|
from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 # NOQA
|
||||||
|
|
||||||
# Converted to C
|
# Converted to C
|
||||||
# def getblock(image):
|
# def getblock(image):
|
||||||
# """Returns a 3 sized tuple containing the mean color of 'image'.
|
# """Returns a 3 sized tuple containing the mean color of 'image'.
|
||||||
#
|
#
|
||||||
# image: a PIL image or crop.
|
# image: a PIL image or crop.
|
||||||
# """
|
# """
|
||||||
# if image.size[0]:
|
# if image.size[0]:
|
||||||
@ -28,7 +28,7 @@ from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2
|
|||||||
# This is not used anymore
|
# This is not used anymore
|
||||||
# def getblocks(image,blocksize):
|
# def getblocks(image,blocksize):
|
||||||
# """Returns a list of blocks (3 sized tuples).
|
# """Returns a list of blocks (3 sized tuples).
|
||||||
#
|
#
|
||||||
# image: A PIL image to base the blocks on.
|
# image: A PIL image to base the blocks on.
|
||||||
# blocksize: The size of the blocks to be create. This is a single integer, defining
|
# blocksize: The size of the blocks to be create. This is a single integer, defining
|
||||||
# both width and height (blocks are square).
|
# both width and height (blocks are square).
|
||||||
@ -46,7 +46,7 @@ from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2
|
|||||||
# Converted to C
|
# Converted to C
|
||||||
# def getblocks2(image,block_count_per_side):
|
# def getblocks2(image,block_count_per_side):
|
||||||
# """Returns a list of blocks (3 sized tuples).
|
# """Returns a list of blocks (3 sized tuples).
|
||||||
#
|
#
|
||||||
# image: A PIL image to base the blocks on.
|
# image: A PIL image to base the blocks on.
|
||||||
# block_count_per_side: This integer determine the number of blocks the function will return.
|
# block_count_per_side: This integer determine the number of blocks the function will return.
|
||||||
# If it is 10, for example, 100 blocks will be returns (10 width, 10 height). The blocks will not
|
# If it is 10, for example, 100 blocks will be returns (10 width, 10 height). The blocks will not
|
||||||
@ -73,7 +73,7 @@ from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2
|
|||||||
# Converted to C
|
# Converted to C
|
||||||
# def diff(first, second):
|
# def diff(first, second):
|
||||||
# """Returns the difference between the first block and the second.
|
# """Returns the difference between the first block and the second.
|
||||||
#
|
#
|
||||||
# It returns an absolute sum of the 3 differences (RGB).
|
# It returns an absolute sum of the 3 differences (RGB).
|
||||||
# """
|
# """
|
||||||
# r1, g1, b1 = first
|
# r1, g1, b1 = first
|
||||||
@ -83,7 +83,7 @@ from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2
|
|||||||
# Converted to C
|
# Converted to C
|
||||||
# def avgdiff(first, second, limit=768, min_iterations=1):
|
# def avgdiff(first, second, limit=768, min_iterations=1):
|
||||||
# """Returns the average diff between first blocks and seconds.
|
# """Returns the average diff between first blocks and seconds.
|
||||||
#
|
#
|
||||||
# If the result surpasses limit, limit + 1 is returned, except if less than min_iterations
|
# If the result surpasses limit, limit + 1 is returned, except if less than min_iterations
|
||||||
# iterations have been made in the blocks.
|
# iterations have been made in the blocks.
|
||||||
# """
|
# """
|
||||||
@ -106,7 +106,7 @@ from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2
|
|||||||
# This is not used anymore
|
# This is not used anymore
|
||||||
# def maxdiff(first,second,limit=768):
|
# def maxdiff(first,second,limit=768):
|
||||||
# """Returns the max diff between first blocks and seconds.
|
# """Returns the max diff between first blocks and seconds.
|
||||||
#
|
#
|
||||||
# If the result surpasses limit, the first max being over limit is returned.
|
# If the result surpasses limit, the first max being over limit is returned.
|
||||||
# """
|
# """
|
||||||
# if len(first) != len(second):
|
# if len(first) != len(second):
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2006/09/14
|
# Created On: 2006/09/14
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@ -15,11 +15,11 @@ from ._cache import string_to_colors
|
|||||||
|
|
||||||
def colors_to_string(colors):
|
def colors_to_string(colors):
|
||||||
"""Transform the 3 sized tuples 'colors' into a hex string.
|
"""Transform the 3 sized tuples 'colors' into a hex string.
|
||||||
|
|
||||||
[(0,100,255)] --> 0064ff
|
[(0,100,255)] --> 0064ff
|
||||||
[(1,2,3),(4,5,6)] --> 010203040506
|
[(1,2,3),(4,5,6)] --> 010203040506
|
||||||
"""
|
"""
|
||||||
return ''.join(['%02x%02x%02x' % (r,g,b) for r,g,b in colors])
|
return ''.join(['%02x%02x%02x' % (r, g, b) for r, g, b in colors])
|
||||||
|
|
||||||
# This function is an important bottleneck of dupeGuru PE. It has been converted to C.
|
# This function is an important bottleneck of dupeGuru PE. It has been converted to C.
|
||||||
# def string_to_colors(s):
|
# def string_to_colors(s):
|
||||||
@ -38,18 +38,18 @@ class Cache:
|
|||||||
self.dbname = db
|
self.dbname = db
|
||||||
self.con = None
|
self.con = None
|
||||||
self._create_con()
|
self._create_con()
|
||||||
|
|
||||||
def __contains__(self, key):
|
def __contains__(self, key):
|
||||||
sql = "select count(*) from pictures where path = ?"
|
sql = "select count(*) from pictures where path = ?"
|
||||||
result = self.con.execute(sql, [key]).fetchall()
|
result = self.con.execute(sql, [key]).fetchall()
|
||||||
return result[0][0] > 0
|
return result[0][0] > 0
|
||||||
|
|
||||||
def __delitem__(self, key):
|
def __delitem__(self, key):
|
||||||
if key not in self:
|
if key not in self:
|
||||||
raise KeyError(key)
|
raise KeyError(key)
|
||||||
sql = "delete from pictures where path = ?"
|
sql = "delete from pictures where path = ?"
|
||||||
self.con.execute(sql, [key])
|
self.con.execute(sql, [key])
|
||||||
|
|
||||||
# Optimized
|
# Optimized
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
if isinstance(key, int):
|
if isinstance(key, int):
|
||||||
@ -62,17 +62,17 @@ class Cache:
|
|||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
raise KeyError(key)
|
raise KeyError(key)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
sql = "select path from pictures"
|
sql = "select path from pictures"
|
||||||
result = self.con.execute(sql)
|
result = self.con.execute(sql)
|
||||||
return (row[0] for row in result)
|
return (row[0] for row in result)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
sql = "select count(*) from pictures"
|
sql = "select count(*) from pictures"
|
||||||
result = self.con.execute(sql).fetchall()
|
result = self.con.execute(sql).fetchall()
|
||||||
return result[0][0]
|
return result[0][0]
|
||||||
|
|
||||||
def __setitem__(self, path_str, blocks):
|
def __setitem__(self, path_str, blocks):
|
||||||
blocks = colors_to_string(blocks)
|
blocks = colors_to_string(blocks)
|
||||||
if op.exists(path_str):
|
if op.exists(path_str):
|
||||||
@ -89,15 +89,15 @@ class Cache:
|
|||||||
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('DatabaseError while setting value for key %r: %s', path_str, str(e))
|
logging.warning('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("create table pictures(path TEXT, mtime INTEGER, blocks TEXT)");
|
self.con.execute("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)
|
||||||
try:
|
try:
|
||||||
self.con.execute("select path, mtime, blocks from pictures where 1=2")
|
self.con.execute("select path, mtime, blocks from pictures where 1=2")
|
||||||
@ -110,23 +110,23 @@ class Cache:
|
|||||||
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)
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.close()
|
self.close()
|
||||||
if self.dbname != ':memory:':
|
if self.dbname != ':memory:':
|
||||||
os.remove(self.dbname)
|
os.remove(self.dbname)
|
||||||
self._create_con()
|
self._create_con()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
if self.con is not None:
|
if self.con is not None:
|
||||||
self.con.close()
|
self.con.close()
|
||||||
self.con = None
|
self.con = None
|
||||||
|
|
||||||
def filter(self, func):
|
def filter(self, func):
|
||||||
to_delete = [key for key in self if not func(key)]
|
to_delete = [key for key in self if not func(key)]
|
||||||
for key in to_delete:
|
for key in to_delete:
|
||||||
del self[key]
|
del self[key]
|
||||||
|
|
||||||
def get_id(self, path):
|
def get_id(self, path):
|
||||||
sql = "select rowid from pictures where path = ?"
|
sql = "select rowid from pictures where path = ?"
|
||||||
result = self.con.execute(sql, [path]).fetchone()
|
result = self.con.execute(sql, [path]).fetchone()
|
||||||
@ -134,15 +134,15 @@ class Cache:
|
|||||||
return result[0]
|
return result[0]
|
||||||
else:
|
else:
|
||||||
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(map(str, rowids))
|
sql = "select rowid, blocks from pictures where rowid in (%s)" % ','.join(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)
|
||||||
|
|
||||||
def purge_outdated(self):
|
def purge_outdated(self):
|
||||||
"""Go through the cache and purge outdated records.
|
"""Go through the cache and purge outdated records.
|
||||||
|
|
||||||
A record is outdated if the picture doesn't exist or if its mtime is greater than the one in
|
A record is outdated if the picture doesn't exist or if its mtime is greater than the one in
|
||||||
the db.
|
the db.
|
||||||
"""
|
"""
|
||||||
@ -159,4 +159,4 @@ class Cache:
|
|||||||
if todelete:
|
if todelete:
|
||||||
sql = "delete from pictures where rowid in (%s)" % ','.join(map(str, todelete))
|
sql = "delete from pictures where rowid in (%s)" % ','.join(map(str, todelete))
|
||||||
self.con.execute(sql)
|
self.con.execute(sql)
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2011-04-20
|
# Created On: 2011-04-20
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
# Heavily based on http://topo.math.u-psud.fr/~bousch/exifdump.py by Thierry Bousch (Public Domain)
|
# Heavily based on http://topo.math.u-psud.fr/~bousch/exifdump.py by Thierry Bousch (Public Domain)
|
||||||
@ -181,7 +181,7 @@ class Fraction:
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '%d/%d' % (self.num, self.den)
|
return '%d/%d' % (self.num, self.den)
|
||||||
|
|
||||||
|
|
||||||
class TIFF_file:
|
class TIFF_file:
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
@ -201,14 +201,14 @@ class TIFF_file:
|
|||||||
logging.debug(self.endian)
|
logging.debug(self.endian)
|
||||||
logging.debug("Slice for offset %d length %d: %r and value: %d", offset, length, slice, val)
|
logging.debug("Slice for offset %d length %d: %r and value: %d", offset, length, slice, val)
|
||||||
return val
|
return val
|
||||||
|
|
||||||
def first_IFD(self):
|
def first_IFD(self):
|
||||||
return self.s2n(4, 4)
|
return self.s2n(4, 4)
|
||||||
|
|
||||||
def next_IFD(self, ifd):
|
def next_IFD(self, ifd):
|
||||||
entries = self.s2n(ifd, 2)
|
entries = self.s2n(ifd, 2)
|
||||||
return self.s2n(ifd + 2 + 12 * entries, 4)
|
return self.s2n(ifd + 2 + 12 * entries, 4)
|
||||||
|
|
||||||
def list_IFDs(self):
|
def list_IFDs(self):
|
||||||
i = self.first_IFD()
|
i = self.first_IFD()
|
||||||
a = []
|
a = []
|
||||||
@ -216,7 +216,7 @@ class TIFF_file:
|
|||||||
a.append(i)
|
a.append(i)
|
||||||
i = self.next_IFD(i)
|
i = self.next_IFD(i)
|
||||||
return a
|
return a
|
||||||
|
|
||||||
def dump_IFD(self, ifd):
|
def dump_IFD(self, ifd):
|
||||||
entries = self.s2n(ifd, 2)
|
entries = self.s2n(ifd, 2)
|
||||||
logging.debug("Entries for IFD %d: %d", ifd, entries)
|
logging.debug("Entries for IFD %d: %d", ifd, entries)
|
||||||
@ -230,7 +230,7 @@ class TIFF_file:
|
|||||||
type = self.s2n(entry+2, 2)
|
type = self.s2n(entry+2, 2)
|
||||||
if not 1 <= type <= 10:
|
if not 1 <= type <= 10:
|
||||||
continue # not handled
|
continue # not handled
|
||||||
typelen = [ 1, 1, 2, 4, 8, 1, 1, 2, 4, 8 ] [type-1]
|
typelen = [1, 1, 2, 4, 8, 1, 1, 2, 4, 8][type-1]
|
||||||
count = self.s2n(entry+4, 4)
|
count = self.s2n(entry+4, 4)
|
||||||
if count > MAX_COUNT:
|
if count > MAX_COUNT:
|
||||||
logging.debug("Probably corrupt. Aborting.")
|
logging.debug("Probably corrupt. Aborting.")
|
||||||
@ -247,7 +247,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(self.s2n(offset, 4, signed),
|
value_j = Fraction(self.s2n(offset, 4, signed),
|
||||||
self.s2n(offset+4, 4, signed))
|
self.s2n(offset+4, 4, signed))
|
||||||
else:
|
else:
|
||||||
# Not a fraction
|
# Not a fraction
|
||||||
@ -255,7 +255,7 @@ class TIFF_file:
|
|||||||
values.append(value_j)
|
values.append(value_j)
|
||||||
offset = offset + typelen
|
offset = offset + typelen
|
||||||
# Now "values" is either a string or an array
|
# Now "values" is either a string or an array
|
||||||
a.append((tag,type,values))
|
a.append((tag, type, values))
|
||||||
return a
|
return a
|
||||||
|
|
||||||
def read_exif_header(fp):
|
def read_exif_header(fp):
|
||||||
@ -283,13 +283,13 @@ 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("%s format", {INTEL_ENDIAN:'Intel', MOTOROLA_ENDIAN:'Motorola'}[data_format])
|
logging.debug("%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.
|
||||||
main_IFD_offset = T.first_IFD()
|
main_IFD_offset = T.first_IFD()
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
def add_tag_to_result(tag, values):
|
def add_tag_to_result(tag, values):
|
||||||
try:
|
try:
|
||||||
stag = EXIF_TAGS[tag]
|
stag = EXIF_TAGS[tag]
|
||||||
@ -298,7 +298,7 @@ def get_fields(fp):
|
|||||||
if stag in result:
|
if stag in result:
|
||||||
return # don't overwrite data
|
return # don't overwrite data
|
||||||
result[stag] = values
|
result[stag] = values
|
||||||
|
|
||||||
logging.debug("IFD at offset %d", main_IFD_offset)
|
logging.debug("IFD at offset %d", main_IFD_offset)
|
||||||
IFD = T.dump_IFD(main_IFD_offset)
|
IFD = T.dump_IFD(main_IFD_offset)
|
||||||
exif_off = gps_off = 0
|
exif_off = gps_off = 0
|
||||||
|
@ -93,8 +93,10 @@ def get_chunks(pictures):
|
|||||||
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
|
||||||
chunk_size = max(MIN_CHUNK_SIZE, chunk_size)
|
chunk_size = max(MIN_CHUNK_SIZE, chunk_size)
|
||||||
logging.info("Creating %d chunks with a chunk size of %d for %d pictures", chunk_count,
|
logging.info(
|
||||||
chunk_size, len(pictures))
|
"Creating %d chunks with a chunk size of %d for %d pictures", chunk_count,
|
||||||
|
chunk_size, len(pictures)
|
||||||
|
)
|
||||||
chunks = [pictures[i:i+chunk_size] for i in range(0, len(pictures), chunk_size)]
|
chunks = [pictures[i:i+chunk_size] for i in range(0, len(pictures), chunk_size)]
|
||||||
return chunks
|
return chunks
|
||||||
|
|
||||||
@ -142,7 +144,7 @@ def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nul
|
|||||||
|
|
||||||
def collect_results(collect_all=False):
|
def collect_results(collect_all=False):
|
||||||
# collect results and wait until the queue is small enough to accomodate a new results.
|
# collect results and wait until the queue is small enough to accomodate a new results.
|
||||||
nonlocal async_results, matches, comparison_count
|
nonlocal async_results, matches, comparison_count, comparisons_to_do
|
||||||
limit = 0 if collect_all else RESULTS_QUEUE_LIMIT
|
limit = 0 if collect_all else RESULTS_QUEUE_LIMIT
|
||||||
while len(async_results) > limit:
|
while len(async_results) > limit:
|
||||||
ready, working = extract(lambda r: r.ready(), async_results)
|
ready, working = extract(lambda r: r.ready(), async_results)
|
||||||
@ -150,7 +152,8 @@ def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nul
|
|||||||
matches += result.get()
|
matches += result.get()
|
||||||
async_results.remove(result)
|
async_results.remove(result)
|
||||||
comparison_count += 1
|
comparison_count += 1
|
||||||
progress_msg = tr("Performed %d/%d chunk matches") % (comparison_count, len(comparisons_to_do))
|
# About the NOQA below: I think there's a bug in pyflakes. To investigate...
|
||||||
|
progress_msg = tr("Performed %d/%d chunk matches") % (comparison_count, len(comparisons_to_do)) # NOQA
|
||||||
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])
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2011-04-20
|
# Created On: 2011-04-20
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@ -27,4 +27,5 @@ def getmatches(files, match_scaled, j):
|
|||||||
if (not match_scaled) and (p1.dimensions != p2.dimensions):
|
if (not match_scaled) and (p1.dimensions != p2.dimensions):
|
||||||
continue
|
continue
|
||||||
matches.append(Match(p1, p2, 100))
|
matches.append(Match(p1, p2, 100))
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2011-05-29
|
# Created On: 2011-05-29
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@ -23,20 +23,20 @@ def get_delta_dimensions(value, ref_value):
|
|||||||
class Photo(fs.File):
|
class Photo(fs.File):
|
||||||
INITIAL_INFO = fs.File.INITIAL_INFO.copy()
|
INITIAL_INFO = fs.File.INITIAL_INFO.copy()
|
||||||
INITIAL_INFO.update({
|
INITIAL_INFO.update({
|
||||||
'dimensions': (0,0),
|
'dimensions': (0, 0),
|
||||||
'exif_timestamp': '',
|
'exif_timestamp': '',
|
||||||
})
|
})
|
||||||
__slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys())
|
__slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys())
|
||||||
|
|
||||||
# These extensions are supported on all platforms
|
# These extensions are supported on all platforms
|
||||||
HANDLED_EXTS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif'}
|
HANDLED_EXTS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif'}
|
||||||
|
|
||||||
def _plat_get_dimensions(self):
|
def _plat_get_dimensions(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _plat_get_blocks(self, block_count_per_side, orientation):
|
def _plat_get_blocks(self, block_count_per_side, orientation):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _get_orientation(self):
|
def _get_orientation(self):
|
||||||
if not hasattr(self, '_cached_orientation'):
|
if not hasattr(self, '_cached_orientation'):
|
||||||
try:
|
try:
|
||||||
@ -48,7 +48,7 @@ class Photo(fs.File):
|
|||||||
except Exception: # Couldn't read EXIF data, no transforms
|
except Exception: # Couldn't read EXIF data, no transforms
|
||||||
self._cached_orientation = 0
|
self._cached_orientation = 0
|
||||||
return self._cached_orientation
|
return self._cached_orientation
|
||||||
|
|
||||||
def _get_exif_timestamp(self):
|
def _get_exif_timestamp(self):
|
||||||
try:
|
try:
|
||||||
with self.path.open('rb') as fp:
|
with self.path.open('rb') as fp:
|
||||||
@ -57,11 +57,11 @@ class Photo(fs.File):
|
|||||||
except Exception:
|
except Exception:
|
||||||
logging.info("Couldn't read EXIF of picture: %s", self.path)
|
logging.info("Couldn't read EXIF of picture: %s", self.path)
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def can_handle(cls, path):
|
def can_handle(cls, path):
|
||||||
return fs.File.can_handle(path) and get_file_ext(path.name) in cls.HANDLED_EXTS
|
return fs.File.can_handle(path) and get_file_ext(path.name) in cls.HANDLED_EXTS
|
||||||
|
|
||||||
def get_display_info(self, group, delta):
|
def get_display_info(self, group, delta):
|
||||||
size = self.size
|
size = self.size
|
||||||
mtime = self.mtime
|
mtime = self.mtime
|
||||||
@ -90,7 +90,7 @@ class Photo(fs.File):
|
|||||||
'percentage': format_perc(percentage),
|
'percentage': format_perc(percentage),
|
||||||
'dupe_count': format_dupe_count(dupe_count),
|
'dupe_count': format_dupe_count(dupe_count),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _read_info(self, field):
|
def _read_info(self, field):
|
||||||
fs.File._read_info(self, field)
|
fs.File._read_info(self, field)
|
||||||
if field == 'dimensions':
|
if field == 'dimensions':
|
||||||
@ -99,7 +99,7 @@ class Photo(fs.File):
|
|||||||
self.dimensions = (self.dimensions[1], self.dimensions[0])
|
self.dimensions = (self.dimensions[1], self.dimensions[0])
|
||||||
elif field == 'exif_timestamp':
|
elif field == 'exif_timestamp':
|
||||||
self.exif_timestamp = self._get_exif_timestamp()
|
self.exif_timestamp = self._get_exif_timestamp()
|
||||||
|
|
||||||
def get_blocks(self, block_count_per_side):
|
def get_blocks(self, block_count_per_side):
|
||||||
return self._plat_get_blocks(block_count_per_side, self._get_orientation())
|
return self._plat_get_blocks(block_count_per_side, self._get_orientation())
|
||||||
|
|
||||||
|
@ -1,27 +1,31 @@
|
|||||||
# Created On: 2011/09/16
|
# Created On: 2011/09/16
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
|
|
||||||
from core.prioritize import (KindCategory, FolderCategory, FilenameCategory, NumericalCategory,
|
from core.prioritize import (
|
||||||
SizeCategory, MtimeCategory)
|
KindCategory, FolderCategory, FilenameCategory, NumericalCategory,
|
||||||
|
SizeCategory, MtimeCategory
|
||||||
|
)
|
||||||
|
|
||||||
coltr = trget('columns')
|
coltr = trget('columns')
|
||||||
|
|
||||||
class DimensionsCategory(NumericalCategory):
|
class DimensionsCategory(NumericalCategory):
|
||||||
NAME = coltr("Dimensions")
|
NAME = coltr("Dimensions")
|
||||||
|
|
||||||
def extract_value(self, dupe):
|
def extract_value(self, dupe):
|
||||||
return dupe.dimensions
|
return dupe.dimensions
|
||||||
|
|
||||||
def invert_numerical_value(self, value):
|
def invert_numerical_value(self, value):
|
||||||
width, height = value
|
width, height = value
|
||||||
return (-width, -height)
|
return (-width, -height)
|
||||||
|
|
||||||
def all_categories():
|
def all_categories():
|
||||||
return [KindCategory, FolderCategory, FilenameCategory, SizeCategory, DimensionsCategory,
|
return [
|
||||||
MtimeCategory]
|
KindCategory, FolderCategory, FilenameCategory, SizeCategory, DimensionsCategory,
|
||||||
|
MtimeCategory
|
||||||
|
]
|
||||||
|
15
package.py
15
package.py
@ -16,9 +16,11 @@ import platform
|
|||||||
import glob
|
import glob
|
||||||
|
|
||||||
from hscommon.plat import ISWINDOWS, ISLINUX
|
from hscommon.plat import ISWINDOWS, ISLINUX
|
||||||
from hscommon.build import (add_to_pythonpath, print_and_do, copy_packages, build_debian_changelog,
|
from hscommon.build import (
|
||||||
|
add_to_pythonpath, print_and_do, copy_packages, build_debian_changelog,
|
||||||
copy_qt_plugins, get_module_version, filereplace, copy, setup_package_argparser,
|
copy_qt_plugins, get_module_version, filereplace, copy, setup_package_argparser,
|
||||||
package_cocoa_app_in_dmg, copy_all, find_in_path)
|
package_cocoa_app_in_dmg, copy_all, find_in_path
|
||||||
|
)
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
parser = ArgumentParser()
|
parser = ArgumentParser()
|
||||||
@ -52,7 +54,8 @@ def package_windows(edition, dev):
|
|||||||
plugin_names = ['accessible', 'codecs', 'iconengines', 'imageformats']
|
plugin_names = ['accessible', 'codecs', 'iconengines', 'imageformats']
|
||||||
copy_qt_plugins(plugin_names, plugin_dest)
|
copy_qt_plugins(plugin_names, plugin_dest)
|
||||||
|
|
||||||
# Since v4.2.3, cx_freeze started to falsely include tkinter in the package. We exclude it explicitly because of that.
|
# Since v4.2.3, cx_freeze started to falsely include tkinter in the package. We exclude it
|
||||||
|
# explicitly because of that.
|
||||||
options = {
|
options = {
|
||||||
'build_exe': {
|
'build_exe': {
|
||||||
'includes': 'atexit',
|
'includes': 'atexit',
|
||||||
@ -151,8 +154,10 @@ def package_debian_distribution(edition, distribution):
|
|||||||
changelog_dest = op.join(debdest, 'changelog')
|
changelog_dest = op.join(debdest, 'changelog')
|
||||||
project_name = debopts['pkgname']
|
project_name = debopts['pkgname']
|
||||||
from_version = {'se': '2.9.2', 'me': '5.7.2', 'pe': '1.8.5'}[edition]
|
from_version = {'se': '2.9.2', 'me': '5.7.2', 'pe': '1.8.5'}[edition]
|
||||||
build_debian_changelog(changelogpath, changelog_dest, project_name, from_version=from_version,
|
build_debian_changelog(
|
||||||
distribution=distribution)
|
changelogpath, changelog_dest, project_name, from_version=from_version,
|
||||||
|
distribution=distribution
|
||||||
|
)
|
||||||
shutil.copy(op.join('images', ed('dg{0}_logo_128.png')), srcpath)
|
shutil.copy(op.join('images', ed('dg{0}_logo_128.png')), srcpath)
|
||||||
os.chdir(destpath)
|
os.chdir(destpath)
|
||||||
cmd = "dpkg-buildpackage -S"
|
cmd = "dpkg-buildpackage -S"
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-04-25
|
# Created On: 2009-04-25
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@ -14,13 +14,12 @@ from PyQt5.QtGui import QDesktopServices
|
|||||||
from PyQt5.QtWidgets import QApplication, QFileDialog, QDialog, QMessageBox
|
from PyQt5.QtWidgets import QApplication, QFileDialog, QDialog, QMessageBox
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
from hscommon.plat import ISLINUX
|
|
||||||
from hscommon import desktop
|
from hscommon import desktop
|
||||||
|
|
||||||
from qtlib.about_box import AboutBox
|
from qtlib.about_box import AboutBox
|
||||||
from qtlib.recent import Recent
|
from qtlib.recent import Recent
|
||||||
from qtlib.util import createActions
|
from qtlib.util import createActions
|
||||||
from qtlib.progress_window import ProgressWindow
|
from qtlib.progress_window import ProgressWindow
|
||||||
|
|
||||||
from . import platform
|
from . import platform
|
||||||
from .result_window import ResultWindow
|
from .result_window import ResultWindow
|
||||||
@ -35,13 +34,13 @@ class DupeGuru(QObject):
|
|||||||
MODELCLASS = None
|
MODELCLASS = None
|
||||||
LOGO_NAME = '<replace this>'
|
LOGO_NAME = '<replace this>'
|
||||||
NAME = '<replace this>'
|
NAME = '<replace this>'
|
||||||
|
|
||||||
DETAILS_DIALOG_CLASS = None
|
DETAILS_DIALOG_CLASS = None
|
||||||
RESULT_WINDOW_CLASS = ResultWindow
|
RESULT_WINDOW_CLASS = ResultWindow
|
||||||
RESULT_MODEL_CLASS = None
|
RESULT_MODEL_CLASS = None
|
||||||
PREFERENCES_CLASS = None
|
PREFERENCES_CLASS = None
|
||||||
PREFERENCES_DIALOG_CLASS = None
|
PREFERENCES_DIALOG_CLASS = None
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.prefs = self.PREFERENCES_CLASS()
|
self.prefs = self.PREFERENCES_CLASS()
|
||||||
@ -49,7 +48,7 @@ class DupeGuru(QObject):
|
|||||||
self.model = self.MODELCLASS(view=self)
|
self.model = self.MODELCLASS(view=self)
|
||||||
self._setup()
|
self._setup()
|
||||||
self.prefsChanged.emit(self.prefs)
|
self.prefsChanged.emit(self.prefs)
|
||||||
|
|
||||||
#--- Private
|
#--- Private
|
||||||
def _setup(self):
|
def _setup(self):
|
||||||
self._setupActions()
|
self._setupActions()
|
||||||
@ -58,24 +57,24 @@ class DupeGuru(QObject):
|
|||||||
self.recentResults.mustOpenItem.connect(self.model.load_from)
|
self.recentResults.mustOpenItem.connect(self.model.load_from)
|
||||||
self.directories_dialog = DirectoriesDialog(self)
|
self.directories_dialog = DirectoriesDialog(self)
|
||||||
self.resultWindow = self.RESULT_WINDOW_CLASS(self.directories_dialog, self)
|
self.resultWindow = self.RESULT_WINDOW_CLASS(self.directories_dialog, self)
|
||||||
self.progress_window = ProgressWindow(self.resultWindow, self.model.progress_window)
|
self.progress_window = ProgressWindow(self.resultWindow, self.model.progress_window)
|
||||||
self.details_dialog = self.DETAILS_DIALOG_CLASS(self.resultWindow, self)
|
self.details_dialog = self.DETAILS_DIALOG_CLASS(self.resultWindow, self)
|
||||||
self.problemDialog = ProblemDialog(parent=self.resultWindow, model=self.model.problem_dialog)
|
self.problemDialog = ProblemDialog(parent=self.resultWindow, model=self.model.problem_dialog)
|
||||||
self.ignoreListDialog = IgnoreListDialog(parent=self.resultWindow, model=self.model.ignore_list_dialog)
|
self.ignoreListDialog = IgnoreListDialog(parent=self.resultWindow, model=self.model.ignore_list_dialog)
|
||||||
self.deletionOptions = DeletionOptions(parent=self.resultWindow, model=self.model.deletion_options)
|
self.deletionOptions = DeletionOptions(parent=self.resultWindow, model=self.model.deletion_options)
|
||||||
self.preferences_dialog = self.PREFERENCES_DIALOG_CLASS(self.resultWindow, self)
|
self.preferences_dialog = self.PREFERENCES_DIALOG_CLASS(self.resultWindow, self)
|
||||||
self.about_box = AboutBox(self.resultWindow, self)
|
self.about_box = AboutBox(self.resultWindow, self)
|
||||||
|
|
||||||
self.directories_dialog.show()
|
self.directories_dialog.show()
|
||||||
self.model.load()
|
self.model.load()
|
||||||
|
|
||||||
# The timer scheme is because if the nag is not shown before the application is
|
# The timer scheme is because if the nag is not shown before the application is
|
||||||
# completely initialized, the nag will be shown before the app shows up in the task bar
|
# completely initialized, the nag will be shown before the app shows up in the task bar
|
||||||
# In some circumstances, the nag is hidden by other window, which may make the user think
|
# In some circumstances, the nag is hidden by other window, which may make the user think
|
||||||
# that the application haven't launched.
|
# that the application haven't launched.
|
||||||
QTimer.singleShot(0, self.finishedLaunching)
|
QTimer.singleShot(0, self.finishedLaunching)
|
||||||
QCoreApplication.instance().aboutToQuit.connect(self.application_will_terminate)
|
QCoreApplication.instance().aboutToQuit.connect(self.application_will_terminate)
|
||||||
|
|
||||||
def _setupActions(self):
|
def _setupActions(self):
|
||||||
# Setup actions that are common to both the directory dialog and the results window.
|
# Setup actions that are common to both the directory dialog and the results window.
|
||||||
# (name, shortcut, icon, desc, func)
|
# (name, shortcut, icon, desc, func)
|
||||||
@ -88,61 +87,61 @@ class DupeGuru(QObject):
|
|||||||
('actionOpenDebugLog', '', '', tr("Open Debug Log"), self.openDebugLogTriggered),
|
('actionOpenDebugLog', '', '', tr("Open Debug Log"), self.openDebugLogTriggered),
|
||||||
]
|
]
|
||||||
createActions(ACTIONS, self)
|
createActions(ACTIONS, self)
|
||||||
|
|
||||||
def _update_options(self):
|
def _update_options(self):
|
||||||
self.model.scanner.mix_file_kind = self.prefs.mix_file_kind
|
self.model.scanner.mix_file_kind = self.prefs.mix_file_kind
|
||||||
self.model.options['escape_filter_regexp'] = self.prefs.use_regexp
|
self.model.options['escape_filter_regexp'] = 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['ignore_hardlink_matches'] = self.prefs.ignore_hardlink_matches
|
self.model.options['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
|
||||||
|
|
||||||
#--- Public
|
#--- Public
|
||||||
def add_selected_to_ignore_list(self):
|
def add_selected_to_ignore_list(self):
|
||||||
self.model.add_selected_to_ignore_list()
|
self.model.add_selected_to_ignore_list()
|
||||||
|
|
||||||
def remove_selected(self):
|
def remove_selected(self):
|
||||||
self.model.remove_selected(self)
|
self.model.remove_selected(self)
|
||||||
|
|
||||||
def confirm(self, title, msg, default_button=QMessageBox.Yes):
|
def confirm(self, title, msg, default_button=QMessageBox.Yes):
|
||||||
active = QApplication.activeWindow()
|
active = QApplication.activeWindow()
|
||||||
buttons = QMessageBox.Yes | QMessageBox.No
|
buttons = QMessageBox.Yes | QMessageBox.No
|
||||||
answer = QMessageBox.question(active, title, msg, buttons, default_button)
|
answer = QMessageBox.question(active, title, msg, buttons, default_button)
|
||||||
return answer == QMessageBox.Yes
|
return answer == QMessageBox.Yes
|
||||||
|
|
||||||
def invokeCustomCommand(self):
|
def invokeCustomCommand(self):
|
||||||
self.model.invoke_custom_command()
|
self.model.invoke_custom_command()
|
||||||
|
|
||||||
def show_details(self):
|
def show_details(self):
|
||||||
self.details_dialog.show()
|
self.details_dialog.show()
|
||||||
|
|
||||||
def showResultsWindow(self):
|
def showResultsWindow(self):
|
||||||
self.resultWindow.show()
|
self.resultWindow.show()
|
||||||
|
|
||||||
#--- Signals
|
#--- Signals
|
||||||
willSavePrefs = pyqtSignal()
|
willSavePrefs = pyqtSignal()
|
||||||
prefsChanged = pyqtSignal(object)
|
prefsChanged = pyqtSignal(object)
|
||||||
|
|
||||||
#--- Events
|
#--- Events
|
||||||
def finishedLaunching(self):
|
def finishedLaunching(self):
|
||||||
if sys.getfilesystemencoding() == 'ascii':
|
if sys.getfilesystemencoding() == 'ascii':
|
||||||
# No need to localize this, it's a debugging message.
|
# No need to localize this, it's a debugging message.
|
||||||
msg = "Something is wrong with the way your system locale is set. If the files you're "\
|
msg = "Something is wrong with the way your system locale is set. If the files you're "\
|
||||||
"scanning have accented letters, you'll probably get a crash. It is advised that "\
|
"scanning have accented letters, you'll probably get a crash. It is advised that "\
|
||||||
"you set your system locale properly."
|
"you set your system locale properly."
|
||||||
QMessageBox.warning(self.directories_dialog, "Wrong Locale", msg)
|
QMessageBox.warning(self.directories_dialog, "Wrong Locale", msg)
|
||||||
|
|
||||||
def application_will_terminate(self):
|
def application_will_terminate(self):
|
||||||
self.willSavePrefs.emit()
|
self.willSavePrefs.emit()
|
||||||
self.prefs.save()
|
self.prefs.save()
|
||||||
self.model.save()
|
self.model.save()
|
||||||
|
|
||||||
def ignoreListTriggered(self):
|
def ignoreListTriggered(self):
|
||||||
self.model.ignore_list_dialog.show()
|
self.model.ignore_list_dialog.show()
|
||||||
|
|
||||||
def openDebugLogTriggered(self):
|
def openDebugLogTriggered(self):
|
||||||
debugLogPath = op.join(self.model.appdata, 'debug.log')
|
debugLogPath = op.join(self.model.appdata, 'debug.log')
|
||||||
desktop.open_path(debugLogPath)
|
desktop.open_path(debugLogPath)
|
||||||
|
|
||||||
def preferencesTriggered(self):
|
def preferencesTriggered(self):
|
||||||
self.preferences_dialog.load()
|
self.preferences_dialog.load()
|
||||||
result = self.preferences_dialog.exec()
|
result = self.preferences_dialog.exec()
|
||||||
@ -151,42 +150,42 @@ class DupeGuru(QObject):
|
|||||||
self.prefs.save()
|
self.prefs.save()
|
||||||
self._update_options()
|
self._update_options()
|
||||||
self.prefsChanged.emit(self.prefs)
|
self.prefsChanged.emit(self.prefs)
|
||||||
|
|
||||||
def quitTriggered(self):
|
def quitTriggered(self):
|
||||||
self.directories_dialog.close()
|
self.directories_dialog.close()
|
||||||
|
|
||||||
def showAboutBoxTriggered(self):
|
def showAboutBoxTriggered(self):
|
||||||
self.about_box.show()
|
self.about_box.show()
|
||||||
|
|
||||||
def showHelpTriggered(self):
|
def showHelpTriggered(self):
|
||||||
base_path = platform.HELP_PATH
|
base_path = platform.HELP_PATH
|
||||||
url = QUrl.fromLocalFile(op.abspath(op.join(base_path, 'index.html')))
|
url = QUrl.fromLocalFile(op.abspath(op.join(base_path, 'index.html')))
|
||||||
QDesktopServices.openUrl(url)
|
QDesktopServices.openUrl(url)
|
||||||
|
|
||||||
#--- model --> view
|
#--- model --> view
|
||||||
def get_default(self, key):
|
def get_default(self, key):
|
||||||
return self.prefs.get_value(key)
|
return self.prefs.get_value(key)
|
||||||
|
|
||||||
def set_default(self, key, value):
|
def set_default(self, key, value):
|
||||||
self.prefs.set_value(key, value)
|
self.prefs.set_value(key, value)
|
||||||
|
|
||||||
def show_message(self, msg):
|
def show_message(self, msg):
|
||||||
window = QApplication.activeWindow()
|
window = QApplication.activeWindow()
|
||||||
QMessageBox.information(window, '', msg)
|
QMessageBox.information(window, '', msg)
|
||||||
|
|
||||||
def ask_yes_no(self, prompt):
|
def ask_yes_no(self, prompt):
|
||||||
return self.confirm('', prompt)
|
return self.confirm('', prompt)
|
||||||
|
|
||||||
def show_results_window(self):
|
def show_results_window(self):
|
||||||
self.showResultsWindow()
|
self.showResultsWindow()
|
||||||
|
|
||||||
def show_problem_dialog(self):
|
def show_problem_dialog(self):
|
||||||
self.problemDialog.show()
|
self.problemDialog.show()
|
||||||
|
|
||||||
def select_dest_folder(self, prompt):
|
def select_dest_folder(self, prompt):
|
||||||
flags = QFileDialog.ShowDirsOnly
|
flags = QFileDialog.ShowDirsOnly
|
||||||
return QFileDialog.getExistingDirectory(self.resultWindow, prompt, '', flags)
|
return QFileDialog.getExistingDirectory(self.resultWindow, prompt, '', flags)
|
||||||
|
|
||||||
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(self.resultWindow, prompt, '', files)
|
destination, chosen_filter = QFileDialog.getSaveFileName(self.resultWindow, prompt, '', files)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# cxfreeze has some problems detecting all dependencies.
|
# cxfreeze has some problems detecting all dependencies.
|
||||||
# This modules explicitly import those problematic modules.
|
# This modules explicitly import those problematic modules.
|
||||||
|
# flake8: noqa
|
||||||
|
|
||||||
import xml.etree.ElementPath
|
import xml.etree.ElementPath
|
||||||
import gzip
|
import gzip
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2012-05-30
|
# Created On: 2012-05-30
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
@ -21,11 +21,11 @@ class DeletionOptions(QDialog):
|
|||||||
self.model = model
|
self.model = model
|
||||||
self._setupUi()
|
self._setupUi()
|
||||||
self.model.view = self
|
self.model.view = self
|
||||||
|
|
||||||
self.linkCheckbox.stateChanged.connect(self.linkCheckboxChanged)
|
self.linkCheckbox.stateChanged.connect(self.linkCheckboxChanged)
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.rejected.connect(self.reject)
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
self.setWindowTitle(tr("Deletion Options"))
|
self.setWindowTitle(tr("Deletion Options"))
|
||||||
self.resize(400, 270)
|
self.resize(400, 270)
|
||||||
@ -34,8 +34,10 @@ class DeletionOptions(QDialog):
|
|||||||
self.verticalLayout.addWidget(self.msgLabel)
|
self.verticalLayout.addWidget(self.msgLabel)
|
||||||
self.linkCheckbox = QCheckBox(tr("Link deleted files"))
|
self.linkCheckbox = QCheckBox(tr("Link deleted files"))
|
||||||
self.verticalLayout.addWidget(self.linkCheckbox)
|
self.verticalLayout.addWidget(self.linkCheckbox)
|
||||||
text = tr("After having deleted a duplicate, place a link targeting the reference file "
|
text = tr(
|
||||||
"to replace the deleted file.")
|
"After having deleted a duplicate, place a link targeting the reference file "
|
||||||
|
"to replace the deleted file."
|
||||||
|
)
|
||||||
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)
|
||||||
@ -46,8 +48,10 @@ class DeletionOptions(QDialog):
|
|||||||
self.linkCheckbox.setText(self.linkCheckbox.text() + tr(" (unsupported)"))
|
self.linkCheckbox.setText(self.linkCheckbox.text() + tr(" (unsupported)"))
|
||||||
self.directCheckbox = QCheckBox(tr("Directly delete files"))
|
self.directCheckbox = QCheckBox(tr("Directly delete files"))
|
||||||
self.verticalLayout.addWidget(self.directCheckbox)
|
self.verticalLayout.addWidget(self.directCheckbox)
|
||||||
text = tr("Instead of sending files to trash, delete them directly. This option is usually "
|
text = tr(
|
||||||
"used as a workaround when the normal deletion method doesn't work.")
|
"Instead of sending files to trash, delete them directly. This option is usually "
|
||||||
|
"used as a workaround when the normal deletion method doesn't work."
|
||||||
|
)
|
||||||
self.directMessageLabel = QLabel(text)
|
self.directMessageLabel = QLabel(text)
|
||||||
self.directMessageLabel.setWordWrap(True)
|
self.directMessageLabel.setWordWrap(True)
|
||||||
self.verticalLayout.addWidget(self.directMessageLabel)
|
self.verticalLayout.addWidget(self.directMessageLabel)
|
||||||
@ -55,15 +59,15 @@ class DeletionOptions(QDialog):
|
|||||||
self.buttonBox.addButton(tr("Proceed"), QDialogButtonBox.AcceptRole)
|
self.buttonBox.addButton(tr("Proceed"), QDialogButtonBox.AcceptRole)
|
||||||
self.buttonBox.addButton(tr("Cancel"), QDialogButtonBox.RejectRole)
|
self.buttonBox.addButton(tr("Cancel"), QDialogButtonBox.RejectRole)
|
||||||
self.verticalLayout.addWidget(self.buttonBox)
|
self.verticalLayout.addWidget(self.buttonBox)
|
||||||
|
|
||||||
#--- Signals
|
#--- Signals
|
||||||
def linkCheckboxChanged(self, changed: int):
|
def linkCheckboxChanged(self, changed: int):
|
||||||
self.model.link_deleted = bool(changed)
|
self.model.link_deleted = bool(changed)
|
||||||
|
|
||||||
#--- model --> view
|
#--- model --> view
|
||||||
def update_msg(self, msg: str):
|
def update_msg(self, msg: str):
|
||||||
self.msgLabel.setText(msg)
|
self.msgLabel.setText(msg)
|
||||||
|
|
||||||
def show(self):
|
def show(self):
|
||||||
self.linkCheckbox.setChecked(self.model.link_deleted)
|
self.linkCheckbox.setChecked(self.model.link_deleted)
|
||||||
self.linkTypeRadio.selected_index = 1 if self.model.use_hardlinks else 0
|
self.linkTypeRadio.selected_index = 1 if self.model.use_hardlinks else 0
|
||||||
@ -73,7 +77,7 @@ class DeletionOptions(QDialog):
|
|||||||
self.model.use_hardlinks = self.linkTypeRadio.selected_index == 1
|
self.model.use_hardlinks = self.linkTypeRadio.selected_index == 1
|
||||||
self.model.direct = self.directCheckbox.isChecked()
|
self.model.direct = self.directCheckbox.isChecked()
|
||||||
return result == QDialog.Accepted
|
return result == QDialog.Accepted
|
||||||
|
|
||||||
def set_hardlink_option_enabled(self, is_enabled: bool):
|
def set_hardlink_option_enabled(self, is_enabled: bool):
|
||||||
self.linkTypeRadio.setEnabled(is_enabled)
|
self.linkTypeRadio.setEnabled(is_enabled)
|
||||||
|
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-04-25
|
# Created On: 2009-04-25
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from PyQt5.QtCore import QRect
|
from PyQt5.QtCore import QRect
|
||||||
from PyQt5.QtWidgets import (QWidget, QFileDialog, QHeaderView, QVBoxLayout, QHBoxLayout, QTreeView,
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QFileDialog, QHeaderView, QVBoxLayout, QHBoxLayout, QTreeView,
|
||||||
QAbstractItemView, QSpacerItem, QSizePolicy, QPushButton, QMainWindow, QMenuBar, QMenu, QLabel,
|
QAbstractItemView, QSpacerItem, QSizePolicy, QPushButton, QMainWindow, QMenuBar, QMenu, QLabel,
|
||||||
QApplication)
|
QApplication
|
||||||
|
)
|
||||||
from PyQt5.QtGui import QPixmap, QIcon
|
from PyQt5.QtGui import QPixmap, QIcon
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
@ -39,7 +41,7 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
self._updateRemoveButton()
|
self._updateRemoveButton()
|
||||||
self._updateLoadResultsButton()
|
self._updateLoadResultsButton()
|
||||||
self._setupBindings()
|
self._setupBindings()
|
||||||
|
|
||||||
def _setupBindings(self):
|
def _setupBindings(self):
|
||||||
self.scanButton.clicked.connect(self.scanButtonClicked)
|
self.scanButton.clicked.connect(self.scanButtonClicked)
|
||||||
self.loadResultsButton.clicked.connect(self.actionLoadResults.trigger)
|
self.loadResultsButton.clicked.connect(self.actionLoadResults.trigger)
|
||||||
@ -51,7 +53,7 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
self.recentFolders.mustOpenItem.connect(self.app.model.add_directory)
|
self.recentFolders.mustOpenItem.connect(self.app.model.add_directory)
|
||||||
self.directoriesModel.foldersAdded.connect(self.directoriesModelAddedFolders)
|
self.directoriesModel.foldersAdded.connect(self.directoriesModelAddedFolders)
|
||||||
self.app.willSavePrefs.connect(self.appWillSavePrefs)
|
self.app.willSavePrefs.connect(self.appWillSavePrefs)
|
||||||
|
|
||||||
def _setupActions(self):
|
def _setupActions(self):
|
||||||
# (name, shortcut, icon, desc, func)
|
# (name, shortcut, icon, desc, func)
|
||||||
ACTIONS = [
|
ACTIONS = [
|
||||||
@ -60,7 +62,7 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
('actionAddFolder', '', '', tr("Add Folder..."), self.addFolderTriggered),
|
('actionAddFolder', '', '', tr("Add Folder..."), self.addFolderTriggered),
|
||||||
]
|
]
|
||||||
createActions(ACTIONS, self)
|
createActions(ACTIONS, self)
|
||||||
|
|
||||||
def _setupMenu(self):
|
def _setupMenu(self):
|
||||||
self.menubar = QMenuBar(self)
|
self.menubar = QMenuBar(self)
|
||||||
self.menubar.setGeometry(QRect(0, 0, 42, 22))
|
self.menubar.setGeometry(QRect(0, 0, 42, 22))
|
||||||
@ -73,7 +75,7 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
self.menuLoadRecent = QMenu(self.menuFile)
|
self.menuLoadRecent = QMenu(self.menuFile)
|
||||||
self.menuLoadRecent.setTitle(tr("Load Recent Results"))
|
self.menuLoadRecent.setTitle(tr("Load Recent Results"))
|
||||||
self.setMenuBar(self.menubar)
|
self.setMenuBar(self.menubar)
|
||||||
|
|
||||||
self.menuFile.addAction(self.actionLoadResults)
|
self.menuFile.addAction(self.actionLoadResults)
|
||||||
self.menuFile.addAction(self.menuLoadRecent.menuAction())
|
self.menuFile.addAction(self.menuLoadRecent.menuAction())
|
||||||
self.menuFile.addSeparator()
|
self.menuFile.addSeparator()
|
||||||
@ -84,21 +86,21 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
self.menuHelp.addAction(self.app.actionShowHelp)
|
self.menuHelp.addAction(self.app.actionShowHelp)
|
||||||
self.menuHelp.addAction(self.app.actionOpenDebugLog)
|
self.menuHelp.addAction(self.app.actionOpenDebugLog)
|
||||||
self.menuHelp.addAction(self.app.actionAbout)
|
self.menuHelp.addAction(self.app.actionAbout)
|
||||||
|
|
||||||
self.menubar.addAction(self.menuFile.menuAction())
|
self.menubar.addAction(self.menuFile.menuAction())
|
||||||
self.menubar.addAction(self.menuView.menuAction())
|
self.menubar.addAction(self.menuView.menuAction())
|
||||||
self.menubar.addAction(self.menuHelp.menuAction())
|
self.menubar.addAction(self.menuHelp.menuAction())
|
||||||
|
|
||||||
# Recent folders menu
|
# Recent folders menu
|
||||||
self.menuRecentFolders = QMenu()
|
self.menuRecentFolders = QMenu()
|
||||||
self.menuRecentFolders.addAction(self.actionAddFolder)
|
self.menuRecentFolders.addAction(self.actionAddFolder)
|
||||||
self.menuRecentFolders.addSeparator()
|
self.menuRecentFolders.addSeparator()
|
||||||
|
|
||||||
# Recent results menu
|
# Recent results menu
|
||||||
self.menuRecentResults = QMenu()
|
self.menuRecentResults = QMenu()
|
||||||
self.menuRecentResults.addAction(self.actionLoadResults)
|
self.menuRecentResults.addAction(self.actionLoadResults)
|
||||||
self.menuRecentResults.addSeparator()
|
self.menuRecentResults.addSeparator()
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
self.setWindowTitle(self.app.NAME)
|
self.setWindowTitle(self.app.NAME)
|
||||||
self.resize(420, 338)
|
self.resize(420, 338)
|
||||||
@ -110,8 +112,8 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
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 = QAbstractItemView.DoubleClicked|QAbstractItemView.EditKeyPressed\
|
triggers = QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed\
|
||||||
|QAbstractItemView.SelectedClicked
|
| QAbstractItemView.SelectedClicked
|
||||||
self.treeView.setEditTriggers(triggers)
|
self.treeView.setEditTriggers(triggers)
|
||||||
self.treeView.setDragDropOverwriteMode(True)
|
self.treeView.setDragDropOverwriteMode(True)
|
||||||
self.treeView.setDragDropMode(QAbstractItemView.DropOnly)
|
self.treeView.setDragDropMode(QAbstractItemView.DropOnly)
|
||||||
@ -136,41 +138,41 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
self.horizontalLayout.addWidget(self.scanButton)
|
self.horizontalLayout.addWidget(self.scanButton)
|
||||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||||
self.setCentralWidget(self.centralwidget)
|
self.setCentralWidget(self.centralwidget)
|
||||||
|
|
||||||
self._setupActions()
|
self._setupActions()
|
||||||
self._setupMenu()
|
self._setupMenu()
|
||||||
|
|
||||||
if self.app.prefs.directoriesWindowRect is not None:
|
if self.app.prefs.directoriesWindowRect is not None:
|
||||||
self.setGeometry(self.app.prefs.directoriesWindowRect)
|
self.setGeometry(self.app.prefs.directoriesWindowRect)
|
||||||
else:
|
else:
|
||||||
moveToScreenCenter(self)
|
moveToScreenCenter(self)
|
||||||
|
|
||||||
def _setupColumns(self):
|
def _setupColumns(self):
|
||||||
header = self.treeView.header()
|
header = self.treeView.header()
|
||||||
header.setStretchLastSection(False)
|
header.setStretchLastSection(False)
|
||||||
header.setSectionResizeMode(0, QHeaderView.Stretch)
|
header.setSectionResizeMode(0, QHeaderView.Stretch)
|
||||||
header.setSectionResizeMode(1, QHeaderView.Fixed)
|
header.setSectionResizeMode(1, QHeaderView.Fixed)
|
||||||
header.resizeSection(1, 100)
|
header.resizeSection(1, 100)
|
||||||
|
|
||||||
def _updateAddButton(self):
|
def _updateAddButton(self):
|
||||||
if self.recentFolders.isEmpty():
|
if self.recentFolders.isEmpty():
|
||||||
self.addFolderButton.setMenu(None)
|
self.addFolderButton.setMenu(None)
|
||||||
else:
|
else:
|
||||||
self.addFolderButton.setMenu(self.menuRecentFolders)
|
self.addFolderButton.setMenu(self.menuRecentFolders)
|
||||||
|
|
||||||
def _updateRemoveButton(self):
|
def _updateRemoveButton(self):
|
||||||
indexes = self.treeView.selectedIndexes()
|
indexes = self.treeView.selectedIndexes()
|
||||||
if not indexes:
|
if not indexes:
|
||||||
self.removeFolderButton.setEnabled(False)
|
self.removeFolderButton.setEnabled(False)
|
||||||
return
|
return
|
||||||
self.removeFolderButton.setEnabled(True)
|
self.removeFolderButton.setEnabled(True)
|
||||||
|
|
||||||
def _updateLoadResultsButton(self):
|
def _updateLoadResultsButton(self):
|
||||||
if self.app.recentResults.isEmpty():
|
if self.app.recentResults.isEmpty():
|
||||||
self.loadResultsButton.setMenu(None)
|
self.loadResultsButton.setMenu(None)
|
||||||
else:
|
else:
|
||||||
self.loadResultsButton.setMenu(self.menuRecentResults)
|
self.loadResultsButton.setMenu(self.menuRecentResults)
|
||||||
|
|
||||||
#--- QWidget overrides
|
#--- QWidget overrides
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
event.accept()
|
event.accept()
|
||||||
@ -181,7 +183,7 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
event.ignore()
|
event.ignore()
|
||||||
if event.isAccepted():
|
if event.isAccepted():
|
||||||
QApplication.quit()
|
QApplication.quit()
|
||||||
|
|
||||||
#--- Events
|
#--- Events
|
||||||
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")
|
||||||
@ -192,14 +194,14 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
self.lastAddedFolder = dirpath
|
self.lastAddedFolder = dirpath
|
||||||
self.app.model.add_directory(dirpath)
|
self.app.model.add_directory(dirpath)
|
||||||
self.recentFolders.insertItem(dirpath)
|
self.recentFolders.insertItem(dirpath)
|
||||||
|
|
||||||
def appWillSavePrefs(self):
|
def appWillSavePrefs(self):
|
||||||
self.app.prefs.directoriesWindowRect = self.geometry()
|
self.app.prefs.directoriesWindowRect = self.geometry()
|
||||||
|
|
||||||
def directoriesModelAddedFolders(self, folders):
|
def directoriesModelAddedFolders(self, folders):
|
||||||
for folder in folders:
|
for folder in folders:
|
||||||
self.recentFolders.insertItem(folder)
|
self.recentFolders.insertItem(folder)
|
||||||
|
|
||||||
def loadResultsTriggered(self):
|
def loadResultsTriggered(self):
|
||||||
title = tr("Select a results file to load")
|
title = tr("Select a results file to load")
|
||||||
files = ';;'.join([tr("dupeGuru Results (*.dupeguru)"), tr("All Files (*.*)")])
|
files = ';;'.join([tr("dupeGuru Results (*.dupeguru)"), tr("All Files (*.*)")])
|
||||||
@ -207,10 +209,10 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
if destination:
|
if destination:
|
||||||
self.app.model.load_from(destination)
|
self.app.model.load_from(destination)
|
||||||
self.app.recentResults.insertItem(destination)
|
self.app.recentResults.insertItem(destination)
|
||||||
|
|
||||||
def removeFolderButtonClicked(self):
|
def removeFolderButtonClicked(self):
|
||||||
self.directoriesModel.model.remove_selected()
|
self.directoriesModel.model.remove_selected()
|
||||||
|
|
||||||
def scanButtonClicked(self):
|
def scanButtonClicked(self):
|
||||||
if self.app.model.results.is_modified:
|
if self.app.model.results.is_modified:
|
||||||
title = tr("Start a new scan")
|
title = tr("Start a new scan")
|
||||||
@ -218,17 +220,17 @@ class DirectoriesDialog(QMainWindow):
|
|||||||
if not self.app.confirm(title, msg):
|
if not self.app.confirm(title, msg):
|
||||||
return
|
return
|
||||||
self.app.model.start_scanning()
|
self.app.model.start_scanning()
|
||||||
|
|
||||||
def selectionChanged(self, selected, deselected):
|
def selectionChanged(self, selected, deselected):
|
||||||
self._updateRemoveButton()
|
self._updateRemoveButton()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
from . import dg_rc
|
from . import dg_rc # NOQA
|
||||||
from ..testapp import TestApp
|
from ..testapp import TestApp
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
dgapp = TestApp()
|
dgapp = TestApp()
|
||||||
dialog = DirectoriesDialog(None, dgapp)
|
dialog = DirectoriesDialog(None, dgapp)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-04-25
|
# Created On: 2009-04-25
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, Qt, QRect, QUrl, QModelIndex, QItemSelection
|
from PyQt5.QtCore import pyqtSignal, Qt, QRect, QUrl, QModelIndex, QItemSelection
|
||||||
from PyQt5.QtWidgets import (QComboBox, QStyledItemDelegate, QStyle, QStyleOptionComboBox,
|
from PyQt5.QtWidgets import (
|
||||||
QStyleOptionViewItem, QApplication)
|
QComboBox, QStyledItemDelegate, QStyle, QStyleOptionComboBox,
|
||||||
|
QStyleOptionViewItem, QApplication
|
||||||
|
)
|
||||||
from PyQt5.QtGui import QBrush
|
from PyQt5.QtGui import QBrush
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
@ -23,10 +25,10 @@ STATES = [tr("Normal"), tr("Reference"), tr("Excluded")]
|
|||||||
|
|
||||||
class DirectoriesDelegate(QStyledItemDelegate):
|
class DirectoriesDelegate(QStyledItemDelegate):
|
||||||
def createEditor(self, parent, option, index):
|
def createEditor(self, parent, option, index):
|
||||||
editor = QComboBox(parent);
|
editor = QComboBox(parent)
|
||||||
editor.addItems(STATES)
|
editor.addItems(STATES)
|
||||||
return editor
|
return editor
|
||||||
|
|
||||||
def paint(self, painter, option, index):
|
def paint(self, painter, option, index):
|
||||||
self.initStyleOption(option, index)
|
self.initStyleOption(option, index)
|
||||||
# No idea why, but this cast is required if we want to have access to the V4 valuess
|
# No idea why, but this cast is required if we want to have access to the V4 valuess
|
||||||
@ -44,19 +46,19 @@ class DirectoriesDelegate(QStyledItemDelegate):
|
|||||||
painter.drawText(rect, Qt.AlignLeft, option.text)
|
painter.drawText(rect, Qt.AlignLeft, option.text)
|
||||||
else:
|
else:
|
||||||
super().paint(painter, option, index)
|
super().paint(painter, option, index)
|
||||||
|
|
||||||
def setEditorData(self, editor, index):
|
def setEditorData(self, editor, index):
|
||||||
value = index.model().data(index, Qt.EditRole)
|
value = index.model().data(index, Qt.EditRole)
|
||||||
editor.setCurrentIndex(value);
|
editor.setCurrentIndex(value)
|
||||||
editor.showPopup()
|
editor.showPopup()
|
||||||
|
|
||||||
def setModelData(self, editor, model, index):
|
def setModelData(self, editor, model, index):
|
||||||
value = editor.currentIndex()
|
value = editor.currentIndex()
|
||||||
model.setData(index, value, Qt.EditRole)
|
model.setData(index, value, Qt.EditRole)
|
||||||
|
|
||||||
def updateEditorGeometry(self, editor, option, index):
|
def updateEditorGeometry(self, editor, option, index):
|
||||||
editor.setGeometry(option.rect)
|
editor.setGeometry(option.rect)
|
||||||
|
|
||||||
|
|
||||||
class DirectoriesModel(TreeModel):
|
class DirectoriesModel(TreeModel):
|
||||||
def __init__(self, model, view, **kwargs):
|
def __init__(self, model, view, **kwargs):
|
||||||
@ -65,18 +67,18 @@ class DirectoriesModel(TreeModel):
|
|||||||
self.model.view = self
|
self.model.view = self
|
||||||
self.view = view
|
self.view = view
|
||||||
self.view.setModel(self)
|
self.view.setModel(self)
|
||||||
|
|
||||||
self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged)
|
self.view.selectionModel().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)
|
||||||
|
|
||||||
def _getChildren(self):
|
def _getChildren(self):
|
||||||
return list(self.model)
|
return list(self.model)
|
||||||
|
|
||||||
def columnCount(self, parent=QModelIndex()):
|
def columnCount(self, parent=QModelIndex()):
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
def data(self, index, role):
|
def data(self, index, role):
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return None
|
return None
|
||||||
@ -96,7 +98,7 @@ class DirectoriesModel(TreeModel):
|
|||||||
elif state == 2:
|
elif state == 2:
|
||||||
return QBrush(Qt.red)
|
return QBrush(Qt.red)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def dropMimeData(self, mimeData, action, row, column, parentIndex):
|
def dropMimeData(self, mimeData, action, row, column, parentIndex):
|
||||||
# the data in mimeData is urlencoded **in utf-8**!!! What we do is to decode, the mime data
|
# the data in mimeData is urlencoded **in utf-8**!!! What we do is to decode, the mime data
|
||||||
# with 'ascii', which works since it's urlencoded. Then, we pass that to urllib.
|
# with 'ascii', which works since it's urlencoded. Then, we pass that to urllib.
|
||||||
@ -111,7 +113,7 @@ class DirectoriesModel(TreeModel):
|
|||||||
self.foldersAdded.emit(paths)
|
self.foldersAdded.emit(paths)
|
||||||
self.reset()
|
self.reset()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def flags(self, index):
|
def flags(self, index):
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled
|
return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled
|
||||||
@ -119,16 +121,16 @@ class DirectoriesModel(TreeModel):
|
|||||||
if index.column() == 1:
|
if index.column() == 1:
|
||||||
result |= Qt.ItemIsEditable
|
result |= Qt.ItemIsEditable
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def headerData(self, section, orientation, role):
|
def headerData(self, section, orientation, role):
|
||||||
if orientation == Qt.Horizontal:
|
if orientation == Qt.Horizontal:
|
||||||
if role == Qt.DisplayRole and section < len(HEADERS):
|
if role == Qt.DisplayRole and section < len(HEADERS):
|
||||||
return HEADERS[section]
|
return HEADERS[section]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def mimeTypes(self):
|
def mimeTypes(self):
|
||||||
return ['text/uri-list']
|
return ['text/uri-list']
|
||||||
|
|
||||||
def setData(self, index, value, role):
|
def setData(self, index, value, role):
|
||||||
if not index.isValid() or role != Qt.EditRole or index.column() != 1:
|
if not index.isValid() or role != Qt.EditRole or index.column() != 1:
|
||||||
return False
|
return False
|
||||||
@ -136,24 +138,24 @@ class DirectoriesModel(TreeModel):
|
|||||||
ref = node.ref
|
ref = node.ref
|
||||||
ref.state = value
|
ref.state = value
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def supportedDropActions(self):
|
def supportedDropActions(self):
|
||||||
# Normally, the correct action should be ActionLink, but the drop doesn't work. It doesn't
|
# Normally, the correct action should be ActionLink, but the drop doesn't work. It doesn't
|
||||||
# work with ActionMove either. So screw that, and accept anything.
|
# work with ActionMove either. So screw that, and accept anything.
|
||||||
return Qt.ActionMask
|
return Qt.ActionMask
|
||||||
|
|
||||||
#--- Events
|
#--- Events
|
||||||
def selectionChanged(self, selected, deselected):
|
def selectionChanged(self, selected, deselected):
|
||||||
newNodes = [modelIndex.internalPointer().ref for modelIndex in self.view.selectionModel().selectedRows()]
|
newNodes = [modelIndex.internalPointer().ref for modelIndex in self.view.selectionModel().selectedRows()]
|
||||||
self.model.selected_nodes = newNodes
|
self.model.selected_nodes = newNodes
|
||||||
|
|
||||||
#--- Signals
|
#--- Signals
|
||||||
foldersAdded = pyqtSignal(list)
|
foldersAdded = pyqtSignal(list)
|
||||||
|
|
||||||
#--- model --> view
|
#--- model --> view
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def refresh_states(self):
|
def refresh_states(self):
|
||||||
self.refreshData()
|
self.refreshData()
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2012-03-13
|
# Created On: 2012-03-13
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
@ -23,11 +23,11 @@ class IgnoreListDialog(QDialog):
|
|||||||
self.model = model
|
self.model = model
|
||||||
self.model.view = self
|
self.model.view = self
|
||||||
self.table = IgnoreListTable(self.model.ignore_list_table, view=self.tableView)
|
self.table = IgnoreListTable(self.model.ignore_list_table, view=self.tableView)
|
||||||
|
|
||||||
self.removeSelectedButton.clicked.connect(self.model.remove_selected)
|
self.removeSelectedButton.clicked.connect(self.model.remove_selected)
|
||||||
self.clearButton.clicked.connect(self.model.clear)
|
self.clearButton.clicked.connect(self.model.clear)
|
||||||
self.closeButton.clicked.connect(self.accept)
|
self.closeButton.clicked.connect(self.accept)
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
self.setWindowTitle(tr("Ignore List"))
|
self.setWindowTitle(tr("Ignore List"))
|
||||||
self.resize(540, 330)
|
self.resize(540, 330)
|
||||||
@ -45,10 +45,14 @@ class IgnoreListDialog(QDialog):
|
|||||||
self.removeSelectedButton = QPushButton(tr("Remove Selected"))
|
self.removeSelectedButton = QPushButton(tr("Remove Selected"))
|
||||||
self.clearButton = QPushButton(tr("Clear"))
|
self.clearButton = QPushButton(tr("Clear"))
|
||||||
self.closeButton = QPushButton(tr("Close"))
|
self.closeButton = QPushButton(tr("Close"))
|
||||||
self.verticalLayout.addLayout(horizontalWrap([self.removeSelectedButton, self.clearButton,
|
self.verticalLayout.addLayout(
|
||||||
None, self.closeButton]))
|
horizontalWrap([
|
||||||
|
self.removeSelectedButton, self.clearButton,
|
||||||
|
None, self.closeButton
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
#--- model --> view
|
#--- model --> view
|
||||||
def show(self):
|
def show(self):
|
||||||
super().show()
|
super().show()
|
||||||
|
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2011-01-21
|
# Created On: 2011-01-21
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QSize
|
from PyQt5.QtCore import Qt, QSize
|
||||||
from PyQt5.QtWidgets import (QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
|
from PyQt5.QtWidgets import (
|
||||||
QSlider, QSizePolicy, QSpacerItem, QCheckBox, QLineEdit, QMessageBox, QSpinBox)
|
QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
|
||||||
|
QSlider, QSizePolicy, QSpacerItem, QCheckBox, QLineEdit, QMessageBox, QSpinBox
|
||||||
|
)
|
||||||
|
|
||||||
from hscommon.plat import ISOSX, ISLINUX
|
from hscommon.plat import ISOSX, ISLINUX
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
@ -25,12 +27,12 @@ class PreferencesDialogBase(QDialog):
|
|||||||
super().__init__(parent, flags, **kwargs)
|
super().__init__(parent, flags, **kwargs)
|
||||||
self.app = app
|
self.app = app
|
||||||
self._setupUi()
|
self._setupUi()
|
||||||
|
|
||||||
self.filterHardnessSlider.valueChanged['int'].connect(self.filterHardnessLabel.setNum)
|
self.filterHardnessSlider.valueChanged['int'].connect(self.filterHardnessLabel.setNum)
|
||||||
self.buttonBox.clicked.connect(self.buttonClicked)
|
self.buttonBox.clicked.connect(self.buttonClicked)
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.rejected.connect(self.reject)
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
|
|
||||||
def _setupScanTypeBox(self, labels):
|
def _setupScanTypeBox(self, labels):
|
||||||
self.scanTypeHLayout = QHBoxLayout()
|
self.scanTypeHLayout = QHBoxLayout()
|
||||||
self.scanTypeLabel = QLabel(self)
|
self.scanTypeLabel = QLabel(self)
|
||||||
@ -43,7 +45,7 @@ class PreferencesDialogBase(QDialog):
|
|||||||
self.scanTypeComboBox.addItem(label)
|
self.scanTypeComboBox.addItem(label)
|
||||||
self.scanTypeHLayout.addWidget(self.scanTypeComboBox)
|
self.scanTypeHLayout.addWidget(self.scanTypeComboBox)
|
||||||
self.widgetsVLayout.addLayout(self.scanTypeHLayout)
|
self.widgetsVLayout.addLayout(self.scanTypeHLayout)
|
||||||
|
|
||||||
def _setupFilterHardnessBox(self):
|
def _setupFilterHardnessBox(self):
|
||||||
self.filterHardnessHLayout = QHBoxLayout()
|
self.filterHardnessHLayout = QHBoxLayout()
|
||||||
self.filterHardnessLabel = QLabel(self)
|
self.filterHardnessLabel = QLabel(self)
|
||||||
@ -82,7 +84,7 @@ class PreferencesDialogBase(QDialog):
|
|||||||
self.filterHardnessHLayoutSub2.addWidget(self.fewerResultsLabel)
|
self.filterHardnessHLayoutSub2.addWidget(self.fewerResultsLabel)
|
||||||
self.filterHardnessVLayout.addLayout(self.filterHardnessHLayoutSub2)
|
self.filterHardnessVLayout.addLayout(self.filterHardnessHLayoutSub2)
|
||||||
self.filterHardnessHLayout.addLayout(self.filterHardnessVLayout)
|
self.filterHardnessHLayout.addLayout(self.filterHardnessVLayout)
|
||||||
|
|
||||||
def _setupBottomPart(self):
|
def _setupBottomPart(self):
|
||||||
# The bottom part of the pref panel is always the same in all editions.
|
# The bottom part of the pref panel is always the same in all editions.
|
||||||
self.fontSizeLabel = QLabel(tr("Font size:"))
|
self.fontSizeLabel = QLabel(tr("Font size:"))
|
||||||
@ -107,18 +109,18 @@ class PreferencesDialogBase(QDialog):
|
|||||||
self.widgetsVLayout.addWidget(self.customCommandLabel)
|
self.widgetsVLayout.addWidget(self.customCommandLabel)
|
||||||
self.customCommandEdit = QLineEdit(self)
|
self.customCommandEdit = QLineEdit(self)
|
||||||
self.widgetsVLayout.addWidget(self.customCommandEdit)
|
self.widgetsVLayout.addWidget(self.customCommandEdit)
|
||||||
|
|
||||||
def _setupAddCheckbox(self, name, label, parent=None):
|
def _setupAddCheckbox(self, name, label, parent=None):
|
||||||
if parent is None:
|
if parent is None:
|
||||||
parent = self
|
parent = self
|
||||||
cb = QCheckBox(parent)
|
cb = QCheckBox(parent)
|
||||||
cb.setText(label)
|
cb.setText(label)
|
||||||
setattr(self, name, cb)
|
setattr(self, name, cb)
|
||||||
|
|
||||||
def _setupPreferenceWidgets(self):
|
def _setupPreferenceWidgets(self):
|
||||||
# Edition-specific
|
# Edition-specific
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
self.setWindowTitle(tr("Preferences"))
|
self.setWindowTitle(tr("Preferences"))
|
||||||
self.resize(304, 263)
|
self.resize(304, 263)
|
||||||
@ -134,15 +136,15 @@ class PreferencesDialogBase(QDialog):
|
|||||||
if (not ISOSX) and (not ISLINUX):
|
if (not ISOSX) and (not ISLINUX):
|
||||||
self.mainVLayout.removeWidget(self.ignoreHardlinkMatches)
|
self.mainVLayout.removeWidget(self.ignoreHardlinkMatches)
|
||||||
self.ignoreHardlinkMatches.setHidden(True)
|
self.ignoreHardlinkMatches.setHidden(True)
|
||||||
|
|
||||||
def _load(self, prefs, setchecked):
|
def _load(self, prefs, setchecked):
|
||||||
# Edition-specific
|
# Edition-specific
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _save(self, prefs, ischecked):
|
def _save(self, prefs, ischecked):
|
||||||
# Edition-specific
|
# Edition-specific
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def load(self, prefs=None):
|
def load(self, prefs=None):
|
||||||
if prefs is None:
|
if prefs is None:
|
||||||
prefs = self.app.prefs
|
prefs = self.app.prefs
|
||||||
@ -163,7 +165,7 @@ class PreferencesDialogBase(QDialog):
|
|||||||
langindex = 0
|
langindex = 0
|
||||||
self.languageComboBox.setCurrentIndex(langindex)
|
self.languageComboBox.setCurrentIndex(langindex)
|
||||||
self._load(prefs, setchecked)
|
self._load(prefs, setchecked)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
prefs = self.app.prefs
|
prefs = self.app.prefs
|
||||||
prefs.filter_hardness = self.filterHardnessSlider.value()
|
prefs.filter_hardness = self.filterHardnessSlider.value()
|
||||||
@ -184,9 +186,10 @@ class PreferencesDialogBase(QDialog):
|
|||||||
QMessageBox.information(self, "", tr("dupeGuru has to restart for language changes to take effect."))
|
QMessageBox.information(self, "", tr("dupeGuru has to restart for language changes to take effect."))
|
||||||
self.app.prefs.language = lang
|
self.app.prefs.language = lang
|
||||||
self._save(prefs, ischecked)
|
self._save(prefs, ischecked)
|
||||||
|
|
||||||
#--- Events
|
#--- Events
|
||||||
def buttonClicked(self, button):
|
def buttonClicked(self, button):
|
||||||
role = self.buttonBox.buttonRole(button)
|
role = self.buttonBox.buttonRole(button)
|
||||||
if role == QDialogButtonBox.ResetRole:
|
if role == QDialogButtonBox.ResetRole:
|
||||||
self.resetToDefaults()
|
self.resetToDefaults()
|
||||||
|
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2011-09-06
|
# Created On: 2011-09-06
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QMimeData, QByteArray
|
from PyQt5.QtCore import Qt, QMimeData, QByteArray
|
||||||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QListView,
|
from PyQt5.QtWidgets import (
|
||||||
QDialogButtonBox, QAbstractItemView, QLabel, QStyle, QSplitter, QWidget, QSizePolicy)
|
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QListView,
|
||||||
|
QDialogButtonBox, QAbstractItemView, QLabel, QStyle, QSplitter, QWidget, QSizePolicy
|
||||||
|
)
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
from qtlib.selectable_list import ComboboxModel, ListviewModel
|
from qtlib.selectable_list import ComboboxModel, ListviewModel
|
||||||
@ -24,7 +26,7 @@ class PrioritizationList(ListviewModel):
|
|||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled
|
return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled
|
||||||
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled
|
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled
|
||||||
|
|
||||||
#--- Drag & Drop
|
#--- Drag & Drop
|
||||||
def dropMimeData(self, mimeData, action, row, column, parentIndex):
|
def dropMimeData(self, mimeData, action, row, column, parentIndex):
|
||||||
if not mimeData.hasFormat(MIME_INDEXES):
|
if not mimeData.hasFormat(MIME_INDEXES):
|
||||||
@ -37,17 +39,17 @@ class PrioritizationList(ListviewModel):
|
|||||||
indexes = list(map(int, strMimeData.split(',')))
|
indexes = list(map(int, strMimeData.split(',')))
|
||||||
self.model.move_indexes(indexes, row)
|
self.model.move_indexes(indexes, row)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def mimeData(self, indexes):
|
def mimeData(self, indexes):
|
||||||
rows = {str(index.row()) for index in indexes}
|
rows = {str(index.row()) for index in indexes}
|
||||||
data = ','.join(rows)
|
data = ','.join(rows)
|
||||||
mimeData = QMimeData()
|
mimeData = QMimeData()
|
||||||
mimeData.setData(MIME_INDEXES, QByteArray(data.encode()))
|
mimeData.setData(MIME_INDEXES, QByteArray(data.encode()))
|
||||||
return mimeData
|
return mimeData
|
||||||
|
|
||||||
def mimeTypes(self):
|
def mimeTypes(self):
|
||||||
return [MIME_INDEXES]
|
return [MIME_INDEXES]
|
||||||
|
|
||||||
def supportedDropActions(self):
|
def supportedDropActions(self):
|
||||||
return Qt.MoveAction
|
return Qt.MoveAction
|
||||||
|
|
||||||
@ -59,9 +61,11 @@ class PrioritizeDialog(QDialog):
|
|||||||
self.model = PrioritizeDialogModel(app=app.model)
|
self.model = PrioritizeDialogModel(app=app.model)
|
||||||
self.categoryList = ComboboxModel(model=self.model.category_list, view=self.categoryCombobox)
|
self.categoryList = ComboboxModel(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(model=self.model.prioritization_list, view=self.prioritizationListView)
|
self.prioritizationList = PrioritizationList(
|
||||||
|
model=self.model.prioritization_list, view=self.prioritizationListView
|
||||||
|
)
|
||||||
self.model.view = self
|
self.model.view = self
|
||||||
|
|
||||||
self.addCriteriaButton.clicked.connect(self.model.add_selected)
|
self.addCriteriaButton.clicked.connect(self.model.add_selected)
|
||||||
self.removeCriteriaButton.clicked.connect(self.model.remove_selected)
|
self.removeCriteriaButton.clicked.connect(self.model.remove_selected)
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
@ -70,11 +74,13 @@ class PrioritizeDialog(QDialog):
|
|||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
self.setWindowTitle(tr("Re-Prioritize duplicates"))
|
self.setWindowTitle(tr("Re-Prioritize duplicates"))
|
||||||
self.resize(700, 400)
|
self.resize(700, 400)
|
||||||
|
|
||||||
#widgets
|
#widgets
|
||||||
msg = tr("Add criteria to the right box and click OK to send the dupes that correspond the "
|
msg = tr(
|
||||||
|
"Add criteria to the right box and click OK to send the dupes that correspond the "
|
||||||
"best to these criteria to their respective group's "
|
"best to these criteria to their respective group's "
|
||||||
"reference position. Read the help file for more information.")
|
"reference position. Read the help file for more information."
|
||||||
|
)
|
||||||
self.promptLabel = QLabel(msg)
|
self.promptLabel = QLabel(msg)
|
||||||
self.promptLabel.setWordWrap(True)
|
self.promptLabel.setWordWrap(True)
|
||||||
self.categoryCombobox = QComboBox()
|
self.categoryCombobox = QComboBox()
|
||||||
@ -88,7 +94,7 @@ class PrioritizeDialog(QDialog):
|
|||||||
self.prioritizationListView.setSelectionBehavior(QAbstractItemView.SelectRows)
|
self.prioritizationListView.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
self.buttonBox = QDialogButtonBox()
|
self.buttonBox = QDialogButtonBox()
|
||||||
self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
|
self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
|
||||||
|
|
||||||
# layout
|
# layout
|
||||||
self.mainLayout = QVBoxLayout(self)
|
self.mainLayout = QVBoxLayout(self)
|
||||||
self.mainLayout.addWidget(self.promptLabel)
|
self.mainLayout.addWidget(self.promptLabel)
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2010-04-12
|
# Created On: 2010-04-12
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QSpacerItem, QSizePolicy,
|
from PyQt5.QtWidgets import (
|
||||||
QLabel, QTableView, QAbstractItemView, QApplication)
|
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QSpacerItem, QSizePolicy,
|
||||||
|
QLabel, QTableView, QAbstractItemView, QApplication
|
||||||
|
)
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
from .problem_table import ProblemTable
|
from .problem_table import ProblemTable
|
||||||
@ -23,18 +25,20 @@ class ProblemDialog(QDialog):
|
|||||||
self.model = model
|
self.model = model
|
||||||
self.model.view = self
|
self.model.view = self
|
||||||
self.table = ProblemTable(self.model.problem_table, view=self.tableView)
|
self.table = ProblemTable(self.model.problem_table, view=self.tableView)
|
||||||
|
|
||||||
self.revealButton.clicked.connect(self.model.reveal_selected_dupe)
|
self.revealButton.clicked.connect(self.model.reveal_selected_dupe)
|
||||||
self.closeButton.clicked.connect(self.accept)
|
self.closeButton.clicked.connect(self.accept)
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
self.setWindowTitle(tr("Problems!"))
|
self.setWindowTitle(tr("Problems!"))
|
||||||
self.resize(413, 323)
|
self.resize(413, 323)
|
||||||
self.verticalLayout = QVBoxLayout(self)
|
self.verticalLayout = QVBoxLayout(self)
|
||||||
self.label = QLabel(self)
|
self.label = QLabel(self)
|
||||||
msg = tr("There were problems processing some (or all) of the files. The cause of "
|
msg = tr(
|
||||||
|
"There were problems processing some (or all) of the files. The cause of "
|
||||||
"these problems are described in the table below. Those files were not "
|
"these problems are described in the table below. Those files were not "
|
||||||
"removed from your results.")
|
"removed from your results."
|
||||||
|
)
|
||||||
self.label.setText(msg)
|
self.label.setText(msg)
|
||||||
self.label.setWordWrap(True)
|
self.label.setWordWrap(True)
|
||||||
self.verticalLayout.addWidget(self.label)
|
self.verticalLayout.addWidget(self.label)
|
||||||
@ -58,7 +62,7 @@ class ProblemDialog(QDialog):
|
|||||||
self.closeButton.setDefault(True)
|
self.closeButton.setDefault(True)
|
||||||
self.horizontalLayout.addWidget(self.closeButton)
|
self.horizontalLayout.addWidget(self.closeButton)
|
||||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
@ -67,4 +71,4 @@ if __name__ == '__main__':
|
|||||||
dgapp = TestApp()
|
dgapp = TestApp()
|
||||||
dialog = ProblemDialog(None, dgapp)
|
dialog = ProblemDialog(None, dgapp)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-04-25
|
# Created On: 2009-04-25
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QRect
|
from PyQt5.QtCore import Qt, QRect
|
||||||
from PyQt5.QtWidgets import (QMainWindow, QMenu, QLabel, QFileDialog, QMenuBar, QWidget,
|
from PyQt5.QtWidgets import (
|
||||||
QVBoxLayout, QAbstractItemView, QStatusBar, QDialog, QPushButton, QCheckBox)
|
QMainWindow, QMenu, QLabel, QFileDialog, QMenuBar, QWidget,
|
||||||
|
QVBoxLayout, QAbstractItemView, QStatusBar, QDialog, QPushButton, QCheckBox
|
||||||
|
)
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
from qtlib.util import moveToScreenCenter, horizontalWrap, createActions
|
from qtlib.util import moveToScreenCenter, horizontalWrap, createActions
|
||||||
@ -28,7 +30,7 @@ class ResultWindow(QMainWindow):
|
|||||||
self.resultsModel = app.RESULT_MODEL_CLASS(self.app, self.resultsView)
|
self.resultsModel = app.RESULT_MODEL_CLASS(self.app, self.resultsView)
|
||||||
self.stats = StatsLabel(app.model.stats_label, self.statusLabel)
|
self.stats = StatsLabel(app.model.stats_label, self.statusLabel)
|
||||||
self._update_column_actions_status()
|
self._update_column_actions_status()
|
||||||
|
|
||||||
self.menuColumns.triggered.connect(self.columnToggled)
|
self.menuColumns.triggered.connect(self.columnToggled)
|
||||||
self.resultsView.doubleClicked.connect(self.resultsDoubleClicked)
|
self.resultsView.doubleClicked.connect(self.resultsDoubleClicked)
|
||||||
self.resultsView.spacePressed.connect(self.resultsSpacePressed)
|
self.resultsView.spacePressed.connect(self.resultsSpacePressed)
|
||||||
@ -37,7 +39,7 @@ class ResultWindow(QMainWindow):
|
|||||||
self.deltaValuesCheckBox.stateChanged.connect(self.deltaTriggered)
|
self.deltaValuesCheckBox.stateChanged.connect(self.deltaTriggered)
|
||||||
self.searchEdit.searchChanged.connect(self.searchChanged)
|
self.searchEdit.searchChanged.connect(self.searchChanged)
|
||||||
self.app.willSavePrefs.connect(self.appWillSavePrefs)
|
self.app.willSavePrefs.connect(self.appWillSavePrefs)
|
||||||
|
|
||||||
def _setupActions(self):
|
def _setupActions(self):
|
||||||
# (name, shortcut, icon, desc, func)
|
# (name, shortcut, icon, desc, func)
|
||||||
ACTIONS = [
|
ACTIONS = [
|
||||||
@ -50,11 +52,23 @@ class ResultWindow(QMainWindow):
|
|||||||
('actionCopyMarked', 'Ctrl+Shift+M', '', tr("Copy Marked to..."), self.copyTriggered),
|
('actionCopyMarked', 'Ctrl+Shift+M', '', tr("Copy Marked to..."), self.copyTriggered),
|
||||||
('actionRemoveMarked', 'Ctrl+R', '', tr("Remove Marked from Results"), self.removeMarkedTriggered),
|
('actionRemoveMarked', 'Ctrl+R', '', tr("Remove Marked from Results"), self.removeMarkedTriggered),
|
||||||
('actionReprioritize', '', '', tr("Re-Prioritize Results..."), self.reprioritizeTriggered),
|
('actionReprioritize', '', '', tr("Re-Prioritize Results..."), self.reprioritizeTriggered),
|
||||||
('actionRemoveSelected', 'Ctrl+Del', '', tr("Remove Selected from Results"), self.removeSelectedTriggered),
|
(
|
||||||
('actionIgnoreSelected', 'Ctrl+Shift+Del', '', tr("Add Selected to Ignore List"), self.addToIgnoreListTriggered),
|
'actionRemoveSelected', 'Ctrl+Del', '',
|
||||||
('actionMakeSelectedReference', 'Ctrl+Space', '', tr("Make Selected into Reference"), self.app.model.make_selected_reference),
|
tr("Remove Selected from Results"), self.removeSelectedTriggered
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'actionIgnoreSelected', 'Ctrl+Shift+Del', '',
|
||||||
|
tr("Add Selected to Ignore List"), self.addToIgnoreListTriggered
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'actionMakeSelectedReference', 'Ctrl+Space', '',
|
||||||
|
tr("Make Selected into Reference"), self.app.model.make_selected_reference
|
||||||
|
),
|
||||||
('actionOpenSelected', 'Ctrl+O', '', tr("Open Selected with Default Application"), self.openTriggered),
|
('actionOpenSelected', 'Ctrl+O', '', tr("Open Selected with Default Application"), self.openTriggered),
|
||||||
('actionRevealSelected', 'Ctrl+Shift+O', '', tr("Open Containing Folder of Selected"), self.revealTriggered),
|
(
|
||||||
|
'actionRevealSelected', 'Ctrl+Shift+O', '',
|
||||||
|
tr("Open Containing Folder of Selected"), self.revealTriggered
|
||||||
|
),
|
||||||
('actionRenameSelected', 'F2', '', tr("Rename Selected"), self.renameTriggered),
|
('actionRenameSelected', 'F2', '', tr("Rename Selected"), self.renameTriggered),
|
||||||
('actionMarkAll', 'Ctrl+A', '', tr("Mark All"), self.markAllTriggered),
|
('actionMarkAll', 'Ctrl+A', '', tr("Mark All"), self.markAllTriggered),
|
||||||
('actionMarkNone', 'Ctrl+Shift+A', '', tr("Mark None"), self.markNoneTriggered),
|
('actionMarkNone', 'Ctrl+Shift+A', '', tr("Mark None"), self.markNoneTriggered),
|
||||||
@ -68,7 +82,7 @@ class ResultWindow(QMainWindow):
|
|||||||
createActions(ACTIONS, self)
|
createActions(ACTIONS, self)
|
||||||
self.actionDelta.setCheckable(True)
|
self.actionDelta.setCheckable(True)
|
||||||
self.actionPowerMarker.setCheckable(True)
|
self.actionPowerMarker.setCheckable(True)
|
||||||
|
|
||||||
def _setupMenu(self):
|
def _setupMenu(self):
|
||||||
self.menubar = QMenuBar()
|
self.menubar = QMenuBar()
|
||||||
self.menubar.setGeometry(QRect(0, 0, 630, 22))
|
self.menubar.setGeometry(QRect(0, 0, 630, 22))
|
||||||
@ -85,7 +99,7 @@ class ResultWindow(QMainWindow):
|
|||||||
self.menuHelp = QMenu(self.menubar)
|
self.menuHelp = QMenu(self.menubar)
|
||||||
self.menuHelp.setTitle(tr("Help"))
|
self.menuHelp.setTitle(tr("Help"))
|
||||||
self.setMenuBar(self.menubar)
|
self.setMenuBar(self.menubar)
|
||||||
|
|
||||||
self.menuActions.addAction(self.actionDeleteMarked)
|
self.menuActions.addAction(self.actionDeleteMarked)
|
||||||
self.menuActions.addAction(self.actionMoveMarked)
|
self.menuActions.addAction(self.actionMoveMarked)
|
||||||
self.menuActions.addAction(self.actionCopyMarked)
|
self.menuActions.addAction(self.actionCopyMarked)
|
||||||
@ -118,14 +132,14 @@ class ResultWindow(QMainWindow):
|
|||||||
self.menuFile.addAction(self.actionExportToCSV)
|
self.menuFile.addAction(self.actionExportToCSV)
|
||||||
self.menuFile.addSeparator()
|
self.menuFile.addSeparator()
|
||||||
self.menuFile.addAction(self.app.actionQuit)
|
self.menuFile.addAction(self.app.actionQuit)
|
||||||
|
|
||||||
self.menubar.addAction(self.menuFile.menuAction())
|
self.menubar.addAction(self.menuFile.menuAction())
|
||||||
self.menubar.addAction(self.menuMark.menuAction())
|
self.menubar.addAction(self.menuMark.menuAction())
|
||||||
self.menubar.addAction(self.menuActions.menuAction())
|
self.menubar.addAction(self.menuActions.menuAction())
|
||||||
self.menubar.addAction(self.menuColumns.menuAction())
|
self.menubar.addAction(self.menuColumns.menuAction())
|
||||||
self.menubar.addAction(self.menuView.menuAction())
|
self.menubar.addAction(self.menuView.menuAction())
|
||||||
self.menubar.addAction(self.menuHelp.menuAction())
|
self.menubar.addAction(self.menuHelp.menuAction())
|
||||||
|
|
||||||
# Columns menu
|
# Columns menu
|
||||||
menu = self.menuColumns
|
menu = self.menuColumns
|
||||||
self._column_actions = []
|
self._column_actions = []
|
||||||
@ -138,7 +152,7 @@ class ResultWindow(QMainWindow):
|
|||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
action = menu.addAction(tr("Reset to Defaults"))
|
action = menu.addAction(tr("Reset to Defaults"))
|
||||||
action.item_index = -1
|
action.item_index = -1
|
||||||
|
|
||||||
# Action menu
|
# Action menu
|
||||||
actionMenu = QMenu(tr("Actions"), self.menubar)
|
actionMenu = QMenu(tr("Actions"), self.menubar)
|
||||||
actionMenu.addAction(self.actionDeleteMarked)
|
actionMenu.addAction(self.actionDeleteMarked)
|
||||||
@ -156,7 +170,7 @@ class ResultWindow(QMainWindow):
|
|||||||
actionMenu.addAction(self.actionRenameSelected)
|
actionMenu.addAction(self.actionRenameSelected)
|
||||||
self.actionActions.setMenu(actionMenu)
|
self.actionActions.setMenu(actionMenu)
|
||||||
self.actionsButton.setMenu(self.actionActions.menu())
|
self.actionsButton.setMenu(self.actionActions.menu())
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
self.setWindowTitle(tr("{} Results").format(self.app.NAME))
|
self.setWindowTitle(tr("{} Results").format(self.app.NAME))
|
||||||
self.resize(630, 514)
|
self.resize(630, 514)
|
||||||
@ -170,8 +184,10 @@ class ResultWindow(QMainWindow):
|
|||||||
self.deltaValuesCheckBox = QCheckBox(tr("Delta Values"))
|
self.deltaValuesCheckBox = QCheckBox(tr("Delta Values"))
|
||||||
self.searchEdit = SearchEdit()
|
self.searchEdit = SearchEdit()
|
||||||
self.searchEdit.setMaximumWidth(300)
|
self.searchEdit.setMaximumWidth(300)
|
||||||
self.horizontalLayout = horizontalWrap([self.actionsButton, self.detailsButton,
|
self.horizontalLayout = horizontalWrap([
|
||||||
self.dupesOnlyCheckBox, self.deltaValuesCheckBox, None, self.searchEdit, 8])
|
self.actionsButton, self.detailsButton,
|
||||||
|
self.dupesOnlyCheckBox, self.deltaValuesCheckBox, None, self.searchEdit, 8
|
||||||
|
])
|
||||||
self.horizontalLayout.setSpacing(8)
|
self.horizontalLayout.setSpacing(8)
|
||||||
self.verticalLayout.addLayout(self.horizontalLayout)
|
self.verticalLayout.addLayout(self.horizontalLayout)
|
||||||
self.resultsView = ResultsView(self.centralwidget)
|
self.resultsView = ResultsView(self.centralwidget)
|
||||||
@ -193,7 +209,7 @@ class ResultWindow(QMainWindow):
|
|||||||
self.setStatusBar(self.statusbar)
|
self.setStatusBar(self.statusbar)
|
||||||
self.statusLabel = QLabel(self)
|
self.statusLabel = QLabel(self)
|
||||||
self.statusbar.addPermanentWidget(self.statusLabel, 1)
|
self.statusbar.addPermanentWidget(self.statusLabel, 1)
|
||||||
|
|
||||||
if self.app.prefs.resultWindowIsMaximized:
|
if self.app.prefs.resultWindowIsMaximized:
|
||||||
self.setWindowState(self.windowState() | Qt.WindowMaximized)
|
self.setWindowState(self.windowState() | Qt.WindowMaximized)
|
||||||
else:
|
else:
|
||||||
@ -201,85 +217,85 @@ class ResultWindow(QMainWindow):
|
|||||||
self.setGeometry(self.app.prefs.resultWindowRect)
|
self.setGeometry(self.app.prefs.resultWindowRect)
|
||||||
else:
|
else:
|
||||||
moveToScreenCenter(self)
|
moveToScreenCenter(self)
|
||||||
|
|
||||||
#--- Private
|
#--- Private
|
||||||
def _update_column_actions_status(self):
|
def _update_column_actions_status(self):
|
||||||
# Update menu checked state
|
# Update menu checked state
|
||||||
menu_items = self.app.model.result_table.columns.menu_items()
|
menu_items = self.app.model.result_table.columns.menu_items()
|
||||||
for action, (display, visible) in zip(self._column_actions, menu_items):
|
for action, (display, visible) in zip(self._column_actions, menu_items):
|
||||||
action.setChecked(visible)
|
action.setChecked(visible)
|
||||||
|
|
||||||
#--- Actions
|
#--- Actions
|
||||||
def actionsTriggered(self):
|
def actionsTriggered(self):
|
||||||
self.actionsButton.showMenu()
|
self.actionsButton.showMenu()
|
||||||
|
|
||||||
def addToIgnoreListTriggered(self):
|
def addToIgnoreListTriggered(self):
|
||||||
self.app.model.add_selected_to_ignore_list()
|
self.app.model.add_selected_to_ignore_list()
|
||||||
|
|
||||||
def copyTriggered(self):
|
def copyTriggered(self):
|
||||||
self.app.model.copy_or_move_marked(True)
|
self.app.model.copy_or_move_marked(True)
|
||||||
|
|
||||||
def deleteTriggered(self):
|
def deleteTriggered(self):
|
||||||
self.app.model.delete_marked()
|
self.app.model.delete_marked()
|
||||||
|
|
||||||
def deltaTriggered(self, state=None):
|
def deltaTriggered(self, state=None):
|
||||||
# The sender can be either the action or the checkbox, but both have a isChecked() method.
|
# The sender can be either the action or the checkbox, but both have a isChecked() method.
|
||||||
self.resultsModel.delta_values = self.sender().isChecked()
|
self.resultsModel.delta_values = self.sender().isChecked()
|
||||||
self.actionDelta.setChecked(self.resultsModel.delta_values)
|
self.actionDelta.setChecked(self.resultsModel.delta_values)
|
||||||
self.deltaValuesCheckBox.setChecked(self.resultsModel.delta_values)
|
self.deltaValuesCheckBox.setChecked(self.resultsModel.delta_values)
|
||||||
|
|
||||||
def detailsTriggered(self):
|
def detailsTriggered(self):
|
||||||
self.app.show_details()
|
self.app.show_details()
|
||||||
|
|
||||||
def markAllTriggered(self):
|
def markAllTriggered(self):
|
||||||
self.app.model.mark_all()
|
self.app.model.mark_all()
|
||||||
|
|
||||||
def markInvertTriggered(self):
|
def markInvertTriggered(self):
|
||||||
self.app.model.mark_invert()
|
self.app.model.mark_invert()
|
||||||
|
|
||||||
def markNoneTriggered(self):
|
def markNoneTriggered(self):
|
||||||
self.app.model.mark_none()
|
self.app.model.mark_none()
|
||||||
|
|
||||||
def markSelectedTriggered(self):
|
def markSelectedTriggered(self):
|
||||||
self.app.model.toggle_selected_mark_state()
|
self.app.model.toggle_selected_mark_state()
|
||||||
|
|
||||||
def moveTriggered(self):
|
def moveTriggered(self):
|
||||||
self.app.model.copy_or_move_marked(False)
|
self.app.model.copy_or_move_marked(False)
|
||||||
|
|
||||||
def openTriggered(self):
|
def openTriggered(self):
|
||||||
self.app.model.open_selected()
|
self.app.model.open_selected()
|
||||||
|
|
||||||
def powerMarkerTriggered(self, state=None):
|
def powerMarkerTriggered(self, state=None):
|
||||||
# see deltaTriggered
|
# see deltaTriggered
|
||||||
self.resultsModel.power_marker = self.sender().isChecked()
|
self.resultsModel.power_marker = self.sender().isChecked()
|
||||||
self.actionPowerMarker.setChecked(self.resultsModel.power_marker)
|
self.actionPowerMarker.setChecked(self.resultsModel.power_marker)
|
||||||
self.dupesOnlyCheckBox.setChecked(self.resultsModel.power_marker)
|
self.dupesOnlyCheckBox.setChecked(self.resultsModel.power_marker)
|
||||||
|
|
||||||
def preferencesTriggered(self):
|
def preferencesTriggered(self):
|
||||||
self.app.show_preferences()
|
self.app.show_preferences()
|
||||||
|
|
||||||
def removeMarkedTriggered(self):
|
def removeMarkedTriggered(self):
|
||||||
self.app.model.remove_marked()
|
self.app.model.remove_marked()
|
||||||
|
|
||||||
def removeSelectedTriggered(self):
|
def removeSelectedTriggered(self):
|
||||||
self.app.model.remove_selected()
|
self.app.model.remove_selected()
|
||||||
|
|
||||||
def renameTriggered(self):
|
def renameTriggered(self):
|
||||||
index = self.resultsView.selectionModel().currentIndex()
|
index = self.resultsView.selectionModel().currentIndex()
|
||||||
# Our index is the current row, with column set to 0. Our filename column is 1 and that's
|
# Our index is the current row, with column set to 0. Our filename column is 1 and that's
|
||||||
# what we want.
|
# what we want.
|
||||||
index = index.sibling(index.row(), 1)
|
index = index.sibling(index.row(), 1)
|
||||||
self.resultsView.edit(index)
|
self.resultsView.edit(index)
|
||||||
|
|
||||||
def reprioritizeTriggered(self):
|
def reprioritizeTriggered(self):
|
||||||
dlg = PrioritizeDialog(self, self.app)
|
dlg = PrioritizeDialog(self, self.app)
|
||||||
result = dlg.exec()
|
result = dlg.exec()
|
||||||
if result == QDialog.Accepted:
|
if result == QDialog.Accepted:
|
||||||
dlg.model.perform_reprioritization()
|
dlg.model.perform_reprioritization()
|
||||||
|
|
||||||
def revealTriggered(self):
|
def revealTriggered(self):
|
||||||
self.app.model.reveal_selected()
|
self.app.model.reveal_selected()
|
||||||
|
|
||||||
def saveResultsTriggered(self):
|
def saveResultsTriggered(self):
|
||||||
title = tr("Select a file to save your results to")
|
title = tr("Select a file to save your results to")
|
||||||
files = tr("dupeGuru Results (*.dupeguru)")
|
files = tr("dupeGuru Results (*.dupeguru)")
|
||||||
@ -289,13 +305,13 @@ class ResultWindow(QMainWindow):
|
|||||||
destination = '{}.dupeguru'.format(destination)
|
destination = '{}.dupeguru'.format(destination)
|
||||||
self.app.model.save_as(destination)
|
self.app.model.save_as(destination)
|
||||||
self.app.recentResults.insertItem(destination)
|
self.app.recentResults.insertItem(destination)
|
||||||
|
|
||||||
#--- Events
|
#--- Events
|
||||||
def appWillSavePrefs(self):
|
def appWillSavePrefs(self):
|
||||||
prefs = self.app.prefs
|
prefs = self.app.prefs
|
||||||
prefs.resultWindowIsMaximized = self.isMaximized()
|
prefs.resultWindowIsMaximized = self.isMaximized()
|
||||||
prefs.resultWindowRect = self.geometry()
|
prefs.resultWindowRect = self.geometry()
|
||||||
|
|
||||||
def columnToggled(self, action):
|
def columnToggled(self, action):
|
||||||
index = action.item_index
|
index = action.item_index
|
||||||
if index == -1:
|
if index == -1:
|
||||||
@ -304,16 +320,16 @@ class ResultWindow(QMainWindow):
|
|||||||
else:
|
else:
|
||||||
visible = self.app.model.result_table.columns.toggle_menu_item(index)
|
visible = self.app.model.result_table.columns.toggle_menu_item(index)
|
||||||
action.setChecked(visible)
|
action.setChecked(visible)
|
||||||
|
|
||||||
def contextMenuEvent(self, event):
|
def contextMenuEvent(self, event):
|
||||||
self.actionActions.menu().exec_(event.globalPos())
|
self.actionActions.menu().exec_(event.globalPos())
|
||||||
|
|
||||||
def resultsDoubleClicked(self, modelIndex):
|
def resultsDoubleClicked(self, modelIndex):
|
||||||
self.app.model.open_selected()
|
self.app.model.open_selected()
|
||||||
|
|
||||||
def resultsSpacePressed(self):
|
def resultsSpacePressed(self):
|
||||||
self.app.model.toggle_selected_mark_state()
|
self.app.model.toggle_selected_mark_state()
|
||||||
|
|
||||||
def searchChanged(self):
|
def searchChanged(self):
|
||||||
self.app.model.apply_filter(self.searchEdit.text())
|
self.app.model.apply_filter(self.searchEdit.text())
|
||||||
|
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-04-29
|
# Created On: 2009-04-29
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from PyQt5.QtCore import QSize
|
from PyQt5.QtCore import QSize
|
||||||
from PyQt5.QtWidgets import (QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget,
|
from PyQt5.QtWidgets import (
|
||||||
QApplication)
|
QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget,
|
||||||
|
QApplication
|
||||||
|
)
|
||||||
|
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
from core.scanner import ScanType
|
from core.scanner import ScanType
|
||||||
@ -31,9 +33,9 @@ SCAN_TYPE_ORDER = [
|
|||||||
class PreferencesDialog(PreferencesDialogBase):
|
class PreferencesDialog(PreferencesDialogBase):
|
||||||
def __init__(self, parent, app):
|
def __init__(self, parent, app):
|
||||||
PreferencesDialogBase.__init__(self, parent, app)
|
PreferencesDialogBase.__init__(self, parent, app)
|
||||||
|
|
||||||
self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged)
|
self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged)
|
||||||
|
|
||||||
def _setupPreferenceWidgets(self):
|
def _setupPreferenceWidgets(self):
|
||||||
scanTypeLabels = [
|
scanTypeLabels = [
|
||||||
tr("Filename"),
|
tr("Filename"),
|
||||||
@ -87,7 +89,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self._setupAddCheckbox('debugModeBox', tr("Debug mode (restart required)"))
|
self._setupAddCheckbox('debugModeBox', tr("Debug mode (restart required)"))
|
||||||
self.widgetsVLayout.addWidget(self.debugModeBox)
|
self.widgetsVLayout.addWidget(self.debugModeBox)
|
||||||
self._setupBottomPart()
|
self._setupBottomPart()
|
||||||
|
|
||||||
def _load(self, prefs, setchecked):
|
def _load(self, prefs, setchecked):
|
||||||
scan_type_index = SCAN_TYPE_ORDER.index(prefs.scan_type)
|
scan_type_index = SCAN_TYPE_ORDER.index(prefs.scan_type)
|
||||||
self.scanTypeComboBox.setCurrentIndex(scan_type_index)
|
self.scanTypeComboBox.setCurrentIndex(scan_type_index)
|
||||||
@ -99,7 +101,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
setchecked(self.tagYearBox, prefs.scan_tag_year)
|
setchecked(self.tagYearBox, prefs.scan_tag_year)
|
||||||
setchecked(self.matchSimilarBox, prefs.match_similar)
|
setchecked(self.matchSimilarBox, prefs.match_similar)
|
||||||
setchecked(self.wordWeightingBox, prefs.word_weighting)
|
setchecked(self.wordWeightingBox, prefs.word_weighting)
|
||||||
|
|
||||||
def _save(self, prefs, ischecked):
|
def _save(self, prefs, ischecked):
|
||||||
prefs.scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
prefs.scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
||||||
prefs.scan_tag_track = ischecked(self.tagTrackBox)
|
prefs.scan_tag_track = ischecked(self.tagTrackBox)
|
||||||
@ -110,15 +112,17 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
prefs.scan_tag_year = ischecked(self.tagYearBox)
|
prefs.scan_tag_year = ischecked(self.tagYearBox)
|
||||||
prefs.match_similar = ischecked(self.matchSimilarBox)
|
prefs.match_similar = ischecked(self.matchSimilarBox)
|
||||||
prefs.word_weighting = ischecked(self.wordWeightingBox)
|
prefs.word_weighting = ischecked(self.wordWeightingBox)
|
||||||
|
|
||||||
def resetToDefaults(self):
|
def resetToDefaults(self):
|
||||||
self.load(preferences.Preferences())
|
self.load(preferences.Preferences())
|
||||||
|
|
||||||
#--- Events
|
#--- Events
|
||||||
def scanTypeChanged(self, index):
|
def scanTypeChanged(self, index):
|
||||||
scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
||||||
word_based = scan_type in (ScanType.Filename, ScanType.Fields, ScanType.FieldsNoOrder,
|
word_based = scan_type in (
|
||||||
ScanType.Tag)
|
ScanType.Filename, ScanType.Fields, ScanType.FieldsNoOrder,
|
||||||
|
ScanType.Tag
|
||||||
|
)
|
||||||
tag_based = scan_type == ScanType.Tag
|
tag_based = scan_type == ScanType.Tag
|
||||||
self.filterHardnessSlider.setEnabled(word_based)
|
self.filterHardnessSlider.setEnabled(word_based)
|
||||||
self.matchSimilarBox.setEnabled(word_based)
|
self.matchSimilarBox.setEnabled(word_based)
|
||||||
@ -129,7 +133,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self.tagTitleBox.setEnabled(tag_based)
|
self.tagTitleBox.setEnabled(tag_based)
|
||||||
self.tagGenreBox.setEnabled(tag_based)
|
self.tagGenreBox.setEnabled(tag_based)
|
||||||
self.tagYearBox.setEnabled(tag_based)
|
self.tagYearBox.setEnabled(tag_based)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from ..testapp import TestApp
|
from ..testapp import TestApp
|
||||||
@ -137,4 +141,5 @@ if __name__ == '__main__':
|
|||||||
dgapp = TestApp()
|
dgapp = TestApp()
|
||||||
dialog = PreferencesDialog(None, dgapp)
|
dialog = PreferencesDialog(None, dgapp)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# Created On: 2011-11-27
|
# Created On: 2011-11-27
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from qtlib.column import Column
|
from qtlib.column import Column
|
||||||
@ -29,4 +29,5 @@ class ResultsModel(ResultsModelBase):
|
|||||||
Column('percentage', defaultWidth=60),
|
Column('percentage', defaultWidth=60),
|
||||||
Column('words', defaultWidth=120),
|
Column('words', defaultWidth=120),
|
||||||
Column('dupe_count', defaultWidth=80),
|
Column('dupe_count', defaultWidth=80),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-05-10
|
# Created On: 2009-05-10
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from ._block_qt import getblocks
|
from ._block_qt import getblocks # NOQA
|
||||||
|
|
||||||
# Converted to C
|
# Converted to C
|
||||||
# def getblock(image):
|
# def getblock(image):
|
||||||
@ -24,7 +24,7 @@ from ._block_qt import getblocks
|
|||||||
# return (red // pixel_count, green // pixel_count, blue // pixel_count)
|
# return (red // pixel_count, green // pixel_count, blue // pixel_count)
|
||||||
# else:
|
# else:
|
||||||
# return (0, 0, 0)
|
# return (0, 0, 0)
|
||||||
#
|
#
|
||||||
# def getblocks(image, block_count_per_side):
|
# def getblocks(image, block_count_per_side):
|
||||||
# width = image.width()
|
# width = image.width()
|
||||||
# height = image.height()
|
# height = image.height()
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-04-29
|
# Created On: 2009-04-29
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@ -25,9 +25,9 @@ SCAN_TYPE_ORDER = [
|
|||||||
class PreferencesDialog(PreferencesDialogBase):
|
class PreferencesDialog(PreferencesDialogBase):
|
||||||
def __init__(self, parent, app):
|
def __init__(self, parent, app):
|
||||||
PreferencesDialogBase.__init__(self, parent, app)
|
PreferencesDialogBase.__init__(self, parent, app)
|
||||||
|
|
||||||
self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged)
|
self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged)
|
||||||
|
|
||||||
def _setupPreferenceWidgets(self):
|
def _setupPreferenceWidgets(self):
|
||||||
scanTypeLabels = [
|
scanTypeLabels = [
|
||||||
tr("Contents"),
|
tr("Contents"),
|
||||||
@ -49,25 +49,25 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self._setupAddCheckbox('debugModeBox', tr("Debug mode (restart required)"))
|
self._setupAddCheckbox('debugModeBox', tr("Debug mode (restart required)"))
|
||||||
self.widgetsVLayout.addWidget(self.debugModeBox)
|
self.widgetsVLayout.addWidget(self.debugModeBox)
|
||||||
self._setupBottomPart()
|
self._setupBottomPart()
|
||||||
|
|
||||||
def _load(self, prefs, setchecked):
|
def _load(self, prefs, setchecked):
|
||||||
scan_type_index = SCAN_TYPE_ORDER.index(prefs.scan_type)
|
scan_type_index = SCAN_TYPE_ORDER.index(prefs.scan_type)
|
||||||
self.scanTypeComboBox.setCurrentIndex(scan_type_index)
|
self.scanTypeComboBox.setCurrentIndex(scan_type_index)
|
||||||
setchecked(self.matchScaledBox, prefs.match_scaled)
|
setchecked(self.matchScaledBox, prefs.match_scaled)
|
||||||
|
|
||||||
def _save(self, prefs, ischecked):
|
def _save(self, prefs, ischecked):
|
||||||
prefs.scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
prefs.scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
||||||
prefs.match_scaled = ischecked(self.matchScaledBox)
|
prefs.match_scaled = ischecked(self.matchScaledBox)
|
||||||
|
|
||||||
def resetToDefaults(self):
|
def resetToDefaults(self):
|
||||||
self.load(preferences.Preferences())
|
self.load(preferences.Preferences())
|
||||||
|
|
||||||
#--- Events
|
#--- Events
|
||||||
def scanTypeChanged(self, index):
|
def scanTypeChanged(self, index):
|
||||||
scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
||||||
fuzzy_scan = scan_type == ScanType.FuzzyBlock
|
fuzzy_scan = scan_type == ScanType.FuzzyBlock
|
||||||
self.filterHardnessSlider.setEnabled(fuzzy_scan)
|
self.filterHardnessSlider.setEnabled(fuzzy_scan)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from ..testapp import TestApp
|
from ..testapp import TestApp
|
||||||
@ -75,4 +75,5 @@ if __name__ == '__main__':
|
|||||||
dgapp = TestApp()
|
dgapp = TestApp()
|
||||||
dialog = PreferencesDialog(None, dgapp)
|
dialog = PreferencesDialog(None, dgapp)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# Created On: 2011-11-27
|
# Created On: 2011-11-27
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from qtlib.column import Column
|
from qtlib.column import Column
|
||||||
@ -20,4 +20,5 @@ class ResultsModel(ResultsModelBase):
|
|||||||
Column('mtime', defaultWidth=120),
|
Column('mtime', defaultWidth=120),
|
||||||
Column('percentage', defaultWidth=60),
|
Column('percentage', defaultWidth=60),
|
||||||
Column('dupe_count', defaultWidth=80),
|
Column('dupe_count', defaultWidth=80),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
15
qt/se/app.py
15
qt/se/app.py
@ -1,9 +1,9 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-05-24
|
# Created On: 2009-05-24
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from core_se import __appname__
|
from core_se import __appname__
|
||||||
@ -18,6 +18,7 @@ from .preferences_dialog import PreferencesDialog
|
|||||||
|
|
||||||
class Directories(DirectoriesBase):
|
class Directories(DirectoriesBase):
|
||||||
ROOT_PATH_TO_EXCLUDE = frozenset(['windows', 'program files'])
|
ROOT_PATH_TO_EXCLUDE = frozenset(['windows', 'program files'])
|
||||||
|
|
||||||
def _default_state_for_path(self, path):
|
def _default_state_for_path(self, path):
|
||||||
result = DirectoriesBase._default_state_for_path(self, path)
|
result = DirectoriesBase._default_state_for_path(self, path)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
@ -30,16 +31,16 @@ class DupeGuru(DupeGuruBase):
|
|||||||
EDITION = 'se'
|
EDITION = 'se'
|
||||||
LOGO_NAME = 'logo_se'
|
LOGO_NAME = 'logo_se'
|
||||||
NAME = __appname__
|
NAME = __appname__
|
||||||
|
|
||||||
DETAILS_DIALOG_CLASS = DetailsDialog
|
DETAILS_DIALOG_CLASS = DetailsDialog
|
||||||
RESULT_MODEL_CLASS = ResultsModel
|
RESULT_MODEL_CLASS = ResultsModel
|
||||||
PREFERENCES_CLASS = Preferences
|
PREFERENCES_CLASS = Preferences
|
||||||
PREFERENCES_DIALOG_CLASS = PreferencesDialog
|
PREFERENCES_DIALOG_CLASS = PreferencesDialog
|
||||||
|
|
||||||
def _setup(self):
|
def _setup(self):
|
||||||
self.directories = Directories()
|
self.directories = Directories()
|
||||||
DupeGuruBase._setup(self)
|
DupeGuruBase._setup(self)
|
||||||
|
|
||||||
def _update_options(self):
|
def _update_options(self):
|
||||||
DupeGuruBase._update_options(self)
|
DupeGuruBase._update_options(self)
|
||||||
self.model.scanner.min_match_percentage = self.prefs.filter_hardness
|
self.model.scanner.min_match_percentage = self.prefs.filter_hardness
|
||||||
@ -48,4 +49,4 @@ class DupeGuru(DupeGuruBase):
|
|||||||
self.model.scanner.match_similar_words = self.prefs.match_similar
|
self.model.scanner.match_similar_words = self.prefs.match_similar
|
||||||
threshold = self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0
|
threshold = self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0
|
||||||
self.model.scanner.size_threshold = threshold * 1024 # threshold is in KB. the scanner wants bytes
|
self.model.scanner.size_threshold = threshold * 1024 # threshold is in KB. the scanner wants bytes
|
||||||
|
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
# Created By: Virgil Dupras
|
# Created By: Virgil Dupras
|
||||||
# Created On: 2009-05-24
|
# Created On: 2009-05-24
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from PyQt5.QtCore import QSize
|
from PyQt5.QtCore import QSize
|
||||||
from PyQt5.QtWidgets import (QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget,
|
from PyQt5.QtWidgets import (
|
||||||
QLineEdit, QApplication)
|
QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget,
|
||||||
|
QLineEdit, QApplication
|
||||||
|
)
|
||||||
|
|
||||||
from hscommon.plat import ISWINDOWS, ISLINUX
|
from hscommon.plat import ISWINDOWS, ISLINUX
|
||||||
from hscommon.trans import trget
|
from hscommon.trans import trget
|
||||||
@ -31,9 +33,9 @@ SCAN_TYPE_ORDER = [
|
|||||||
class PreferencesDialog(PreferencesDialogBase):
|
class PreferencesDialog(PreferencesDialogBase):
|
||||||
def __init__(self, parent, app, **kwargs):
|
def __init__(self, parent, app, **kwargs):
|
||||||
super().__init__(parent, app, **kwargs)
|
super().__init__(parent, app, **kwargs)
|
||||||
|
|
||||||
self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged)
|
self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged)
|
||||||
|
|
||||||
def _setupPreferenceWidgets(self):
|
def _setupPreferenceWidgets(self):
|
||||||
scanTypeLabels = [
|
scanTypeLabels = [
|
||||||
tr("Filename"),
|
tr("Filename"),
|
||||||
@ -73,23 +75,26 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||||
self.horizontalLayout_2.addItem(spacerItem1)
|
self.horizontalLayout_2.addItem(spacerItem1)
|
||||||
self.verticalLayout_4.addLayout(self.horizontalLayout_2)
|
self.verticalLayout_4.addLayout(self.horizontalLayout_2)
|
||||||
self._setupAddCheckbox('ignoreHardlinkMatches', tr("Ignore duplicates hardlinking to the same file"), self.widget)
|
self._setupAddCheckbox(
|
||||||
|
'ignoreHardlinkMatches',
|
||||||
|
tr("Ignore duplicates hardlinking to the same file"), self.widget
|
||||||
|
)
|
||||||
self.verticalLayout_4.addWidget(self.ignoreHardlinkMatches)
|
self.verticalLayout_4.addWidget(self.ignoreHardlinkMatches)
|
||||||
self._setupAddCheckbox('debugModeBox', tr("Debug mode (restart required)"), self.widget)
|
self._setupAddCheckbox('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()
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
PreferencesDialogBase._setupUi(self)
|
PreferencesDialogBase._setupUi(self)
|
||||||
|
|
||||||
if ISLINUX:
|
if ISLINUX:
|
||||||
# Under linux, whether it's a Qt layout bug or something else, the size threshold text edit
|
# Under linux, whether it's a Qt layout bug or something else, the size threshold text edit
|
||||||
# doesn't have enough space, so we make the pref pane higher to compensate.
|
# doesn't have enough space, so we make the pref pane higher to compensate.
|
||||||
self.resize(self.width(), 530)
|
self.resize(self.width(), 530)
|
||||||
elif ISWINDOWS:
|
elif ISWINDOWS:
|
||||||
self.resize(self.width(), 440)
|
self.resize(self.width(), 440)
|
||||||
|
|
||||||
def _load(self, prefs, setchecked):
|
def _load(self, prefs, setchecked):
|
||||||
scan_type_index = SCAN_TYPE_ORDER.index(prefs.scan_type)
|
scan_type_index = SCAN_TYPE_ORDER.index(prefs.scan_type)
|
||||||
self.scanTypeComboBox.setCurrentIndex(scan_type_index)
|
self.scanTypeComboBox.setCurrentIndex(scan_type_index)
|
||||||
@ -97,17 +102,17 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
setchecked(self.wordWeightingBox, prefs.word_weighting)
|
setchecked(self.wordWeightingBox, prefs.word_weighting)
|
||||||
setchecked(self.ignoreSmallFilesBox, prefs.ignore_small_files)
|
setchecked(self.ignoreSmallFilesBox, prefs.ignore_small_files)
|
||||||
self.sizeThresholdEdit.setText(str(prefs.small_file_threshold))
|
self.sizeThresholdEdit.setText(str(prefs.small_file_threshold))
|
||||||
|
|
||||||
def _save(self, prefs, ischecked):
|
def _save(self, prefs, ischecked):
|
||||||
prefs.scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
prefs.scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
||||||
prefs.match_similar = ischecked(self.matchSimilarBox)
|
prefs.match_similar = ischecked(self.matchSimilarBox)
|
||||||
prefs.word_weighting = ischecked(self.wordWeightingBox)
|
prefs.word_weighting = ischecked(self.wordWeightingBox)
|
||||||
prefs.ignore_small_files = ischecked(self.ignoreSmallFilesBox)
|
prefs.ignore_small_files = ischecked(self.ignoreSmallFilesBox)
|
||||||
prefs.small_file_threshold = tryint(self.sizeThresholdEdit.text())
|
prefs.small_file_threshold = tryint(self.sizeThresholdEdit.text())
|
||||||
|
|
||||||
def resetToDefaults(self):
|
def resetToDefaults(self):
|
||||||
self.load(preferences.Preferences())
|
self.load(preferences.Preferences())
|
||||||
|
|
||||||
#--- Events
|
#--- Events
|
||||||
def scanTypeChanged(self, index):
|
def scanTypeChanged(self, index):
|
||||||
scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
|
||||||
@ -115,7 +120,7 @@ class PreferencesDialog(PreferencesDialogBase):
|
|||||||
self.filterHardnessSlider.setEnabled(word_based)
|
self.filterHardnessSlider.setEnabled(word_based)
|
||||||
self.matchSimilarBox.setEnabled(word_based)
|
self.matchSimilarBox.setEnabled(word_based)
|
||||||
self.wordWeightingBox.setEnabled(word_based)
|
self.wordWeightingBox.setEnabled(word_based)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
from ..testapp import TestApp
|
from ..testapp import TestApp
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# Created On: 2011-11-27
|
# Created On: 2011-11-27
|
||||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||||
#
|
#
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||||
# 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.hardcoded.net/licenses/bsd_license
|
# http://www.hardcoded.net/licenses/bsd_license
|
||||||
|
|
||||||
from qtlib.column import Column
|
from qtlib.column import Column
|
||||||
@ -19,4 +19,5 @@ class ResultsModel(ResultsModelBase):
|
|||||||
Column('percentage', defaultWidth=60),
|
Column('percentage', defaultWidth=60),
|
||||||
Column('words', defaultWidth=120),
|
Column('words', defaultWidth=120),
|
||||||
Column('dupe_count', defaultWidth=80),
|
Column('dupe_count', defaultWidth=80),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
pytest>=2.0.0
|
pytest>=2.0.0
|
||||||
pytest-monkeyplus>=1.0.0
|
pytest-monkeyplus>=1.0.0
|
||||||
|
flake8
|
||||||
|
|
||||||
|
17
tox.ini
Normal file
17
tox.ini
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[tox]
|
||||||
|
envlist = py33,py34
|
||||||
|
skipsdist = True
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
commands =
|
||||||
|
flake8
|
||||||
|
py.test core core_se core_me core_pe hscommon
|
||||||
|
deps =
|
||||||
|
-r{toxinidir}/requirements.txt
|
||||||
|
-r{toxinidir}/requirements-extra.txt
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
exclude = .tox,env,build,hscommon,qtlib,cocoalib,cocoa,help,./get-pip.py,./qt/dg_rc.py,./core*/tests,qt/run_template.py,cocoa/run_template.py,./run.py,./pkg
|
||||||
|
max-line-length = 120
|
||||||
|
ignore = W391,W293,E302,E261,E226,E227,W291,E262,E303,E265
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user