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:
Virgil Dupras 2014-10-13 15:08:59 -04:00
parent 24643a9b5d
commit 2166a0996c
46 changed files with 794 additions and 612 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@
*.waf* *.waf*
.lock-waf* .lock-waf*
.idea .idea
.tox
build build
dist dist

121
build.py
View File

@ -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')

View File

@ -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)

View File

@ -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):

View File

@ -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 []

View File

@ -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

View File

@ -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:&quot;Courier New&quot;,monospace; font-family:&quot;Courier New&quot;,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>

View File

@ -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
""" """

View File

@ -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
""" """

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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)

View File

@ -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

View File

@ -1,2 +1,3 @@
__version__ = '6.8.0' __version__ = '6.8.0'
__appname__ = 'dupeGuru Music Edition' __appname__ = 'dupeGuru Music Edition'

View File

@ -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)

View File

@ -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

View File

@ -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
]

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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])

View File

@ -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

View File

@ -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())

View File

@ -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
]

View File

@ -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"

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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_())

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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_())

View File

@ -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())

View File

@ -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_())

View File

@ -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),
] ]

View File

@ -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()

View File

@ -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_())

View File

@ -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),
] ]

View File

@ -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

View File

@ -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

View File

@ -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),
] ]

View File

@ -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
View 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